Repository navigation
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
Description
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 thecontrolled_by_parentdetail 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
- Source reading only. Reach was NOT measured.
- After PR fix(plugin-security): a controlled_by_parent detail's read scope requires object-level read on its master #24985 (plugin-security: a controlled_by_parent detail is readable by a caller with no read on its master object, where ADR-0055 refuses it (detail withheld under the disclosure ruling) #24962), a by-id update or delete of an unreadable detail answers the missing-record shape before this leg is asked.
- The one insert shape measured met the reference check first.
- Whether any write path reaches this leg with the difference live is the first thing to measure.
Dedupe
-
controlled_by_parent detail write gate master edit check differs from master write door→ 28 hits. The nearest:- plugin-security: a controlled_by_parent detail is readable by a caller with no read on its master object, where ADR-0055 refuses it (detail withheld under the disclosure ruling) #24962 (open, the read half);
- finding(security): every write to a controlled_by_parent detail TWO levels below its root master answers 403 "master … not editable" for a principal who edits that master — a regression against 17.7.0, measured on hotcrm's crm_quote_line_item #23093, security(explain):
explaindelete on a controlled_by_parent record reports the ownership floor, not the master check the data door runs — the master's editor is refused beside a DELETE 200 #22530 and finding(lint): the runtime gate's object-write baseline drops the written object's stored self, so a master save is refused (422) for a stored detail's violation — the gate's "never charged to someone else's write" contract #22118 (closed, other write-gate defects); - finding(security): controlled_by_parent derives its master through a REQUIRED LOOKUP (runtime and lint, third tier) where ADR-0055 §1 requires exactly one required master_detail and fails closed — a widening no record states #23100 (closed, the master derivation).
None covers this leg's question. New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 20, 2026
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:54Zsecurity-plugin.ts. The dev reads their state first.5323310017) applies to this card, its PR and every comment. Reach is recorded only as an abstract statement. Detail is withheld pending the maintainer.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 20 · 2026-10-20T22:30Z
Session:
session_011TdZ8GjixNZFLz1xJD9zzKAccount:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24986-cbp-master-edit-legWorktree:
objectstack-issue-24986Domain:
domain:servicesSeat:
domain:services#1File surface: measure first. Only on shown reach:
packages/plugins/plugin-security/src/security-plugin.ts,assertMasterRowEditable's object-level leg (~:9214atf32d795ce5) and nothing else in that file, NEW tests underplugin-security/src/, and one new.changeset/24986-*.md. ⛔ Notpackages/objectql/**, ⛔ notpackages/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 thenThread-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/main7adfaec2a1.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
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
objectstack-fleet commentedon Oct 21, 2026
ACCEPT → PR #25008 comment
6305076014·domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-21T00:39ZGenerated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Landed ·
domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-21T01:09Z · ⛔ The #9387 disclosure rule applies.added_to_merge_queue2026-10-21T00:42Z →merged2026-10-21T01:08Zorigin/mainby contentaaee0840be(one parent,62bc129308), 3 files.assertMasterRowEditable's object-level leg reads the master's posture fromgetObjectSecurityMetaand asks the door's three questionsState change in this stroke:
pm:dispatchedoff and the assignee cleared. The card closedcompletedon the merge.What changes: a
controlled_by_parentdetail write asks its master the object-levelupdatequestion the master's own write door asks (ADR-0055 §3). The posture must resolve, the requiredupdatecapabilities must be held, and the grant is asked under the posture. A principal the master's door admits is unchanged (ACCEPT6305076014).Follow-up: #25012, a write path that does not ask for master edit. It is redacted, for triage.
Generated by Claude Code