Repository navigation
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
Description
Ruled: 6134050293 · letter A · 2026-10-12T22:48Z
Filing gate ①, class (a): a reproducible defect, measured at a public door. It is a regression against the released engine.
reach: HTTP on this tree, and a named producer. The producer is hotcrm's crm_quote_line_item → crm_quote → crm_account. hotcrm's pinned 17.7.0 engine admits all 15 writes that this tree refuses.
Filed by domain:cli seat 2 (#22648) · os-steve · session session_01VoSxBQujKLZKPwK2u5ehQ6. It comes from #15953 slice 4's dev (os-dev-report 6131548194, out-of-scope finding 1). That slice's new family, @objectstack/verify's parent-derived write reach (PR #23091), found it. ⛔ Not a claim. Lane and grade are triage's.
Reader who acts: triage grades it. The named seam is packages/plugins/plugin-security/src/security-plugin.ts#assertMasterRowEditable, which reads as domain:services. This seat does not land there.
Dedupe: semantic issue search scoped to this repository, all states.
controlled_by_parent chain two-level master detail write refused assertMasterRowEditable PERMISSION_DENIED master not editable row-level security: 17 hits, all closed. None is a two-level write refusal. The nearest are console: a permitted editor gets no Edit / Delete on controlled_by_parent records — the record header hides them although security/explain answers allowed and the PATCH returns 200 (HotCRM sales manager on contracts and quotes, 17.7.0) #22721 (the console hiding Edit oncontrolled_by_parentrecords, 17.7.0, single-level), 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, security(explain):POST /api/v1/security/explainanswers allowed for an update of a controlled_by_parent record whose own PATCH refuses — explain asks sharing's canEdit, which reads controlled_by_parent as org-shared #22514 and security(explain): explain'sreadverdict for a row the caller cannot read disagrees with the read door, and differs from its answer for an id no row carries (a controlled_by_parent detail under an unreadable master, measured) #22792 (explaindisagreeing with the door), andassertControlledByParentWriteanswers a metadata defect and a missing row with the same403 PERMISSION_DENIED"requires edit access to its master record" #7474 (the envelope's wording).nested master-detail grandchild write 403 regression quote line item: 4 hits, all closed, none this subject.- CONTROL: the first query reaches the
controlled_by_parentfamily (console: a permitted editor gets no Edit / Delete on controlled_by_parent records — the record header hides them although security/explain answers allowed and the PATCH returns 200 (HotCRM sales manager on contracts and quotes, 17.7.0) #22721, 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, spec(contracts): theControlledByParentWriteDenialLegTSDoc saysmaster_chainrefusals name a master above the record's own master; the walk's first hop reads the record's own master #22497), so the search covers this seam.
Measured at the door (this tree, 275ad4229a and 8b4a5802f0)
The fixture app is account (private) ← quote (controlled_by_parent) ← quote_line (controlled_by_parent). One signed-in principal holds:
account {read, edit, modifyAllRecords};quote {read, create, edit};quote_line {read, create, edit, delete}.
The answers:
PATCH /api/v1/data/pwr_quote/IDwith an empty body answers200.PATCH /api/v1/data/pwr_quote_line/IDanswers403 PERMISSION_DENIED:[Security] Access denied: update on 'pwr_quote_line' requires edit access to its master record (master 'pwr_quote' not editable by this user (row-level security)).- Delete and insert answer the same.
- The same holds for a principal who owns the whole chain, and with
modifyAllRecordson both masters.
Against the published engine (hotcrm, read-only clone at 1d7148bf)
- Run A, hotcrm's own pinned
@objectstack/verify17.7.0 over its own artifact: all 15 writes tocrm_quote_line_itembysales_managerandsales_repare admitted, consistently with their admittedcrm_quoteedits. - Runs B and C, this tree's engine over the same artifact: all of them are refused. That is 31 contradicted writes across the personas.
- On this tree, single-level details (
crm_contract,crm_quote,crm_opportunity_line_item,crm_contact) agree with ADR-0055 §3.
The contract
ADR-0055 §3: "editing/deleting/creating a detail requires edit access to its master". The 2026-09-07 amendment: the write gate "walks the chain hop by hop … until it reaches a master that governs its own rows", and "a detail whose whole chain is reachable stays readable and writable".
Here the middle master (quote) is itself controlled_by_parent, and the caller edits it (200). The hop still answers "not editable (row-level security)".
Where it seems to break (a lead, not a diagnosis)
The dev read assertMasterRowEditable: the hop's computeRlsFilter(master, 'update') plus ql.findOne under the caller's context answers no row for a controlled_by_parent middle master. It also notes that the @objectstack/plugin-security 17.3.0 chain walk (#11082, PR #11183) is what the amendment describes. When this regressed is NOT MEASURED.
Points for the dev
- Bisect first. Measure on the released 17.7.0 engine against this tree, using the fixture shape above. PR feat(verify): the parent-derived write reach family, runnable as os verify --parent-write-reach (#15953 slice 4) #23091's pinned test
finds today's engine refusing a two-level detail whose master the persona edits(packages/verify/src/parent-write-reach.test.ts) is a ready reproduction. - Fix the hop, so that a
controlled_by_parentmaster's editability is its own write gate's answer, which composes the chain. ⛔ Do not open the gate. The cycle guard and the depth bound keep denying. - Pins:
- a two-level detail is writable exactly when its middle master is editable by the caller;
- CONTROL: a single-level detail is unchanged;
- CONTROL: a caller who cannot edit the middle master is still refused.
- When this lands, PR feat(verify): the parent-derived write reach family, runnable as os verify --parent-write-reach (#15953 slice 4) #23091's pinned expectation flips from
contradictedtoproven. That PR's README andcli.mdxsay so.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 12, 2026
Triage: first grade,
bug·security·priority:p1·target:v18·domain:services·area:access, intopm:queue. A regression against 17.7.0: the chain walk is restored to ADR-0055 §3 as amended, and widened no furtherTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T20:51Z. ⛔ Not a claim, ⛔ not a dispatch.target:v18: a write gate refuses writes that the released 17.7.0 engine admits, on a real product. HotCRM's quote line items are refused for every persona that edits the quote: 31 contradicted writes. Shipping v18 with this breaks HotCRM's quote editing. It is fail-closed, so nothing is exposed; the cost is broken work.securitywithout escalation: it is an authorization gate. The fix restores the contract ADR-0055 §3 states (the 2026-09-07 amendment: the gate "walks the chain hop by hop … until it reaches a master that governs its own rows"). Restoring a ruled contract is not a loosening. ⛔ If the fix would admit any write the 17.7.0 engine refuses, it is a loosening, and it stops and escalates.assertMasterRowEditabledecides acontrolled_by_parentmiddle master's editability by walking on to the master that governs its own rows, as the amendment says. ⛔ Not by reading the middle row under its own row-level filter.modifyAllRecordsshort-cut that the contract does not state.packages/plugins/plugin-security(domain:services); rationale: the write gate's chain walk.objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 3 · 2026-10-12T21:20Z
Session:
session_01FzQy26KznSa5j2gnf3AzsEAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-23093-cbp-two-level-chain-walkWorktree:
objectstack-issue-23093Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/plugins/plugin-security/src/security-plugin.ts(only the write gate's chain walk:assertMasterRowEditableand the hop loop ofassertControlledByParentWrite) and that plugin's tests for this gate; one.changeset/23093-*.md; if PR #23091 has merged first, its pinned expectations inpackages/verify/src/parent-write-reach.test.tsandpackages/cli/test/verify-parent-write-reach.test.ts(expectation flips only, declared todomain:cliseat 2 in#22648) (stop on breach; explain in the report)Container & model:
M (security write gate; bisect first, then a fix bounded by the released 17.7.0 engine's answers),mode:subagent,model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"Clause-②: no — a runtime authorization behaviour restored to ADR-0055 §3 as amended; no published accept set or public surface moves.
Responsibility:
plugin-security's write gate (assertMasterRowEditable, the platform's own code) refuses a two-levelcontrolled_by_parentwrite that ADR-0055 §3 and the released 17.7.0 engine admit | none: every detail write passes this gate, and no other path admits it | hotcrm'scrm_quote_line_itemundercrm_quoteundercrm_account, for every persona that edits the quote (31 contradicted writes, measured at the door on the card), and any app with a two-levelcontrolled_by_parentchainThread-read: 6132405210
Serial constraints cleared: same file: PR #22900 (#15204 stage 6b-2, draft,
needs-user-decision) editssecurity-plugin.tsonly at its boot regions (hunks at about 4431 to 4751) and one doc comment nearCbpRelation(about 588), disjoint from this region (about 9300 to 9550); declared to the epic in#15194. PR #23091 (domain:cliseat 2, draft) pins today's refusal inpackages/verify; the flip order is declared in#22648. No other open PR touchessecurity-plugin.ts(the open PRs' file lists, read at this claim). Recent same-file landings:06cc45dc3(#22895),0253192f7(#22863),d112087b8(#22854); they are bisect candidates, not claims.Generated by Claude Code
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{ "issue": 23093, "status": "needs_decision", "branch": "claude/issue-23093-cbp-two-level-chain-walk", "pr": null, "session": "session_01FzQy26KznSa5j2gnf3AzsE — the dispatching PM's session (mode:subagent); identity = the branch named in claim 6132814355", "premise_still_valid": false, "summary": "PREMISE FALSE: the engine did not regress. On the card's fixture shape (quote_line controlled_by_parent under quote controlled_by_parent under account private), driven through @objectstack/verify bootStack + hooks.run as each signed-in persona, the released 17.7.0 engine (4e4e88142, the plugin-security@17.7.0 tag commit) and main (6662e3f59) give the SAME answer once the persona is the same: a persona holding position org_member is refused every line write (update/insert/delete) under a quote it did not create — 403 PERMISSION_DENIED, \"[Security] Access denied: update on 'rp_quote_line' requires edit access to its master record (master 'rp_quote' not editable by this user (row-level security))\" — while its direct update of that same quote answers ok; a persona without org_member is admitted on both trees. What differs between hotcrm's Run A and Runs B/C is the INSTRUMENT, not the gate: 17.7.0's bootStack pinned AuthPlugin autoDefaultOrganization: false (org-less personas, positions ['everyone'], tenant none, zero sys_organization rows); commit 34dba5ae1 (#22186, ADR-0131 C1) removed that pin, so harness sign-ups became Default Organization members (positions ['org_member','everyone']). 17.7.0's own serve/dev composition already binds every sign-up as role member (plugin-auth reconcile-membership.ts, membershipPolicy auto) — read from source, not booted. Mechanism (H1 confirmed, located one step further): the hop for a controlled_by_parent MIDDLE master runs computeRlsFilter(master, 'update') with the platform ownership floor owner_only_writes (created_by == current_user.id, positions ['org_member']) standing, because assertMasterRowEditable deliberately does not set masterGateCoversThisWrite; the by-id write of that same master drops it (step 2.7, the 2026-08-15 ruling recorded in commit 6feac910b). Discriminator measured: the same org-member persona's chain where IT created the quote is admitted for all 5 writes on both trees. The gate region (assertControlledByParentWrite + assertMasterRowEditable) is unchanged 17.7.0 -> main except the leg labels. HARD STOP: any fix that admits these writes admits writes the 17.7.0 engine refuses for an org member — triage 6132405210's escalation condition. The candidate (A below) was prototyped locally, built, measured and restored; nothing pushed, no PR. The branch is pushed empty at 6662e3f59 as the claim's landing marker.", "tests": "Scratch reproduction (outside the repo, never committed; copied untracked into packages/verify/src of each tree, removed after): boots the fixture with securityDefaultPermissionSets plus four persona sets, seeds chains as system (owner_id other / own), plus a 'door' chain whose quote the persona inserts itself; each persona has a twin with org_member toggled (17.7.0: insert sys_member role member; main: delete the persona's sys_member rows). Builds (all under os-verify-lock, slot issue-23093): main closure `pnpm turbo run build --filter=@objectstack/verify^... --concurrency=2` VERDICT command-exit 0 (46 tasks); 17.7.0 closure same command in a detached worktree at 4e4e88142, VERDICT command-exit 0 (32 tasks). Runs (`pnpm exec vitest run --maxWorkers=2 src/zz-repro-23093.test.ts`, each VERDICT command-exit 0, Tests 1 passed): 2x2 per persona rp_manager (account read/edit/modifyAll, quote read/create/edit, line CRUD) — 17.7.0 no-org_member: other+own+door lines 15/15 ok; 17.7.0 org_member: other+own lines 10/10 refused 'row-level security', door 5/5 ok; main org_member: other+own 10/10 refused 'row-level security', door 5/5 ok; main no-org_member: 15/15 ok. Quote and contract direct updates ok in every cell. Controls identical on both trees: rp_rootro (account read-only) refused everywhere; rp_midro (quote read-only) lines refused 'no edit permission on master rp_quote'; rp_member (owner-only) other chain 404 RECORD_NOT_FOUND on by-id writes and insert refused. Prototype A (one line in assertMasterRowEditable: computeRlsFilter(..., { dropPlatformOwnershipFloor, masterGateCoversThisWrite: !delegatedWrite && this.declaresControlledByParent(rel.master) })): plugin-security build VERDICT command-exit 0, marker grep -c in dist/index.js = 1; matrix run VERDICT command-exit 0: every line answer equals the same principal's direct quote-update answer — org-member rp_manager other+own 10/10 ok, rp_member own 5/5 ok (these 15 are what 17.7.0 refuses for an org member); rp_rootro 10/10 still refused (now at the account hop, 'no edit permission on master rp_account', the same text 17.7.0 gives its no-org_member persona); rp_midro still refused; rp_member other insert still refused 'row-level security'. Restore: `git checkout HEAD -- packages/plugins/plugin-security/src/security-plugin.ts`; git diff HEAD = 0 lines; hash-object 0986060dcc5f = HEAD blob 0986060dcc5f; rebuild VERDICT command-exit 0; `node scripts/ablation-dist-preflight.mjs @objectstack/plugin-security MARKER --absent` -> marker absent from all 6 built files, tree clean. No package test/typecheck run: the branch has no diff (`dispatch-gates --commands --repo objectstack-ai/objectstack` exit 2: 'this branch changes nothing against origin/main (merge base 6662e3f59)'). NOT MEASURED: an app-authored update RLS policy on the middle master under prototype A; a 17.7.0 serve boot (the member auto-bind there is read from source).", "mcp_calls": "0", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/23093/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Not REST: one git push of the empty branch.", "open_questions": [ { "question": "Should the chain walk stop applying the platform ownership floor (owner_only_writes) on a controlled_by_parent MIDDLE master — i.e. extend the 2026-08-15 ruling recorded in commit 6feac910b ('the master gate is the sole row-write authority for a controlled_by_parent detail') from that object's own by-id write to the hop that decides edit access for its details? Measured today, on both 17.7.0 and main, one org-member principal gets 200 on PATCH quote and 403 on every line under it unless it created the quote; ADR-0055's amendment says a detail whose whole chain is reachable stays writable, while assertMasterRowEditable's docblock records the opposite choice ('A master that is itself a detail keeps its floor exactly as it does today'). Any answer that admits these writes admits writes the released 17.7.0 engine refuses for an org member, which triage 6132405210 sends to escalation.", "options": [ "A — Extend the ruling to the hop. One line in assertMasterRowEditable: pass masterGateCoversThisWrite = !delegatedWrite && declaresControlledByParent(rel.master), so the RLS leg on a middle master is the same composition its own by-id write uses (only the PLATFORM floor drops; authored update RLS and the derive-from-select floor still run; the walk continues to its master as today; delegated writes keep both floors, mirroring step 2.7's !delegatorSets). Measured on the fixture: each line answer equals the same principal's direct quote-update answer; newly admitted = 15 writes for org members who can already update that quote; root-not-editable, middle-not-editable and unreadable-root refusals measured unchanged; the cycle and depth guards sit in the loop, outside the edited leg (code path untouched, not in the measured matrix). Widening: an org_member who can PATCH the middle master by id gains writes on its details, nobody else. Cost: S, one PR — the line, the docblock bullet rewritten, pins in controlled-by-parent-chain.test.ts (its fixture needs the floor and an org_member position), negative-pin ablations, patch changeset; PR #23091's pinned expectation flips (domain:cli). No ADR amendment needed: ADR-0055 already states the target.", "B — Keep the floor (status quo, equal to the 17.7.0 engine). Record it instead: an ADR-0055 line that a middle master's editability in the walk includes the platform floor (creator-only for rank-and-file members), and PR #23091 reports that cell held rather than contradicted. Cost: an ADR amendment (Tier H) plus the verify family change; no engine change. Consequence: hotcrm's quote lines stay editable only by the quote's creator and by principals outside the floor's domain (org owner/admin, non-members); no authorable lever widens it — modifyAllRecords on both masters included (card-measured: still refused) — the same undeclared creator-only rule that ruling removed for direct writes.", "C — Drop the floor in the hop only for principals holding modifyAllRecords (or another widener) on the middle master. Excluded verbatim by triage ('no modifyAllRecords short-cut that the contract does not state'); listed only to close it." ], "recommendation": "A, but only with the maintainer's explicit approval of the permission-boundary widening, as the 2026-08-15 ruling's direction 1 approved the single-level analogue. The four axes: (1) Real business need, measured: hotcrm's crm_quote_line_item -> crm_quote -> crm_account (31 contradicted writes on the card; here 15/15 line writes refused for an org-member manager on two chains while the quote update is 200). In-repo examples at 6662e3f59 hold 3 controlled_by_parent objects, all single-level, so the pull is one named external product. A serves it; B leaves it with no authorable fix; C serves only principals holding modifyAllRecords on the middle master (hotcrm's personas' grants not measured here). (2) Long-term soundness: A gives one composition for one (principal, master row, update), the 'one composition, both paths' direction already ruled for leg 1 (commit 498f4e884) and leg 2 (#8679). B keeps two answers for that triple. C adds a second widening path the contract never states. (3) Keeping AI authors from getting it wrong: under A a declared controlled_by_parent chain means the same thing at every level, so the declaration is the enforcement. Under B a natural quote/line chain an AI writes is silently creator-only, with no publish-time error and no lever. C hides the rule behind a bit. (4) Start-up scope discipline: A adds no surface, key or gate. B adds an ADR amendment and a verify-family exception. C adds a conditional path. Nobody proposes a new gate." } ], "out_of_scope_findings": [ "carrier: the PM / triage seat · noted, not filed — the card's title and its p1/target:v18 rationale ('a regression against 17.7.0', 'Shipping v18 with this breaks HotCRM's quote editing') rest on hotcrm Run A, whose 17.7.0 bootStack personas were org-less; measured here, the 17.7.0 engine refuses the same writes for an org member, so the break predates v18 for any deployment whose sign-ups are members. Re-grading is the triage seat's call.", "carrier: PR #23091 (domain:cli seat 2) · noted, not filed — its pinned test 'finds today's engine refusing a two-level detail whose master the persona edits' is a correct reading of today's engine; only its framing as a 17.7.0 regression is contradicted by this run. Under option A its expectation flips (declared in #22648); under B it becomes held.", "carrier: hotcrm (pins @objectstack/verify 17.7.0) · noted, not filed — 17.7.0's bootStack pins autoDefaultOrganization: false, so hotcrm's verify runs measure org-less personas (positions ['everyone']) that no serve deployment has; every floor-scoped answer there reads wider than production. Its upgrade past 34dba5ae1 is the carrier." ], "gates": "none owed — empty change set (dispatch-gates --commands --repo objectstack-ai/objectstack, tree 6662e3f59: exit 2, 'this branch changes nothing against origin/main'); the PM's 66-command reading was not run because no file changed.", "line_budget": "n/a — no skills/** or ruled-ledger file touched", "deviations": [ "No draft PR and no changeset: the dispatch's hard stop applied (the only fix shape found admits writes the 17.7.0 engine refuses), so the order's PR/changeset/label steps were not reached; label-write not called.", "Bisect done as endpoint measurement plus source identification rather than a per-commit build bisect: the engine answer is identical at 17.7.0 and main once the persona shape matches (2x2 above), so there is no engine commit to bisect to; the instrument change is identified by source diff as 34dba5ae1 (harness pin removed), not by booting 34dba5ae1 and its parent.", "A prototype edit to security-plugin.ts was built and measured locally to give the decision exact data, then restored (hash and dist-absence proofs in tests); never committed or pushed.", "Fetching PR #23091's branch into refs/issue-23093/pr23091 also updated the shared remote-tracking ref origin/claude/issue-15953-s4-parent-write-reach (read-only side effect); the owned ref was deleted afterwards." ], "files_changed": [] }Generated by Claude Code
11 remaining items
objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 4 · 2026-10-12T23:17Z
Session:
session_01FzQy26KznSa5j2gnf3AzsEAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-23093-cbp-two-level-chain-walk(pushed empty at6662e3f59by the previous dispatch, an ancestor ofmain)Worktree:
objectstack-issue-23093Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)Ruling-ref: 6134050293 (director binding of the maintainer's 「同意」, letter A) · 6133922339 (the direct-channel record, 「A 延伸裁决 (推荐)」)
File surface:
packages/plugins/plugin-security/src/security-plugin.ts(onlyassertMasterRowEditable's write-RLS leg and its docblock bullet) and that plugin's tests for this gate (controlled-by-parent-chain.test.tsand neighbours); one.changeset/23093-*.md; if PR #23091 has merged first, its pinned expectations inpackages/verify/src/parent-write-reach.test.tsandpackages/cli/test/verify-parent-write-reach.test.ts(expectation flips only, declared in6132790096) (stop on breach; explain in the report)Container & model:
M (ruled one-line composition change plus four pins, every refusal pin ablated),mode:subagent,model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"Clause-②: no — a runtime authorization behaviour under the maintainer's ruling A; no published accept set or public surface moves.
Responsibility:
plugin-security's write gate (assertMasterRowEditable, the platform's own code) keeps the platform ownership floor at acontrolled_by_parentmiddle master that the same principal's direct write has dropped since the 2026-08-15 ruling | none: every detail write passes this gate | hotcrm'scrm_quote_line_itemundercrm_quotefor organization members who can update the quote (31 refused writes on the card; 15/15 in the dev's fixture), on 17.7.0 andmainalikeThread-read: 6134050293
Serial constraints cleared: same file: PR #22900 (#15204 stage 6b-2, draft) edits only the boot regions and one doc comment near
CbpRelation, disjoint (declared in#15194). No commit touchedsecurity-plugin.tsbetween6662e3f59andmain8464380248. PR #23091 (domain:cli, draft, open) pins today's answer; the flip order is declared in6132790096. No other open PR touches the file (open PRs' file lists, read at this claim).Generated by Claude Code
fix(plugin-security): a controlled_by_parent middle master is judged …
test(verify): the two-level parent write reach is proven once the cha…
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 23093, "status": "done", "branch": "claude/issue-23093-cbp-two-level-chain-walk", "pr": "https://github.com/objectstack-ai/objectstack/pull/23114", "session": "session_01FzQy26KznSa5j2gnf3AzsE — the dispatching PM's session (mode:subagent); identity = the branch named in claim 6134458430", "premise_still_valid": true, "summary": "Ruling A implemented: assertMasterRowEditable's write-RLS leg now passes masterGateCoversThisWrite: !delegatedWrite && this.declaresControlledByParent(rel.master) to computeRlsFilter, so a controlled_by_parent middle master on a non-delegated write is judged by its own direct-write composition (only the platform floor yields; app update RLS and the derive-from-select floor still run; a delegated write keeps both floors). The docblock bullet ('A master that is itself a detail keeps its floor ...') is rewritten to state the ruling and why the vouch holds structurally (same predicate that continues the walk). H1 confirmed: the prior prototype line is the whole engine change. H2 confirmed: an app-authored update policy on the middle master still refuses the line write; measured consequence: before the fix the CREATOR of a middle master whose app policy refuses its direct update could still write its lines (floor OR policy), now refused, matching the direct update (the fork invariant's other direction; no recipe beyond the changeset's behaviour notice). H3 confirmed by pin 4 and ablation A4. No fork: zero cases at either layer where a line write's answer differs from the same principal's direct quote update. PR #23091 had MERGED before the PR opened, so origin/main (21c86444e) was merged in and its verify expectation flipped (pwr_quote_line proven); its cli test needs no flip (single-level fixture).", "tests": "HEAD 343e21e33 (final, after merging origin/main 21c86444e): `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2` -> 'Test Files 191 passed (191) / Tests 3966 passed | 45 skipped (4011)', VERDICT command-exit 0; `pnpm --filter @objectstack/plugin-security typecheck` VERDICT command-exit 0 ('check:test-typecheck: OK'); `pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2 src/parent-write-reach.test.ts` -> 'Tests 10 passed (10)', VERDICT command-exit 0 (pwr_quote_line proven, summary held 135 / contradictions 0 / hardFailures 0); `pnpm --filter @objectstack/verify typecheck` OK; closure `pnpm turbo run build --concurrency=2 --filter='@objectstack/verify^...' --filter=@objectstack/verify` 47/47, VERDICT command-exit 0; narrowed eslint `--no-inline-config --format json` on the 3 touched TS files: 3 files, 0 errors, 0 warnings, none ignored; eslint.config.mjs never enables type-aware linting (no parserOptions.project), so untouched files' verdicts cannot move; full `pnpm lint` is CI's. At c6a48bd1d (pre-merge): suite 193 files / 3981 passed / 45 skipped, VERDICT 0; the new test file is in tsconfig.test.json's program (--listFiles count 1). PRE-FIX BASELINE on main 846438024, new pin file: 'Tests 5 failed | 4 passed (9)' (pin 1 line update refused '(master cbc_quote not editable by this user (row-level security))'; pin-2 refusals at the quote hop, not the root; pin 3 creator case ADMITTED; pin 4 non-delegated control refused). POST-FIX: 'Tests 9 passed (9)'. ABLATIONS, each from committed c6a48bd1d via `node scripts/ablation-replace.mjs` in WRAP mode under os-verify-lock, inside a driver with `trap restore EXIT INT TERM` (git checkout HEAD -- absolute path, blob compared to HEAD blob, empty = failure); the test imports ./security-plugin.js from src, so no dist leg: A0 old leg (masterGateCoversThisWrite: false): anchor 1->0, blob 12a9565cd3b9->cb1cf13b368e, 'Tests 5 failed | 4 passed', pin 1 'line update: expected { kind: refused ... } to deeply equal { kind: admitted }'; A2 walk stops after the first hop: 2 failed (both pin-2 cases; root-unreadable insert passed the gate and met VALIDATION_FAILED downstream); A3a middle object-permission leg skipped for cbp: 1 failed (pin 3 no-grant: admitted); A3b middle write-RLS leg skipped for cbp (bare widening): 4 failed (pin 3 app-policy + creator, both pin 4): admitted; A4 '!delegatedWrite &&' removed: 2 failed (both pin-4 cases admitted while the delegated quote update is refused). Every leg: 'ok restored: blob == HEAD (12a9565cd3b9) and git diff HEAD is empty'; driver re-check: hash-object == HEAD blob 12a9565cd3b9d8e369cc1021f42e7de17077c631, git diff HEAD 0 lines; green re-run from the committed state: 5 cbp files, 71 passed, VERDICT 0. FULL STACK (scratch zz-repro-23093.test.ts, the previous dev's matrix plus an rp_locked H2 persona, copied untracked into packages/verify/src and removed after; plugin-security dist rebuilt from c6a48bd1d, marker grep -c 1 in dist/index.js and index.mjs): 5 personas x {org_member, toggled} x chains other/own/door x 3 line verbs, invariant violations [] ; manager 15/15 admitted both variants; rp_locked 30/30 line writes refused naming rp_quote (row-level security), door chain included; root-RO refused at the root; middle-RO refused naming rp_quote. Whole-repo sweep: 11 files carry the refusal text; none pins the two-level refusal under the floor (org_member fixtures are single-level); the full suite stayed green.", "mcp_calls": "0", "api_writes": "3 REST writes (each through the fleet-write relay as objectstack-fleet[bot], 2 dispatches): (1) pr_create, run 38325882287 = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23114/assignees (zhuangjianguo), read back 10461 bytes identical; (2) POST /repos/objectstack-ai/objectstack/issues/23093/comments (this os-dev-report, scripts/pm/post-stamped.mjs). Not REST: 3 git pushes of claude/issue-23093-cbp-two-level-chain-walk (empty probe 6662e3f59..846438024, c6a48bd1d, 343e21e33). label-write: 0 calls (the dispatch named no label; a changeset is present so skip-changeset does not apply); the labels documentation / size/l / tests / tooling were attached by the labeler, not by this run.", "open_questions": [ { "question": "Two published doc sentences become false when this merges and are outside the claim's file surface, so they were not edited: packages/verify/README.md (about line 136) and content/docs/deployment/cli.mdx (about line 2014), both saying 'Today's engine refuses every write to a detail two levels below its root master ... so an app that declares such a chain fails the run.' Who rewrites them?", "options": [ "A: a patch round on PR #23114: delete or rewrite the sentence in both files (the README ships in @objectstack/verify's tarball; plain prose, no changeset needed beyond the existing one)", "B: route to domain:cli seat 2 (#22648), the author of both sentences, as a follow-up PR" ], "recommendation": "A, because the sentence turns false at this PR's merge and the README is published with the same release; it is two prose edits and no gate." } ], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted, not filed — the RlsFilterOptions.masterGateCoversThisWrite docblock (security-plugin.ts, about line 442) lists the call sites that vouch for coverage; the chain hop is now one of them and is documented at its call site only. Recorded in PR #23114's Acceptance notes." ], "gates": "66 derived / 66 run / 0 NOT-MEASURED at 343e21e33, every recorded exit 0: 'dispatch-gates --ran: 66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero — all 66 recorded an exit code and none of them is 3)'. The derivation equals the PM's 66-command reading (gates2.txt) line for line. At c6a48bd1d check:i18n first answered PREREQUISITE NOT MET (exit 3, CLI closure unbuilt), was re-run after the CLI closure build (59/59 turbo cache) and exited 0; at 343e21e33 it exited 0 directly.", "line_budget": "n/a — no skills/** or ruled-ledger file touched", "deviations": [ "The pins landed in a new sibling file, controlled-by-parent-chain-middle-master.test.ts, not in controlled-by-parent-chain.test.ts (the suggested route). That suite's rig is a read-only engine double whose principals hold no position, so the floor never binds and no write can show a row changed. The new file uses a real ObjectQL + SQL driver, the real sharing middleware and member_default with org_member.", "origin/main 21c86444e was merged into the branch (merge commit d6b81d9fa) because PR #23091 merged during the run and its test file exists only on main. The merge was clean, with no regen deferral recorded. The closure was rebuilt and the full suite re-run on the merged head.", "The PR body (its one write) cites local verification at c6a48bd1d, before the merge. The final-head numbers are in this report. If the seat wants the body to match, replace the parenthetical of the 'Local verification' heading with: 'at 343e21e33: plugin-security 191 files / 3966 passed / 45 skipped; verify parent-write-reach 10/10; 66/66 gates exit 0'.", "The commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude). The harness reminder asked for a model-named Co-Authored-By; AGENTS.md takes precedence.", "PR #23091's cli test (packages/cli/test/verify-parent-write-reach.test.ts) was not edited: its fixture is single-level (vpw_contract under vpw_account), so it has no two-level expectation to flip." ], "files_changed": [ ".changeset/23093-cbp-middle-master-floor.md", "packages/plugins/plugin-security/src/security-plugin.ts", "packages/plugins/plugin-security/src/controlled-by-parent-chain-middle-master.test.ts", "packages/verify/src/parent-write-reach.test.ts" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Claim amendment ·
domain:servicesseat 2 ·session_01FzQy26KznSa5j2gnf3AzsE· 2026-10-13T00:40Z, under claim6134458430. ⛔ Not a new claim. The file surface grows by the texts PR #23114 makes false, for a patch round on the same branch. These are prose and comment edits only:.changeset/15953-verify-parent-write-reach-family.md(feat(verify): the parent-derived write reach family, runnable as os verify --parent-write-reach (#15953 slice 4) #23091's pending release note, line 19: "Today's engine refuses every write to a detail two levels below its root master, …"). This is a foreign changeset:check-empty-changesetreads the edit as a DELIBERATE CORRECTION, red by design, and it owes a same-head contract-review-tier confirmation before the queue.packages/verify/README.md(about line 137) andcontent/docs/deployment/cli.mdx(about line 2014): the same sentence, published in@objectstack/verify's tarball and on the docs site.packages/plugins/plugin-security/src/security-plugin.ts, themasterGateCoversThisWriteoption docblock (about line 442): its list of the call sites that vouch now gains the chain hop.Declared to
domain:cliseat 2 in#22648, the author of the first three.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 23093, "status": "done", "branch": "claude/issue-23093-cbp-two-level-chain-walk", "pr": "https://github.com/objectstack-ai/objectstack/pull/23114", "session": "session_01FzQy26KznSa5j2gnf3AzsE — the dispatching PM's session (mode:subagent); identity = the branch named in claim 6134458430 (file surface amended by 6135431019)", "premise_still_valid": true, "summary": "Patch round on PR #23114 (answer A to my open question). Four prose and comment edits at 15058d6ff, nothing else: (1) .changeset/15953-verify-parent-write-reach-family.md line 19, (2) packages/verify/README.md (about line 136), (3) content/docs/deployment/cli.mdx (about line 2014). The 'Today's engine refuses every write to a detail two levels below its root master ...' sentence in each now states the ruled behaviour: the detail write is admitted exactly when the caller holds the verb on the detail and may update the middle master directly, which also needs edit access to the root, and refused otherwise, so the family reports such a chain proven. No issue numbers were added to the README or docs prose. (4) security-plugin.ts masterGateCoversThisWrite option docblock: a bullet naming the chain hop in assertMasterRowEditable as a vouching call site, pointing at that call site's own bullet instead of restating the reason. check-empty-changeset is red on (1) by design (DELIBERATE CORRECTION). The old sentence was not restored, and no @objectstack/verify changeset was added. The branch was not re-merged with main: main moved 2 commits (a3d47c2c5), neither touching these files.", "tests": "HEAD 15058d6ff. `pnpm --filter @objectstack/plugin-security typecheck && pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/controlled-by-parent-chain-middle-master.test.ts src/controlled-by-parent-chain.test.ts && pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2 src/parent-write-reach.test.ts` under os-verify-lock: 'check:test-typecheck: OK', 'Test Files 2 passed (2) / Tests 29 passed (29)', 'Test Files 1 passed (1) / Tests 10 passed (10)', VERDICT command-exit 0. Builds: closure `turbo run build` over cli / verify / lint / spec / the i18n set (59/59, 41 cached, VERDICT 0), then a workspace build excluding docs (73/73, 66 cached, VERDICT 0). The fresh worktree lacked dist for client-react and 8 other packages, so check:skill-examples and check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3); the whole gate list was re-run after the build. Narrowed eslint on security-plugin.ts (--no-inline-config --format json): 1 file, 0 errors, 0 warnings. The full plugin-security suite was not re-run this round: the only code-file change is a comment in a docblock. Its last full run was at 343e21e33, the code head: 191 files, 3966 passed, 45 skipped, VERDICT 0.", "mcp_calls": "0", "api_writes": "1 REST write this round: POST /repos/objectstack-ai/objectstack/issues/23093/comments (this os-dev-report, via scripts/pm/post-stamped.mjs through the fleet-write relay, which also sends POST /repos/objectstack-ai/objectstack/dispatches). Not REST: 1 git push (343e21e33..15058d6ff). No PR body PATCH, no label write.", "open_questions": [], "out_of_scope_findings": [], "gates": "96 derived / 96 run / 0 NOT-MEASURED at 15058d6ff: 'dispatch-gates --ran: 96 derived famil(ies) accounted for — 96 run, 0 NOT-MEASURED (a DERIVED zero — all 96 recorded an exit code and none of them is 3)'. Every exit is 0 except `node scripts/check-empty-changeset.mjs --base origin/main` :: exit 1, which is red by design. Its verdict lines: 'This PR changes a changeset it did not add:' / '.changeset/15953-verify-parent-write-reach-family.md' / 'present on the merge base and CHANGED by this PR -- this is somebody else's release note'; the gate names this class DELIBERATE CORRECTION ('Remedy: do NOT restore it -- say so on the PR and get it confirmed'). The seat owes the same-head contract-review-tier confirmation. check-changeset-no-major and check-adr-0087-registration exit 0, and no @objectstack/verify changeset was asked for. The 30 docs families newly pulled in (check:docs, doc-frontmatter, doc-anchors, docs-redirects, published-readme-links, cli-examples-parity and the rest) exit 0.", "line_budget": "n/a — no skills/** or ruled-ledger file touched", "deviations": [ "PR body not PATCHed. Text for the seat to append (Markdown, no angle-bracket fragments):\n\n### Patch round (15058d6ff)\n\nFour texts that this change made false or incomplete are corrected. These are prose and comment edits only, per the claim amendment 6135431019:\n\n- `.changeset/15953-verify-parent-write-reach-family.md`, line 19: the pending parent-write-reach release note. Its sentence \"Today's engine refuses every write to a detail two levels below its root master ...\" now states the ruled behaviour: the write is admitted exactly when the caller holds the verb on the detail and may update the middle master directly, which also needs edit access to the root. It is refused otherwise, so the family reports such a chain `proven`. `check-empty-changeset` is red on this file BY DESIGN, a DELIBERATE CORRECTION of a pending release note this PR made false: \"This PR changes a changeset it did not add: .changeset/15953-verify-parent-write-reach-family.md present on the merge base and CHANGED by this PR -- this is somebody else's release note\". It is not restored, and no changeset is added for `@objectstack/verify`.\n- `packages/verify/README.md` and `content/docs/deployment/cli.mdx`: the same sentence, rewritten the same way.\n- `security-plugin.ts`, the `masterGateCoversThisWrite` option docblock: its list of the call sites that vouch for coverage now includes the chain hop in `assertMasterRowEditable`, pointing at that call site's bullet instead of restating the reason.\n\nLocal verification at 15058d6ff:\n\n- `dispatch-gates --ran`: 96 derived, 96 run, 0 NOT-MEASURED. Every exit is 0 except `check-empty-changeset`, which exits 1 as above.\n- `pnpm --filter @objectstack/plugin-security typecheck`: OK.\n- The pin file and `controlled-by-parent-chain.test.ts`: 29 passed.\n- `packages/verify` `parent-write-reach.test.ts`: 10 passed.\n\nAt 343e21e33, the code head, which this round changes only in comments:\n\n- `plugin-security`: 191 files, 3966 tests passed, 45 skipped.\n- 66 of 66 gates exit 0.", "The worktree was recreated from the local branch at 343e21e33 to make these edits. After the push it was removed again (node_modules deleted, `git worktree remove` without --force)." ], "files_changed": [ ".changeset/15953-verify-parent-write-reach-family.md", "packages/verify/README.md", "content/docs/deployment/cli.mdx", "packages/plugins/plugin-security/src/security-plugin.ts" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Seat ACCEPT: PR #23114 at
15058d6ff· ruling A · lands with one designed reddomain:servicesseat 2 ·session_01FzQy26KznSa5j2gnf3AzsE· 2026-10-13T01:38Z, under claim6134458430(amended6135431019) andRuling-ref6134050293/6133922339. The dev's reports are6135392664(code round) and6136047254(patch round). Checked on GitHub and onorigin/main, not from the reports.Checklist
main, assigneezhuangjianguo. The first line isFixes #23093, thenClause-②: no. The only closing keyword is on 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.check-governed-merges --pr 23114), and under the 3000-line threshold. The files:security-plugin.ts);controlled-by-parent-chain-middle-master.test.ts);6132790096);packages/verify/README.md,content/docs/deployment/cli.mdx.security-plugin.ts(epic territory) are untouched.assertMasterRowEditable's write-RLS leg passesmasterGateCoversThisWrite: !delegatedWrite && this.declaresControlledByParent(rel.master). That yields the platform ownership floor only, at acontrolled_by_parentmiddle master on a non-delegated write. An app's own update RLS and the derive-from-select floor still run, and a delegated write keeps both floors. The hop's docblock bullet is rewritten to state the ruling, and the option docblock names the new vouching call site.ObjectQLand a SQL driver):code: PERMISSION_DENIED,status: 403and the message naming the caller's own object, and that no row moved.main846438024: 5 failed of 9. After it: 9 passed.!delegatedWriteremoved) each went red, with the restore proved by blob hash..changeset/23093-cbp-middle-master-floor.md(@objectstack/plugin-security: patch): its "Admitted now", "Still refused", "Also refused now" and "Unchanged" bullets match the diff and the pins..changeset/15953-verify-parent-write-reach-family.mdline 19, inpackages/verify/README.mdand incontent/docs/deployment/cli.mdxnow states the ruled behaviour: admitted exactly when the caller holds the verb on the detail and may update the middle master directly, which needs the root editable; refused otherwise; the chain is reportedproven.6136179351on PR fix(plugin-security): a controlled_by_parent middle master is judged at the chain hop by its own direct-write composition (#23093) #23114 is PASS on this head, and doubles as the DELIBERATE CORRECTION confirmation (it names the note and judges each rewritten sentence).15058d6ff(latest run per name): 33 success, 7 expected skips, 1 failure,Check Changeset. That failure is red by design, under the three conditions recorded on the PR in6136189909.mcp_calls0 in both rounds; the relay writes werepr_createand two report comments. The seat appended the patch-round text to the PR body.Out-of-scope findings
masterGateCoversThisWriteoption docblock's call-site list was missing the new hop. It is fixed in this PR's patch round, asAcceptance notes.Landing:
pr_ready+automerge_enablethrough the relay. This seat follows it to MERGED. When it merges, #23093 closescompletedand its board labels are cleared.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed · PR #23114 merged through the merge queue at 2026-10-13T02:04Z as
dfb3ba2b23, and that commit is onorigin/main(read 2026-10-13T02:05Z).domain:servicesseat 2 ·session_01FzQy26KznSa5j2gnf3AzsE, under claim6134458430, ruling A (6133922339/6134050293) and ACCEPT6136199706.main:assertMasterRowEditablepassesmasterGateCoversThisWrite: !delegatedWrite && this.declaresControlledByParent(rel.master)(1 hit). The old "Today's engine refuses every write to a detail …" sentence has 0 hits across the README,cli.mdxand.changeset. The control, the replacement "answered the same way" sentence, has 1 hit in each of the three files.ready_for_review, thenauto_merge_enabled, thenadded_to_merge_queueat 2026-10-13T01:41Z, with the one designed red recorded in6136189909. The PR's only closing keyword wasFixes #23093, and no other card closed in the window.pwr_quote_lineisproven), in the order declared at6132790096. Nothing is left fordomain:cli.completedby that keyword. This write clearspm:dispatchedand the assignee.domain:services,area:access,security,priority:p1and the type label stay.maindoes not publish.@objectstack/plugin-securityships.changeset/23093-cbp-middle-master-floor.mdat the next version release, together with the corrected 15953 note. hotcrm picks the fix up when it upgrades past its 17.7.0 pin.Generated by Claude Code