Skip to content

plugin-security: the controlled_by_parent detail write gate's master-edit check may not ask the same question as the master's own write door (reach not measured; detail withheld under the disclosure ruling) #24986

Closed

Description

@objectstack-fleet

Filing gate ①, the security exception ("may leak data": measure reach first). Filed by the domain:services PM seat, seat 1 (seat post #6021, session_011TdZ8GjixNZFLz1xJD9zzK), from a source reading by #24962's dev (PR #24985). ⛔ Not a claim. Unlabelled and ungraded for triage.

Disclosure: the maintainer's ruling recorded on #9387 (5323310017) applies. This card records only that the reading exists, where it lives, and the governing text. ⛔ No probe, grant shape, request or served value goes on this card, its PR or any comment. The seat holds the detail privately.

Reader, and what they do: triage grades it. Then the domain:services lane (plugin-security) measures reach FIRST, and only on reach does it fix.

Where it lives (origin/main f32d795ce5)

  • The check: packages/plugins/plugin-security/src/security-plugin.ts, assertMasterRowEditable (~:9214), the object-level leg of the controlled_by_parent detail write gate.
  • The comparison: the master object's own write door (the security middleware's object-permission step).
  • The reading: the two may not ask the same question of the permission evaluator for every master posture.

Governing text

ADR-0055 §4 (docs/adr/0055-master-detail-controlled-by-parent.md:66): a member who cannot edit master M cannot write a detail under M. The detail write gate's master leg must answer as the master's own write door answers.

Status of the reading

Dedupe


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage (R547): p1 · domain:services · area:access · bug + security · pm:queue. Measure reach first, then fix only if reach is shown. · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-20T21:54Z


    Generated by Claude Code

  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    priority:p1High: required for production / M2
    on Oct 20, 2026
  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 20 · 2026-10-20T22:30Z
    Session: session_011TdZ8GjixNZFLz1xJD9zzK
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24986-cbp-master-edit-leg
    Worktree: objectstack-issue-24986
    Domain: domain:services
    Seat: domain:services#1
    File surface: measure first. Only on shown reach: packages/plugins/plugin-security/src/security-plugin.ts, assertMasterRowEditable's object-level leg (~:9214 at f32d795ce5) and nothing else in that file, NEW tests under plugin-security/src/, and one new .changeset/24986-*.md. ⛔ Not packages/objectql/**, ⛔ not packages/spec/**, ⛔ no widening anywhere (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: default (opus), quoting the --tier output "no path-derived mandate … floor sonnet · default opus · ceiling fable"
    Clause-②: no
    Responsibility: plugin-security's controlled_by_parent detail write gate asks the master's object-level edit question in a form that may differ from the master's own write door | ADR-0055 §4 states the rule | not measured: reach is this dispatch's first act, and the card is graded as a possible integrity leak until then
    Thread-read: 6302536575
    Serial constraints cleared: Region-serial in security-plugin.ts with PR #24985 (#24962, this seat, in the merge queue; computeControlledByParentFilter) and PR #24988 (seat 2's #24757; reportNameFoldCollisions). Both are disjoint from assertMasterRowEditable. Whichever lands later merges main first. #24932 (seat 2's, needs-user-decision) is not in flight.

    Order, per triage R547: measure reach first. Fix only if reach is shown; with no reach, the report states so and the card closes on the readings.

    Disclosure (#9387, 5323310017): ⛔ no probe, grant shape or request on any public surface. Concrete readings go to the seat privately.

    Readings taken on origin/main 7adfaec2a1.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24986,
      "status": "done",
      "branch": "claude/issue-24986-cbp-master-edit-leg",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25008",
      "session": "session_011TdZ8GjixNZFLz1xJD9zzK — subagent run; the parent's harness-stamped id (commit trailer Claude-Session)",
      "premise_still_valid": true,
      "summary": "Reach measured first, on a real ObjectQL + SqlDriver + SecurityPlugin (throwaway probes, never committed; e4d604efd3, which includes #24985). Reach is SHOWN: for a class of principals the master's own write door refuses at its object gate on an update of the master, while they may read it and the detail's own door admits them, the master-edit leg was the deciding check and admitted them. Insert, by-id update and by-id delete of a detail landed, and checkControlledByParentWrite answered allow. Two inputs produce the class independently (the master's posture; a capability the master requires for update), and the grant half has a named real producer. Gates ahead of the leg: a principal that cannot read the master is stopped by the read door (reference check on insert; read scope on by-id, since #24985), and the on-behalf-of agent half is stopped the same way where the ceiling does not cover the master; the delegator half is the direct case. Fix (PR #25008, draft): leg 1 of assertMasterRowEditable now asks the master door's object-level update question with the door's inputs from getObjectSecurityMeta (posture resolves; required update capabilities held; grant asked under the posture). No other region touched, no rule re-derived, no widening. The capability and unresolved-posture parts go beyond the card's lead under the bounded in-place rule (all four conditions hold); both are recorded in open_questions for a veto. Pins: new controlled-by-parent-master-object-write.test.ts (13 tests), one ablation leg per pinned behaviour. Declared envelope moves: such an insert now answers PERMISSION_DENIED/403 ahead of the reference check (was VALIDATION_FAILED for a principal that also cannot read the master; same 403 for an existing and a nonexistent master id, pinned); an insert under an unresolvable master object answers 403 (was OBJECT_NOT_FOUND/404). Concrete probes and served values are withheld per the #9387 ruling (5323310017) and held by the seat. The worktree and its node_modules are removed right after this comment posts.",
      "tests": "All under scripts/pm/os-verify-lock.sh, VERDICT lines read. Reach probes (throwaway, deleted, never committed): 4 runs, VERDICT command-exit 0 each; readings private. Post-fix re-run of the same probes: every reach row refused 403 / leg object_permission; controls unchanged. New suite pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/controlled-by-parent-master-object-write.test.ts: 13 passed. Ablation after the fix was committed (38081b6a05), node scripts/ablation-replace.mjs wrap mode under a trap-armed driver, subject imported from src (relative ./security-plugin.js, no dist in the path): posture option deleted -> 3 failed / 10 passed; capability check disabled -> 1 failed / 12 passed; unresolved check disabled -> 2 failed / 11 passed (insert answered OBJECT_NOT_FOUND/404, check answered allow). Each restore proven: blob 7dbb423417f0 == HEAD and git diff HEAD empty. Package, at b24e619107 (code byte-identical to final head 85f0113f1e; the last commit edits .changeset only): pnpm --filter @objectstack/plugin-security test -> Test Files 199 passed, Tests 4114 passed / 45 skipped; typecheck -> exit 0 (tsc x2 + check:test-typecheck OK). Before the merge of main (ac70f18533): 4113 passed / 45 skipped, typecheck exit 0. test:repo not run locally (diff does not touch it; CI Test Core runs it).",
      "gates": "Closure build pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-security^...' build: VERDICT command-exit 0 (at e4d604efd3, rebuilt after the merge at b24e619107: exit 0). node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at final head 85f0113f1e: 69 commands, each exit captured before any pipe, 69 exit 0; --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). Same union at b24e619107: 69/69 exit 0, --ran 0 unrun. At ac70f18533: 67 exit 0, 2 exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:i18n; workspace build absent), both green at later heads. Runner PIDs: 17481 (ac70f18533), 19302 (b24e619107, lock queue-timeout 99, NOT MEASURED, nothing ran), re-queued run under the kept slot (b24e619107, completed), 1657 (85f0113f1e). PR check-runs at 85f0113f1e when this report was written: No other open PR may claim the same issue: success; 18.x upgrade harness (opt-in): skipped; Packed-tarball smoke (opt-in): skipped; Check Changeset, Governed Surface Queue Guard, Lint (CI release & engine gates, ESLint, PM dispatch-gates, docs & code guards, repo & tooling gates, scoped x3), Type Check (consumer gates, debt ledger, source gates, workspace), The card this PR closes must claim this branch, Part-of PR must not also close its card, single-writer path, Check PR Size, Check Documentation Links, docs-affected, filter: in_progress. Test Core and Build Core had not been scheduled yet. Not waited on, per the dispatch contract.",
      "mcp_calls": "0 — no MCP GitHub tool called (reads went through gh api REST reads)",
      "api_writes": "2 — fleet relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/25008/assignees (os-justin), read back identical; (2) comment -> POST /repos/objectstack-ai/objectstack/issues/24986/comments (this report). Not REST: git push x5 to claude/issue-24986-cbp-master-edit-leg (empty branch probe, fix, changeset, merge of main, changeset trim).",
      "open_questions": [
        {
          "question": "Scope: the card's lead names one input (the master's posture). The fix also makes the leg ask the master door's capability prerequisite and refuse an unresolvable master posture. Keep all three?",
          "options": [
            "A — all three (as shipped): the leg asks exactly the door's object-level update question. Cost: two extra if-blocks, each pinned and ablated.",
            "B — posture only: drop the capability block and the unresolved block. Cost: a measured reach (a master's declared update capability is not enforced for its details) stays open, and an unresolved posture keeps reading as not private on this leg."
          ],
          "recommendation": "A. Real need: the capability case has measured reach at every by-id and insert door. Long-term: one question for the leg and the door, as every other door pairs the posture with its unresolved case. AI authoring: a declared requiredPermissions.update is enforced for details too (declared = enforced). Startup scope: no new capability, no new gate."
        },
        {
          "question": "An insert under a master object that does not resolve now answers PERMISSION_DENIED/403 on the object_permission leg (the master door's own answer for an unresolved posture) where the engine answered OBJECT_NOT_FOUND/404. Keep, or answer it as a non-verdict?",
          "options": [
            "A — keep the 403 (as shipped): matches the master's own door; the reach it closes was checkControlledByParentWrite answering allow for such a detail.",
            "B — answer a non-verdict (unresolvable): needs a new reason in the spec vocabulary (ControlledByParentWriteUnresolvableReason), a packages/spec change this dispatch forbids."
          ],
          "recommendation": "A, because it is the door's own answer, it never widens, and B is a spec-contract change for a separate card if the maintainer wants that envelope."
        }
      ],
      "out_of_scope_findings": [
        "class: b · reach: measured at a public engine write door on this run's real stack; exception: security (may alter data), so the concrete readings are held privately · evidence: ADR-0055 §3 ('editing/deleting/creating a detail requires edit access to its master') against one write path of a controlled_by_parent detail that never asks the master-edit gate; probes and served values private · Seam: spec:object.sharingModel controlled_by_parent → runtime:SecurityPlugin.assertControlledByParentWrite · dedupe words: controlled_by_parent detail write master edit not asked, cbp write path master edit access, ADR-0055 section 3 write path",
        "carrier: none (承接者:无) · noted in PR #25008 Acceptance notes, not filed — the @objectstack/spec TSDoc of ControlledByParentWriteDenialLeg reads object_permission as 'no object-level update grant on a master'; that leg now also carries the capability and unresolved-posture refusals of the door's object gate"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25008 comment 6305076014 · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-21T00:39Z


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-21T01:09Z · ⛔ The #9387 disclosure rule applies.

    reading result
    PR #25008 timeline added_to_merge_queue 2026-10-21T00:42Z → merged 2026-10-21T01:08Z
    origin/main by content squash aaee0840be (one parent, 62bc129308), 3 files. assertMasterRowEditable's object-level leg reads the master's posture from getObjectSecurityMeta and asks the door's three questions
    closing references the PR body closes #24986 only

    State change in this stroke: pm:dispatched off and the assignee cleared. The card closed completed on the merge.

    What changes: a controlled_by_parent detail write asks its master the object-level update question the master's own write door asks (ADR-0055 §3). The posture must resolve, the required update capabilities must be held, and the grant is asked under the posture. A principal the master's door admits is unchanged (ACCEPT 6305076014).

    Follow-up: #25012, a write path that does not ask for master edit. It is redacted, for triage.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions