Skip to content

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

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk

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

  1. 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.
  2. POST /api/v1/data/qow_task {"name":"orphan-task"} → 201, project: null (id ROW).
  3. 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.
  4. 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.

Activity

  1. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 1, 2026
  2. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    • 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.

    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 1, 2026
  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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/main 2678600d: 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/main 2678600d

    State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim. Order: pm-orders/server/order-28665.md in the project.

  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  6. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

    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).

  7. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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 its Fixes line.

    reading value
    PR state merged, 0b63f5f4c5
    card closed (completed); pm:dispatched removed in this stroke, domain:* / area:* kept

    No domain:ui follow-up owed (ACCEPT 6559653275, item 5).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions