You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Studio OWD: forcing "Controlled by parent" on a detail that already holds rows with a null master hides those rows from everyone, admin included, while the confirm only says they "may fail validation" #28665
Filed by the PM seat for the W3-RT runner session_017xRBcRX4wwwVw5bEckffdp, whose own write was refused by its session permissions (RUNNER rule 2: staged text, posted verbatim through the same door).
QA-source: #28656 · studio-authoring.access-owd-and-suggestions · observed beside the clauses
What happens
Since #27458, saving "Controlled by parent" on a detail whose master_detail is not yet required answers 409 DESTRUCTIVE_CHANGE. The panel opens a confirm, "Make the relationship required? … Field 'project' is now required but has no default value…", and "Make required and save" re-sends the draft with ?force=true. The confirm relays only that "existing rows with null values may fail validation".
What actually happens to such rows: after publish, a pre-existing detail row with a null master becomes unreachable through every door, for the admin too. The list answers total: 0, and GET and PATCH /api/v1/data/OBJ/ID answer 404 RECORD_NOT_FOUND, while the row is still stored. That is ADR-0055 fail-closed access derivation (no master, so no access), not a security hole. But the author is told "may fail validation" and gets no count of the rows they are about to hide and no way to reach them afterwards.
Expected: the confirm states how many existing rows have no master and that they become unreadable once the object is controlled by its parent, and ideally offers to stop or to assign them first. Alternatively, an admin door keeps such rows reachable for repair.
Closest prior card: #24964 (closed), "a master-detail row whose master is missing can be neither read nor removed through any door". This is the null-master case, reached through the #27458 force path.
Measured at framework 6e11a929 (main, 2026-11-01), on a stock showcase boot with a scratch package installed at runtime. Measured through the API with the request shape the panel's confirm sends, not by clicking the panel.
Reproduction
As admin, install a scratch package PKG. Publish qow_project (private) and qow_task (private), with project as a master_detail to qow_project, not required.
POST /api/v1/data/qow_task {"name":"orphan-task"} → 201, project: null (id ROW).
PUT /api/v1/meta/object/qow_task?mode=draft&package=PKG&force=true with sharingModel: "controlled_by_parent" and fields.project.required: true → 200, which is what "Make required and save" sends. Then POST /api/v1/packages/PKG/publish-drafts.
As admin: GET /api/v1/data/qow_task → total: 0. GET /api/v1/data/qow_task/ROW → 404 RECORD_NOT_FOUND. PATCH /api/v1/data/qow_task/ROW {"project":"P"} → 404. The row is still in the table.
Mechanism
The DESTRUCTIVE_CHANGE refusal for "field became required, no default" is generic. It does not know that, under controlled_by_parent, a null master also removes every reader (plugin-security derives the detail's access from its master, ADR-0055 §1, and fails closed). The Studio confirm (PackageOwdOverviewPanel, the #27458 path) relays that generic message.
Read fresh. After "Controlled by parent" is forced on a detail, its existing null-master rows become unreachable through every door, the admin's included (ADR-0055 fail-closed). The confirm only says they "may fail validation". The rows stay stored, so this is no leak. It is a silent loss of access with an understated warning. p2.
Lane: domain:server, where the destructive-change answer counts the null-master rows and names them in the 409. The Studio confirm then relays that count, in the same PR or as a domain:ui follow-up. The claim names the files.
Claim: PM loop round 1 · card thread · 2026-11-01T13:24Z
Session: session_01FEkQ5oc56xChD6Wrc4cGSt (the seat); the card thread is session_01FEkQ5miRY4M71TfnNGvmsh
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-wm509f (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#5
Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z) with the triage grade as filed; split among the server seats agreed in the project (seat 1 takes #28564, seat 5 takes #28565#28661#28658#28663#28665).
File surface: packages/metadata-protocol/src/protocol.ts, only the destructive-change judgement region (the function holding field_required_no_default), plus a helper module if the count needs one; tests; a @objectstack/metadata-protocol changeset. ⛔ No ui/** (the Studio confirm relay is an owed domain:ui follow-up the seat files), ⛔ no plugin-security, ⛔ no packages/spec/**.
Container & model: M, mode:thread, model and effort as the thread was opened
Clause-②: no
Metadata-protocol: none
Responsibility: the code is @objectstack/metadata-protocol's destructive-change answer · the platform path is ADR-0055 fail-closed derivation, correct by design; the defect is the understated 409 text · reached by any admin forcing "Controlled by parent" on a detail with null-master rows, used today through Studio's OWD panel
Thread-read: 6558255783
Premise: holds (read at origin/main2678600d: protocol.ts builds field_required_no_default with the generic "may fail validation" text and reads no rows)
Serial constraints cleared: #28564 (seat 1) edits protocol.ts active doors and duplicatePackage; #25467 (os-zhuang) edits parseCommitItems; this card stays in the destructive-change judgement region and merges origin/main before the PR.
Resume-from: none — fresh from origin/main2678600d
State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28665.md in the project.
os-dev-report
{
"issue": 28665,
"status": "done",
"branch": "claude/project-thread-wm509f",
"pr": "#28710",
"session": "session_01FEkQ5miRY4M71TfnNGvmsh",
"premise_still_valid": true,
"summary": "The field_required_no_default finding on the master_detail of a body whose sharingModel is controlled_by_parent (switched in this save, or already) now counts the stored rows whose master is null. When there are any, its message (and so the 409 text) says how many, that they become unreadable through every door, the admin's included, once the object is controlled by its parent (ADR-0055), and to set the master on them first. The count is one system-authority engine.count on that column, made in the saveMetaItem gate through a new helper module (protocol-destructive-null-master.ts); detectDestructiveObjectChanges is untouched. There is no count on any other save or under force. Zero rows or a failed count leaves today's text. No code or member was added (Clause-② no, Metadata-protocol none). PackageOwdOverviewPanel already renders each issues[].message in its confirm, so the count reaches Studio with no ui change. The field to relay is issues[].message of the field_required_no_default entry. Card assignee was not touched.",
"tests": "New packages/objectql/src/protocol-destructive-null-master.test.ts, 8 cases through the real saveMetaItem gate. RED on base (metadata-protocol dist rebuilt from 2678600): Tests 3 failed | 5 passed (8). The failures are 'expected ... to contain 2 existing rows have no project', '... 1 existing row ...', and 'vi.fn() to be called 1 times, but got 0 times'. GREEN at 40638b7 and again at final head 01fa82d after the merge and a rebuild: with protocol-destructive.test.ts, Test Files 2 passed (2), Tests 15 passed (15), VERDICT command-exit 0. ABLATION: ablation-replace on the anchor 'if (masters.length === 0) return findings;' with '>= 0'. Mutation landed (blob b97adc28f536 to 2a91551eaa9a), the dist preflight found the marker in 2 built files, Tests 3 failed | 5 passed. Restored blob == HEAD with git diff HEAD empty; after a rebuild the preflight --absent passed (24 files) and Tests 8 passed (8). LIVE PROBE on a stock showcase (pnpm dev -- --fresh, package app.qow, steps 1-3 of the card with 2 orphan rows and 1 owned): base 409 'Field project is now required but has no default value — existing rows with null values may fail validation.'; head 409 '... 2 existing rows have no project, and once the object is controlled by its parent they become unreadable through every door, the admin's included ... Set project on each of them first.' On both, force=true gives 200, publish gives published 1, and the admin list gives total 1. Final head 01fa82d: metadata-protocol typecheck exit 0, its suite 291 files / 21950 tests passed (4 files / 26 tests skipped); objectql typecheck exit 0, its --project local suite 513 files / 9906 tests passed.",
"gates": "dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 62 families at 01fa82d (the same list as at 40638b7). Each was run, with exit 0 captured before any pipe. --ran reconciles: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN. Record: pm-orders/server/28665-gates-ran.txt in the project. In the first pass at 40638b7, check-plugin-teardown-shape.mjs --self-test gave exit 3 on the shallow clone; after git fetch --depth=1 of 621a487 it gave 0, and 0 on the final pass. eslint --no-inline-config packages/metadata-protocol packages/objectql exit 0 at 01fa82d. The CI check-runs on PR 28710 are in_progress at report time; the seat holds them.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 — relay pr_create (fleet-write run 41317304461, PR 28710, body read back identical 12180 bytes, assignees none, no labels sent: the order names none and a changeset is present) and this os-dev-report comment via post-stamped. Plus 1 git push (not a REST write).",
"open_questions": [
{ "question": "A detail switched to controlled_by_parent whose master_detail was ALREADY required raises no finding today, so no 409 carries a count, yet masterless rows can exist there (rows stored before an earlier forced required toggle). Should that save warn too?", "options": ["A: leave it; this PR covers every save that already answers 409, and Studio always sends the master required in the same save, so the panel path is covered", "B: a new forceable finding when sharingModel becomes controlled_by_parent and the count is positive. That is a new 409 on a save that passes today (a narrowing arm, a decision for the maintainer under ruling 2)"], "recommendation": "A, because ruling 2 says not a new block, and the reachable author path (Studio OWD) is covered by this PR" }
],
"out_of_scope_findings": []
}
ACCEPT → PR #28710 comment 6559653275 · head 01fa82d0 · 2026-11-01T15:01Z. Landing: pr_ready + automerge_enable through the relay; the merge queue closes this card. The domain:ui follow-up the order named is not owed (the Studio confirm already renders issues[].message; see the ACCEPT, item 5).
Activity
objectstack-fleet commentedon Nov 1, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 真挡得住的权限 | none | P②
Triage: p2 ·
bug·domain:server·area:access·pm:queue· 2026-11-01T12:57Z⛔ Not a claim.
domain:server, where the destructive-change answer counts the null-master rows and names them in the 409. The Studio confirm then relays that count, in the same PR or as adomain:uifollow-up. The claim names the files.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Claim: PM loop round 1 · card thread · 2026-11-01T13:24Z
Session:
session_01FEkQ5oc56xChD6Wrc4cGSt(the seat); the card thread issession_01FEkQ5miRY4M71TfnNGvmshAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-wm509f(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#5Ruling-ref: 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z) with the triage grade as filed; split among the server seats agreed in the project (seat 1 takes #28564, seat 5 takes #28565 #28661 #28658 #28663 #28665).
File surface:
packages/metadata-protocol/src/protocol.ts, only the destructive-change judgement region (the function holdingfield_required_no_default), plus a helper module if the count needs one; tests; a@objectstack/metadata-protocolchangeset. ⛔ Noui/**(the Studio confirm relay is an oweddomain:uifollow-up the seat files), ⛔ no plugin-security, ⛔ nopackages/spec/**.Container & model:
M,mode:thread, model and effort as the thread was openedClause-②: no
Metadata-protocol: none
Responsibility: the code is
@objectstack/metadata-protocol's destructive-change answer · the platform path is ADR-0055 fail-closed derivation, correct by design; the defect is the understated 409 text · reached by any admin forcing "Controlled by parent" on a detail with null-master rows, used today through Studio's OWD panelThread-read: 6558255783
Premise: holds (read at
origin/main2678600d:protocol.tsbuildsfield_required_no_defaultwith the generic "may fail validation" text and reads no rows)Serial constraints cleared: #28564 (seat 1) edits
protocol.tsactive doors andduplicatePackage; #25467 (os-zhuang) editsparseCommitItems; this card stays in the destructive-change judgement region and mergesorigin/mainbefore the PR.Resume-from: none — fresh from
origin/main2678600dState:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim. Order:pm-orders/server/order-28665.mdin the project.objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5miRY4M71TfnNGvmsh
os-dev-report
{
"issue": 28665,
"status": "done",
"branch": "claude/project-thread-wm509f",
"pr": "#28710",
"session": "session_01FEkQ5miRY4M71TfnNGvmsh",
"premise_still_valid": true,
"summary": "The field_required_no_default finding on the master_detail of a body whose sharingModel is controlled_by_parent (switched in this save, or already) now counts the stored rows whose master is null. When there are any, its message (and so the 409 text) says how many, that they become unreadable through every door, the admin's included, once the object is controlled by its parent (ADR-0055), and to set the master on them first. The count is one system-authority engine.count on that column, made in the saveMetaItem gate through a new helper module (protocol-destructive-null-master.ts); detectDestructiveObjectChanges is untouched. There is no count on any other save or under force. Zero rows or a failed count leaves today's text. No code or member was added (Clause-② no, Metadata-protocol none). PackageOwdOverviewPanel already renders each issues[].message in its confirm, so the count reaches Studio with no ui change. The field to relay is issues[].message of the field_required_no_default entry. Card assignee was not touched.",
"tests": "New packages/objectql/src/protocol-destructive-null-master.test.ts, 8 cases through the real saveMetaItem gate. RED on base (metadata-protocol dist rebuilt from 2678600): Tests 3 failed | 5 passed (8). The failures are 'expected ... to contain 2 existing rows have no project', '... 1 existing row ...', and 'vi.fn() to be called 1 times, but got 0 times'. GREEN at 40638b7 and again at final head 01fa82d after the merge and a rebuild: with protocol-destructive.test.ts, Test Files 2 passed (2), Tests 15 passed (15), VERDICT command-exit 0. ABLATION: ablation-replace on the anchor 'if (masters.length === 0) return findings;' with '>= 0'. Mutation landed (blob b97adc28f536 to 2a91551eaa9a), the dist preflight found the marker in 2 built files, Tests 3 failed | 5 passed. Restored blob == HEAD with git diff HEAD empty; after a rebuild the preflight --absent passed (24 files) and Tests 8 passed (8). LIVE PROBE on a stock showcase (pnpm dev -- --fresh, package app.qow, steps 1-3 of the card with 2 orphan rows and 1 owned): base 409 'Field project is now required but has no default value — existing rows with null values may fail validation.'; head 409 '... 2 existing rows have no project, and once the object is controlled by its parent they become unreadable through every door, the admin's included ... Set project on each of them first.' On both, force=true gives 200, publish gives published 1, and the admin list gives total 1. Final head 01fa82d: metadata-protocol typecheck exit 0, its suite 291 files / 21950 tests passed (4 files / 26 tests skipped); objectql typecheck exit 0, its --project local suite 513 files / 9906 tests passed.",
"gates": "dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 62 families at 01fa82d (the same list as at 40638b7). Each was run, with exit 0 captured before any pipe. --ran reconciles: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN. Record: pm-orders/server/28665-gates-ran.txt in the project. In the first pass at 40638b7, check-plugin-teardown-shape.mjs --self-test gave exit 3 on the shallow clone; after git fetch --depth=1 of 621a487 it gave 0, and 0 on the final pass. eslint --no-inline-config packages/metadata-protocol packages/objectql exit 0 at 01fa82d. The CI check-runs on PR 28710 are in_progress at report time; the seat holds them.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 — relay pr_create (fleet-write run 41317304461, PR 28710, body read back identical 12180 bytes, assignees none, no labels sent: the order names none and a changeset is present) and this os-dev-report comment via post-stamped. Plus 1 git push (not a REST write).",
"open_questions": [
{ "question": "A detail switched to controlled_by_parent whose master_detail was ALREADY required raises no finding today, so no 409 carries a count, yet masterless rows can exist there (rows stored before an earlier forced required toggle). Should that save warn too?", "options": ["A: leave it; this PR covers every save that already answers 409, and Studio always sends the master required in the same save, so the panel path is covered", "B: a new forceable finding when sharingModel becomes controlled_by_parent and the count is positive. That is a new 409 on a save that passes today (a narrowing arm, a decision for the maintainer under ruling 2)"], "recommendation": "A, because ruling 2 says not a new block, and the reachable author path (Studio OWD) is covered by this PR" }
],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
ACCEPT → PR #28710 comment 6559653275 · head
01fa82d0· 2026-11-01T15:01Z. Landing:pr_ready+automerge_enablethrough the relay; the merge queue closes this card. Thedomain:uifollow-up the order named is not owed (the Studio confirm already rendersissues[].message; see the ACCEPT, item 5).objectstack-fleet commentedon Nov 1, 2026
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Landed · 2026-11-01T15:20Z · PR #28710 merged through the merge queue as
0b63f5f4c5; this card closed as completed by itsFixesline.0b63f5f4c5pm:dispatchedremoved in this stroke,domain:*/area:*keptNo
domain:uifollow-up owed (ACCEPT 6559653275, item 5).