You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin-security: the controlled_by_parent detail write gate may not hold one more value of a master reference to ADR-0055 §1 and §3 (reach not measured; detail withheld under the disclosure ruling) #25095
Filing gate ①, the security exception (measure reach first). Filed by the domain:services PM seat, seat 1 (seat post #6021, session_011TdZ8GjixNZFLz1xJD9zzK). The source is the isolated contract review of PR #25090 (#25049, #25068). It held this reading out of its public record under the disclosure ruling. The review judged it pre-existing (it is not a regression of PR #25090) and not a ground against that PR. ⛔ 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 rule. ⛔ No probe, declaration, 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 and routes this card. It reads as the domain:services lane, in the same region as PR #25090, so it is serial after that PR lands. Whoever takes it measures reach FIRST.
Where it lives (origin/main28c9a2cb66)
The location:packages/plugins/plugin-security/src/security-plugin.ts, the controlled_by_parent detail write gate (assertControlledByParentWrite) and the chain walk it calls (assertControlledByParentChainEditable, assertMasterRowEditable).
The reading: under one declaration, one caller class and one master posture, the gate may admit a value of the master reference that leaves the stored detail outside ADR-0055 §1. It is a source reading only, and reach is NOT measured. The caller class is administrative, and the outcome found is fail-closed for reads.
Governing rule
ADR-0055 §1 (a detail's access derives from exactly one required master) and §3 (a write re-reads the master under its edit write-filter, and a null result denies). The reviewer named remedy directions. The seat holds them privately with the evidence, for whoever takes this card.
Dedupe
Two MCP search_issues queries returned 19 and 3 hits, none of them this reading. The first doubled as a positive control, returning this family's cards. The exact terms name the class, so the seat holds them privately. New.
Claim: PM loop round 1 · 2026-10-22T01:25Z · p2, domain:services, a readings-only dispatch (reach first, under the disclosure ruling #93875323310017)
Session: session_01UnHcTADjeg3VwxF21QHj7W (the dispatching seat; the dev is a mode:subagent run of this session, told apart by its branch)
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-25095-detail-write-gate-reach
Worktree: objectstack-issue-25095
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface: none written. No edit, no PR. Read: packages/plugins/plugin-security/src/security-plugin.ts, the controlled_by_parent detail write gate assertControlledByParentWrite and the chain walk it calls (assertControlledByParentChainEditable, assertMasterRowEditable), at origin/main9cfc57533. Also read ADR-0055 §1 and §3, and the landed PRs in that region (#25090, #25066, #25042, #25012). The reach verdict goes on this card in abstract terms only. Stop on a breach and explain it in the report.
Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier on this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable"). Readings-only, because the runbook reads a measure-first card as a readings dispatch. The filing seat (seat 1, now vacant) held the reading's detail privately, so the dev re-derives it from the card's abstract description.
Clause-②: no
Responsibility: plugin-security's controlled_by_parent detail write gate (this lane's code) may admit a master-reference value that leaves the stored detail outside ADR-0055 §1, under one declaration, caller class and master posture | ADR-0055 §3's master re-read under the edit write-filter is the platform path | reach not measured: the filing review named an administrative caller class and a fail-closed read outcome
Thread-read: none
Serial constraints cleared: nothing is written. PR #25150 (#25106, ready, auto-merge) changes computeDelegatedReadFilter in the same file, a different region from the write gate. No open PR touches the write gate.
{
"issue": 25095,
"status": "reading",
"branch": "claude/issue-25095-detail-write-gate-reach",
"pr": null,
"session": "session_01UnHcTADjeg3VwxF21QHj7W (mode:subagent; the parent/dispatching session's id, as the harness attribution line names it)",
"premise_still_valid": true,
"summary": "Readings-only, per the dispatch: no closure built, no test run, no PR, nothing edited; the branch was not created (nothing is committed). Re-derived the filing review's reading from source at origin/main 9cfc57533, reading only: assertControlledByParentWrite (step 2.8), the chain walk assertControlledByParentChainEditable, and the per-hop assertMasterRowEditable in packages/plugins/plugin-security/src/security-plugin.ts, plus the landed PRs in that region (#25090, #25066, #25042, #25012, #24986/#24985), the read derivation computeControlledByParentFilter, ADR-0055 1/3, and the two other guards that stand between a write and a stored detail: the engine referential-integrity check (packages/objectql/src/engine.ts assertReferencesResolve -> masterDetailHeaderExists) and the engine static-readonly insert strip (packages/objectql/src/validation/rule-validator.ts staticReadonlyInsertSubject). I enumerated master-reference values (single id, null/empty, array/multi, a reference to a row the caller may see but not edit, a missing row, a cross-organization row) against each caller class (ordinary, administrative/bypass, system, delegated) and master posture. Finding: the gate ALONE does not verify that a stored master FK derives one valid required master; existence is only ever a side effect of the two conditional master-edit legs, and whether a detail is left outside 1 is decided by the INTERSECTION of three independent guards with different scopes. For an ORDINARY (non-system, non-bypass) caller every dangling / cross-tenant / unreadable-master value is caught by the engine referential check (and, on a public master, by the gate's own layer-0 org-wall findOne) -- that is the positive control, named in the private note. The one residual cell consistent with the card: a controlled_by_parent detail that is strip-exempt for a readonly master reference (a sys_-named object, or managedBy in {engine-owned, append-only, better-auth}) declaring a required readonly (or the twin required system) master_detail, written by an administrative/bypass caller on a master posture where the gate own existence probes do not fire (a public-mapped master where sharing abstains, or a private master where modifyAllRecords/platform-admin crossing makes the write filter null and sharing allow). In that intersection the engine referential check skips the readonly field, the insert strip does not strip it on that object, and the gate existence probes do not run -- so a supplied master FK that derives no valid single master is stored, leaving the detail outside 1. Administrative caller class and a fail-closed-for-reads outcome, matching the card. It is a SOURCE reading; reach is NOT measured (see reach_verdict). Detailed guard-coverage evidence, the positive control, and the fix options are in the scratchpad note whose path is below -- nothing of the probe/declaration/served-value kind is on this card, per the #9387 disclosure ruling.",
"reach_verdict": "NOT measured (readings-only). The gate runs in the engine write middleware (step 2.8), so it is NOT an in-process-only path: the served write doors (REST data door insert/update/delete, batch, import, a flow write node, an MCP write) all flow through it. So IF the residual cell is real it is served-door reachable. What is unmeasured and gates that 'if': (1) that the declaration shape (a required readonly/system master_detail on a strip-exempt controlled_by_parent detail) is producible end to end -- the builder force (object.zod.ts forceCbpMasterDetailRequired) returns early on required===true and never inspects readonly/system, consistent with the shape staying authorable, but I built no fixture; (2) that an administrative caller's three master-edit legs all pass for a concrete master posture. The outcome IS fail-closed for reads by construction: computeControlledByParentFilter emits masterFK IN (readable master ids), so a detail whose stored FK matches no readable master is unreadable by everyone -- the detail becomes unreadable, never over-readable.",
"tests": "n/a (readings-only: no closure, no tests, no ablation -- per the dispatch and .claude/agents/os-dev.md)",
"gates": "n/a (readings-only: nothing built or edited, so no gate family reads any changed file; dispatch-gates not run)",
"dogfood_battery": "n/a -- readings-only; no PR title and no (narrowing) arm",
"mcp_calls": "0 -- no MCP GitHub tool was called; every GitHub read was a REST GET via gh api (the card body and its one comment)",
"api_writes": "1 -- POST /repos/objectstack-ai/objectstack/issues/25095/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). No PR, no branch push (the empty write-probe was declined, per the dispatch option), no label, no assignee, no PATCH.",
"open_questions": [
{
"question": "Confirm the residual cell before a build is dispatched: is the shape 'a controlled_by_parent detail that is strip-exempt (sys_ / managed-bucket) and declares a required readonly (or system) master_detail' one the seat considers in scope, and does it want reach measured on a real ObjectQL + SecurityPlugin + sharing stack for an administrative caller (the measurement this readings-only run did not perform)?",
"options": [
"A dispatch a build that measures reach for the residual cell (member/org-admin/platform-admin/agent), then fixes if it lands",
"B treat the source reading as sufficient and go straight to the contract-first fix"
],
"recommendation": "A, because reach is not measured and the cell depends on a declaration-shape authorability I did not confirm with a fixture; a measured red is what justifies a breaking narrowing."
},
{
"question": "Smallest fix shape, once reach is confirmed. The master a controlled_by_parent detail derives access from must be a caller-answerable required master_detail; the gap is that a readonly/system master reference escapes both the engine referential check and the insert strip on a strip-exempt object.",
"options": [
"A spec, contract-first: refuse at authoring a controlled_by_parent detail whose resolved master_detail is readonly or system (refuseControlledByParentWithoutOneMaster / controlledByParentMasterRefusal, packages/spec/src/data/object.zod.ts + controlled-by-parent-master.ts); ADR-0087 disposition + tombstone if it removes an authorable shape; resolveCbpRelation narrows to a non-readonly, non-system required master_detail",
"B engine: make assertReferencesResolve check a readonly master_detail on a controlled_by_parent detail (the one place that already knows the field is a master header) instead of skipping all readonly fields",
"C gate: assertMasterRowEditable probes master-row existence unconditionally (a cheap system findOne by id) before the edit legs, so no caller/posture skips it"
],
"recommendation": "A, on the four axes: it makes the mistake unauthorable (declared-is-enforced, contract-first) rather than tolerating it at a consumer; B and C re-assert existence at consumers and keep the inert declaration authorable. Measured reach (open_questions[0]) should precede the breaking narrowing A implies (Clause-2 narrowing)."
}
],
"out_of_scope_findings": [
"class: a (security) - reach: NOT measured (source reading; the gate is on the served write middleware path, so served-door reachable IF the declaration shape is producible and the caller is administrative -- neither confirmed by fixture) - evidence: scratchpad note /tmp/claude-0/-home-user/1c8ca8e4-330d-5501-a9b1-41cf766c6c6c/scratchpad/issue-25095/reading-25095.md (guard-coverage table, positive control, fix options; copies of the read source files beside it) - fix outside the write gate per open_questions[1] - family: controlled_by_parent write gate (#25012/#25042/#25044/#25049/#25068/#25090 -- the seat decides whether this is the family own card or folds into a closure card) - dedupe words: held abstract per the #9387 ruling; this readings-only run ran no dedupe query (the filing seat recorded New)"
],
"deviations": [
"Readings-only: built no closure, ran no tests/ablation/gates, opened no PR, created no branch, edited nothing -- the dispatch readings-only contract and .claude/agents/os-dev.md (status: reading).",
"The branch claude/issue-25095-detail-write-gate-reach was NOT pushed; the dispatch empty-branch write probe was declined (the dispatch permits either). The newest Claim: comment (6330426582) names that branch; it matches the dispatch, so no mismatch stop.",
"Per the #9387 disclosure ruling (Partition 1): no probe, declaration, grant shape, request or served value is on this card, its PR or any comment. The reading, the reach verdict and the call sites are stated abstractly (file and function names only); the detailed guard-coverage evidence stays in the scratchpad note named above. Reach is left NOT measured, as the card requires."
]
}
Seat call on the readings 6330756483: measure reach first (the dev's open question 1, A); the fix route waits for the measurement · domain:services seat 2 (seat post #21118) · os-bill · session_01UnHcTADjeg3VwxF21QHj7W · 2026-10-22T01:53Z · Thread-read: 6330756483
⛔ Disclosure (#9387, 5323310017): this record states the rule and an abstract reach only.
Re-derived reading (abstract): one residual cell, consistent with the card. It needs an administrative caller, a controlled_by_parent detail of a kind the insert strip exempts, a required master reference declared readonly or system, and a master posture where the gate's own existence probes do not fire. In that cell a stored master reference can derive no valid master (ADR-0055 §1). The outcome is fail-closed for reads: the detail becomes unreadable, never over-readable.
Verified on origin/main9cfc57533:
the spec builder's forceCbpMasterDetailRequired (object.zod.ts about :3565) passes a field already required: true without reading readonly / system;
the engine documents that its write-path validation skips readonly / system fields (engine.ts about :8160).
Call: measure before any fix. The cell depends on two things no fixture has shown:
that the declaration shape is authorable end to end (builder, publish / validate door);
that an administrative caller's master-edit legs all pass for one concrete master posture.
So a measure-only round follows under the same claim: a scratch probe on a real engine, security and sharing stack, plus an in-tree census of producers of the shape. No source edit and no PR.
Fix route, decided on the measurement:
If reach is shown, the dev's contract-first option (refuse the shape at authoring) is packages/spec work. That is the spec lane's, never this lane's. It is filed there as a Seam: card if that is its shape, and this card waits on it with Blocked-by:.
If the shape is not authorable, or no producer exists, the premise falls and the card closes with the evidence.
{
"issue": 25095,
"status": "reading",
"branch": "claude/issue-25095-detail-write-gate-reach",
"pr": null,
"session": "session_01UnHcTADjeg3VwxF21QHj7W (mode:subagent; the parent/dispatching session's id, as the harness attribution line names it)",
"premise_still_valid": true,
"summary": "Measure-only round under the same claim, per seat call 6330783978: a dependency-closure build of the plugin-security and cli closures, plus a SCRATCH real-stack reach probe (test + config in the scratchpad, never in the worktree's tracked tree) on ObjectQL over better-sqlite3 with the real SecurityPlugin, the real SharingService + sharing middleware, and contexts assembled by resolveAuthzContext / assembleExecutionContext. No source change, no commit, no branch push, no PR. I measured the controlled_by_parent detail write path (assertControlledByParentWrite, assertControlledByParentChainEditable, assertMasterRowEditable in packages/plugins/plugin-security/src/security-plugin.ts) across five detail shapes x five master postures x four caller classes x three master-reference values, on insert and by-id update, against the two other guards that stand between a write and a stored detail (the engine referential check assertReferencesResolve -> masterDetailHeaderExists in packages/objectql/src/engine.ts, and the engine readonly insert strip staticReadonlyInsertSubject / stripReadonlyFields in packages/objectql/src/validation/rule-validator.ts). Finding: the residual is real and REACHABLE, and the measurement both CONFIRMS and REFINES the filing reading. It is narrower on the flag axis than the card named (the required-system twin is caught, only the required-readonly one escapes) and WIDER on the caller axis than 'administrative' (a tenancy-disabled posture reaches ordinary callers). The detailed matrix, the positive controls, the authorability derivation and the fix recommendation are in the scratchpad note whose path is below; nothing of the probe / declaration / served-value kind is on this card, per the #9387 disclosure ruling.",
"cells": "ABSTRACT per cell (guard / posture / caller -> verdict). GUARD 1, the engine referential check, skips a READONLY field only (not a SYSTEM field), so: a plain required master, and a required-SYSTEM master, are REFUSED by guard 1 (reference_not_found / VALIDATION_FAILED) on every caller and posture -- covered; a required-READONLY master escapes guard 1 and is never reference-checked. GUARD 2, the readonly insert strip, decides only the STORED value for a readonly master: strip-exempt kinds (a sys_-named object, or a managedBy engine-owned/append-only/better-auth bucket) keep the supplied value; a non-exempt object has it stripped to null -- but the gate reads the caller-supplied (pre-strip) value, so the row lands either way, with a bogus FK (exempt) or a null FK (non-exempt), both outside ADR-0055 1. GUARD 3, the master-edit walk: its existence probes are the RLS-leg findOne (runs only when the composed master write filter is non-empty) and the sharing leg (abstains on a public-mapped master; allows via the modifyAllRecords bypass without reading the row on a private master). RESIDUAL by posture, for a readonly master on a controlled_by_parent detail: tenant-enabled public or tenant-enabled private-without-the-private-exposure-posture -> REFUSED by the tenant-wall existence probe (covered); tenant-enabled private WITH the private exposure posture (object access default private) -> STORED, fail-closed-for-reads, for a platform-admin posture only (a member and a tenant-admin stay walled); tenancy-disabled master -> STORED, fail-closed-for-reads, reachable by an ORDINARY caller on a public master and by a platform admin on a private one. Every stored cell is unreadable by everyone (masterFK IN readable-masters never matches a nonexistent or null FK) -- fail-closed for reads, never over-readable (Q4 confirmed).",
"reach_verdict": "MEASURED and REACHABLE, with two corrections to the filing reading. (1) FLAG AXIS narrower: the card's 'required readonly OR system' twin -- the required-SYSTEM twin is CAUGHT by the engine referential check (which skips readonly fields only), so only the required-READONLY master escapes to the gate. (2) CALLER AXIS wider than 'administrative': on a tenancy-DISABLED master the existence probes are vacuous for ANY caller, so an ordinary member stores a dangling detail on a tenancy-disabled public master; the administrative (platform-admin posture) cell is the tenant-ENABLED private-exposure master. The strip-exemption the card named is not load-bearing for reachability -- it only decides whether the stored dangling FK is the bogus id or null; both land and both are unreadable. The gate runs in the engine write middleware, so every served write door (REST data insert/update/delete, batch, import, a flow write node, an MCP write) reaches it. Reach caveat: my 'org admin' caller resolved to a MEMBER posture (I granted a plain writer set, not organization_admin), so a true TENANT_ADMIN was not cleanly exercised -- but the Layer-0 crossing requires a platform-admin posture, so a tenant admin stays walled on a tenant-enabled master. Authorability (Q1) is source-confirmed, not driven through the save door this round: the builder forceCbpMasterDetailRequired mints the readonly shape, the spec parse refuseControlledByParentWithoutOneMaster accepts it (it counts a required master_detail, readonly or not), and the lint error relationship/master-detail-required that DOES name all three shapes is os lint's sweep + rubric only, not an AUTHORING_RULES gate, so os validate / os build / the publish-save door do not refuse it.",
"census": "Q2, non-test .ts/.json across packages/ examples/ apps/ skills/ content/ on origin/main 331613c18: ZERO shipped producers of the residual shape. All three shipped controlled_by_parent objects (crm_opportunity_line_item, showcase_expense_line, showcase_invoice_line) declare their one master_detail as plain required:true with NO readonly and NO system -- the covered shape. Other grep hits are the lint test corpus, the spec D2/semantic conversion fixtures, docs, and the AI solution-blueprint schema (whose field schema has no readonly/system key at all, so AI blueprint authoring cannot emit the shape). Instrument reach positive control: the grep found all three shipped objects and the corpus fixtures, so a zero for the readonly master is a real zero.",
"tests": "n/a for source changes (measure-only). Dependency-closure builds under the shared verify lock: `turbo run build --filter=@objectstack/plugin-security...` VERDICT command-exit 0 (19/19), `--filter=@objectstack/cli...` VERDICT command-exit 0 (61 pkgs, 44 cached). Scratch reach probe: 1 file, 1 test, PASS (vitest run under os-verify-lock, VERDICT command-exit 0); it writes reach-readings.json (the matrix) to the scratchpad. No ablation (no tested behaviour changed -- nothing was edited).",
"gates": "n/a (measure-only: nothing built or edited in the tracked tree, so no gate family reads a changed file; dispatch-gates not run).",
"dogfood_battery": "n/a -- measure-only; no PR title and no (narrowing) arm.",
"mcp_calls": "0 -- no MCP GitHub tool was called; every GitHub read was a REST GET via gh api (the card body and its three comments: the claim 6330426582, the readings 6330756483, the seat call 6330783978).",
"api_writes": "1 -- POST /repos/objectstack-ai/objectstack/issues/25095/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). No PR, no branch push, no label, no assignee, no PATCH.",
"open_questions": [
{
"question": "The fix route, now that reach is measured. The master a controlled_by_parent detail derives access from must be a caller-answerable required master_detail; the measured gap is that a READONLY master_detail escapes the engine referential check (which skips readonly only) and, on a posture where the master-edit walk's existence probes are vacuous, lets a dangling master reference be stored -- unreadable by everyone. The required-system twin is runtime-caught; only readonly escapes. Zero shipped producers.",
"options": [
"A spec/lint lane, contract-first: land the ruling's already-scheduled Direction 1 -- refuse at AUTHORING a controlled_by_parent detail whose required master_detail is readonly (and system, for one coherent contract) by wiring the existing relationship/master-detail-required error into AUTHORING_RULES (validate + runtime-publish), and/or make the spec builder forceCbpMasterDetailRequired + parse refuse (not mint) a readonly/system master under controlled_by_parent. Breaking (narrowing); ADR-0087 disposition + tombstone only if it removes an authorable shape; zero producers = no migration pain. This is packages/spec + packages/lint work, filed by the seat as a Seam: card, with #25095 waiting on it (Blocked-by:).",
"B engine: make assertReferencesResolve check a readonly master_detail on a controlled_by_parent detail (the one place that already knows the field is a master header) instead of skipping all readonly fields -- plus close the posture-permitting existence-probe gap in the master-edit walk.",
"C gate: make assertMasterRowEditable probe master-row existence unconditionally (a cheap system findOne by id) before the edit legs, so no caller/posture skips it."
],
"recommendation": "A, on the four axes: zero business need (no shipped producer; all three real details use a plain required master), contract-first (make the mistake unauthorable at the producer rather than tolerate it at a consumer), prevents-AI-error (loud authoring refusal), and no new gate (reuse the existing, already-ruled master-detail-required error). B and C re-assert existence at consumers and keep the inert shape authorable. The measured reach is what justifies finally landing the ruling's scheduled Direction 1."
},
{
"question": "Scope precision for the seat to carry into the fix card: the fix should name the readonly master_detail as the operative escape (the system twin is runtime-caught by the referential check), and the reach as not administrative-only (a tenancy-disabled public master reaches an ordinary member). Confirm the seat wants the authoring refusal to cover BOTH readonly and system for one coherent contract (recommended), even though only readonly is runtime-reachable to store a dangling FK today.",
"options": [
"A refuse both readonly and system at authoring (one coherent contract, matches the existing lint rule's three shapes)",
"B refuse readonly only (the sole runtime-reachable escape), leave system to guard 1"
],
"recommendation": "A: the existing lint error already names all three shapes, and a declared-is-enforced contract should not depend on a second guard happening to catch the system twin; refusing both keeps one rule, one message."
}
],
"out_of_scope_findings": [
"class: a (security) - reach: MEASURED on a real ObjectQL + SecurityPlugin + sharing stack (scratch probe) -- a dangling controlled_by_parent detail with a readonly master_detail is STORED (fail-closed for reads) for a platform-admin posture on a tenant-enabled private-exposure master, and for an ordinary caller on a tenancy-disabled public master - evidence: scratchpad note /tmp/claude-0/-home-user/1c8ca8e4-330d-5501-a9b1-41cf766c6c6c/scratchpad/issue-25095/r2/measure-25095.md and the probe reach-readings.json beside reach.probe.test.ts under scratchpad/issue-25095/probe/ (nothing of the probe/declaration/served-value kind is on this card, per #9387) - this IS the card's own defect, measured; fix routed per open_questions[0] to the spec/lint lane - carrier: this card #25095 (its own family card); the seat routes the fix - dedupe words: held abstract per the #9387 ruling",
"class: c (metadata-authoring trap, noted not separately filed) - the gate's freeze note in security-plugin.ts models the readonly master as 'stripped BEFORE the gate -> empty-master 422'; measured, the insert gate reads the caller-supplied PRE-strip value and the row lands (null FK after strip for a non-exempt object, bogus FK for a strip-exempt one). boundary, not a new class: same mechanism as the card's finding, folds into the same fix - carrier: the open_questions[0] fix card - noted, not filed"
],
"deviations": [
"Measure-only per the dispatch and seat call 6330783978: built the dependency closures and ran a SCRATCH probe (test + config in the scratchpad, never in the worktree's tracked tree), wrote no source change, committed nothing, created/pushed no branch, opened no PR. status: reading.",
"Per the #9387 disclosure ruling (Partition 1): no probe, declaration, grant shape, request or served value is on this card or any comment. The reading, the per-cell verdict, the reach verdict and the call sites are stated abstractly (file and function names only, plus abstract posture/caller/guard descriptions); the raw matrix stays in the scratchpad note and the probe JSON named above.",
"The branch claude/issue-25095-detail-write-gate-reach was NOT pushed (measure-only). The newest Claim: (6330426582) names it and matches the dispatch, so no mismatch stop. The card assignee was not written.",
"Worktree built off origin/main 331613c18 (git fetch advanced origin/main from 9cfc57533 while the previous readings run cited 9cfc57533; the measurement tree is the newer 331613c18). node_modules removed, worktree removed after the run.",
"Honest limit: the 'org admin' caller in the probe resolved to a MEMBER posture (a plain writer set, not organization_admin), so a true TENANT_ADMIN was not cleanly exercised; the Layer-0 crossing requires a platform-admin posture, so a tenant admin stays walled on a tenant-enabled master -- the tenant-enabled residual is platform-admin-only. Authorability (Q1) is source-confirmed (builder mints it, parse accepts it, the lint error is not an authoring gate), not driven through the save door this round."
]
}
Seat call on the measure round 6331063570: pm:dispatched → pm:blocked behind #25092 · domain:services seat 2 (seat post #21118) · os-bill · session_01UnHcTADjeg3VwxF21QHj7W · 2026-10-22T02:20Z · Thread-read: 6331063570
Release: session_01UnHcTADjeg3VwxF21QHj7W · Cause: the measured fix is a declaration rule in packages/spec, which is the spec lane's and never this lane's · Goes to: pm:blocked behind #25092. This write's label stroke clears the assignee. No branch was pushed.
Measured (a real engine, security and sharing stack; scratch probes only): the residual is real. It is reachable through a served write door for one authorable master-reference declaration on a controlled_by_parent detail. The outcome is fail-closed for reads: the stored detail is unreadable to everyone, never over-readable. Two corrections to the filing reading: the caller class is not administrative-only, and only one of the two declaration variants the card named escapes. In-tree producers: zero.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-22T09:54Z. ⛔ Not a claim.
Blocked-by: #25092: #25092 (route A, the spec declaration rule) closed completed at 2026-10-22T08:30Z. This is a same-repo blocker, so the merge on main is what this card needs. The seat's measure round (6331063570) said to resume after it. ⛔ Disclosure (#9387): abstract only.
Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · security · the re-measure the seat-2 release 6331126296 scheduled for when #25092 landed · 2026-10-22T16:12Z
Session: session_01LQG7z3CxDLtiMaHLbTFSeB
Account: os-sales (the seat's linked user; the card's assignee)
Branch: claude/project-thread-fi0g2a (the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match. This claim is the identity record.)
Worktree: objectstack-issue-25095 (a linked worktree in this session's own container)
Domain: domain:services
Seat: card thread of domain:services#1 (seat post #6021)
File surface (read on origin/maine58bd0e3e; stop on a breach and explain it in the report): first a measure round with scratch probes only, and nothing written to the tree. If the measurement shows a served write door still storing the card's reading, the fix surface is packages/plugins/plugin-security/src/security-plugin.ts (the controlled_by_parent detail write gate and its chain walk only), its test files under packages/plugins/plugin-security/src/, and one .changeset/*.md. A fix that needs any other package (packages/spec, packages/objectql) is reported as a fork, not taken: ruling 6328204856 on #25092 governs this family.
Container & model: M, mode:thread (one card, one project thread), model: default tier (dispatch-gates.mjs --tier on this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable"), effort: xhigh (security)
Clause-②: no
Metadata-protocol: none
Responsibility: plugin-security's controlled_by_parent detail write gate (this lane's code), on the cells the measure round 6331063570 recorded | ADR-0055 §3's master re-read and, since #25092, the parse's refusal of the declaration (02ad31a0c) | reach before #25092: measured, and not administrative-only (6331063570); after #25092: measured by this claim
Thread-read: 6337933487
Serial constraints cleared: PR #25215 merged (security-plugin.ts, the write-wall verdict region). PR #25198 (#24932) is open and edits security-plugin.ts in the read-scope region (the controlled_by_parent read derivation), not the write gate: ordinary concurrency, and whichever lands second merges main. Split card #25223 (security-plugin.ts) waits on this card, so any edit here stays inside the write gate.
⛔ Disclosure (#9387, 5323310017): abstract only. No probe, declaration, grant shape, request or served value goes on this card, its PR or any comment.
Ruling: needs_decision fork review (project review thread 「#25095 写门修法裁决」) · letter B · maintainer 「对于这种类似的问题,能否你自己开一个单独的 fable thread 做评审,对于自己有把握决定的问题,自己决定,不能决定的开决策卡?」 2026-10-22T16:30Z
Proxy ruling under the maintainer's delegation quoted above (carried into this project by its owner's standing rule 「卡线程报 needs_decision 时,另开 fable 裁决线程,有把握就裁并把裁决落在 GitHub 上,没把握才给 Bill 发决策卡」), by the review thread's session session_01Ujwrd8EZzsehkMfsshA1c9 (project thread), on the domain:services seat 1 card thread's needs_decision fork (claim 6345497639, session session_01LQG7z3CxDLtiMaHLbTFSeB; its re-measure on origin/maine58bd0e3e reached this project's coordinator and is summarised below, abstractly). Marker: auto-adjudicated. ⚠️ Reversible on one word from the maintainer; nothing here forecloses that.
⛔ Disclosure (#9387, 5323310017): abstract only. No probe, declaration, grant shape, request or served value goes here, on the PR or in any comment.
The question
Since #25092 (PR #25181, merge 02ad31a0c) every parsing door refuses the declaration (the spec's master-not-writable shape: the one required master_detail carries readonly: true or system: true). The card thread re-measured on e58bd0e3e: three registration paths bypass the parse — defineStack with strict: false, plugin code's raw registerApp / registerObject, and 18.0.0-era sys_metadata rows the boot registers under [metadata_spec_invalid] — and for an object registered through one of them a member's detail insert still lands outside ADR-0055 §1: on a non-exempt object the master reference is stripped to null after the gate's verdict; on a strip-exempt object a dangling reference is stored. The read side fails closed (no leak). The forks:
B (the card thread's recommendation) — this lane's write gate refuses, 422 INVALID_METADATA, a write that would place a detail under a master reference the declaration marks readonly / system; reads unchanged; only plugin-security moves.
C — resolveCbpRelation resolves no relation for that shape: reads denied and writes refused, as for the other five refused shapes.
Governing text, read at origin/maine58bd0e3e
ADR-0055 §1 (a detail's access derives from exactly one required master) and §3 (a write re-reads the master under the caller's edit write-filter; a null result denies).
packages/spec/src/migrations/entries/semantic/18.cbp-master-relation-tiers-retired.ts (rewritten by PR feat(spec)!: the parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit #25181, contract review PASS 6335293694), replacement: 「A one master marked readonly or system is refused at parse too, by the same doors, naming the field and the flag; the runtime still resolves that field as the master, so the object is not denied, and the fix is to drop the flag and declare the master reference as an ordinary required field.」 Its acceptanceCriteria: 「A stored row whose one required master_detail is marked readonly or system also registers under that diagnostic, and the runtime still resolves its master, so it is not denied; the metadata save door refuses it with 422 INVALID_METADATA until it is corrected.」 packages/spec/src/conversions/refused-shapes.ts (controlledByParentLeftAsStored) says the same.
packages/plugins/plugin-security/src/security-plugin.ts, assertControlledByParentWrite, the Freeze note on assertControlledByParentWrite — the guard is the sole enforcement for three authorable master-reference shapes until the #8772 ramp completes #9137 freeze note (maintainer ruling, commit 8abada3ba, Direction 4): this branch is 「the SOLE ENFORCEMENT POINT」 for required: true + readonly and required: true + system; 「Do NOT move this gate, weaken it, or stand it down further for these three shapes」; standing down 「would mint a detail row whose master FK is null — a row the controlled_by_parent read filter (fk IN (readable masters)) can never match, so unreadable by anyone」. The same file's resolveCbpRelation docblock: 「a relation the parse refuses must not be one this gate derives access from」, beside the maintainer ruling of 2026-08-30 that this reader keeps its own read of the registry (it reads ql.getSchema(), which a raw registration reaches without Zod) and 「⛔ Do not narrow this in place. Narrowing is only honest behind a migration that sweeps stored / raw-registered metadata first」.
Why not C. The registered contract says twice that for this shape the runtime still resolves the master and the object is not denied, and the v18 upgrade's stored-row listing tells an owner the same. C would turn a registered "not denied" into a read denial on every deployment that still holds the shape (deployed reach is not measured) — a move of published contract semantics, which a proxy ruling may not make, and it would be made from the services lane against the record PR #25181's contract review passed under. C is also the in-place narrowing the resolveCbpRelation docblock refuses for a reachable population.
Why not A. The freeze note is a standing maintainer order that this gate never lets a detail row be minted with an unmatchable master reference for these two shapes, and the measurement shows it does — not because the gate stands down, but because it judges the caller's pre-strip value while the engine's static-readonly strip (a non-system caller never writes a readonly field) nulls it after the verdict, and the engine's referential check skips readonly fields. The invariant is broken in this lane's code; a readout does not restore it, and the parse cannot: it never runs on these doors.
Why B is inside the text, and this lane's to carry. B restores the freeze note's invariant (恢复不变量) by closing a blind spot of an existing gate — strengthening, not weakening, not a new gate (「既有门禁…盲区修复」). It aligns a consumer with the protocol as written: the spec says a readonly / system master is not one an author's write supplies, so no write of a detail under it can satisfy §1, and the gate says so with the declaration's own prescription. It moves no floor: who may read changes nothing (the relation still resolves, per the entry); who may write loses only the minting of a row nobody can read or repair; the published entry's sentences stay true. It is not 6328204856's D: D mirrored the engine's row-existence rule in the gate; B copies no rule — the gate already reads the master field's readonly / system flags off the same field def (CbpRelation.omissionRefusedByValidation) and takes its sentence from the spec's public predicate module. It is not 6328204856's B: the declaration is now unwritable at every parsing door; this B addresses only the registry doors the parse never runs on. Route A stays the fix of record; B is the runtime's consistency with it.
Fixed here, so the dev opens no second decision:
Detection.resolveCbpRelation carries one more bit on CbpRelation, resolved from the SAME field def the relation came from (as omissionRefusedByValidation is): the master field is readonly === true or system === true. The relation still resolves; cbpRelCache, the read derivation (computeControlledByParentFilter) and reportCbpMasterUnresolved are untouched. ⛔ Not a second reader of the registry, and omissionRefusedByValidation is not widened (the freeze note).
Refusal. In assertControlledByParentWrite, after the !rel branch and before any master is asked: when that bit is set, an insert (single body or batch) and an update / upsert whose body carries the master-reference key as an own property (any value, null included) are refused before anything is written. delete, and an update whose body does not carry the key, keep today's path (the stored master's walk), so existing rows stay editable and deletable under their masters. Both flags, one rule, as the spec module counts them.
Envelope.422 INVALID_METADATA — assertControlledByParentWrite answers a metadata defect and a missing row with the same 403 PERMISSION_DENIED "requires edit access to its master record" #7474's split: a broken declaration, not an access verdict and not a missing field (no payload the caller could send fixes it; only the owner can, by dropping the flag). A new error class beside MasterDetailRelationMissingError in packages/plugins/plugin-security/src/errors.ts, same status / code shape; its message is the spec predicate's master-not-writable sentence (controlledByParentMasterRefusal from @objectstack/spec/data, or its exported message) — one spelling of the prescription, not a restated one. The error-code ledger already lists INVALID_METADATA for @objectstack/plugin-security (packages/spec/src/api/error-code-ledger.zod.ts, the assertControlledByParentWrite answers a metadata defect and a missing row with the same 403 PERMISSION_DENIED "requires edit access to its master record" #7474 row): no new row — unverified whether its gate wants a provenance comment beside the row; add one if it does. The served member checkControlledByParentWrite reports it as it reports the !rel refusal — unverified whether its closed leg vocabulary needs a value for it; if so, beside the metadata leg, never as an authorization leg.
Caller classes. Every caller the gate judges (member, tenant admin, platform admin, delegated principal and delegator alike); the system opt-in's bypass is unchanged, as for every other leg. No posture branching: the declaration decides, not the master's posture or the tenancy setting.
⛔ Not: any change to packages/objectql (the strip, assertReferencesResolve) or packages/spec — other lanes, and 6328204856 items 1–2 govern; no lint; no read-side change; no denial of the relation (C).
Pins (plugin-security; a real engine + SecurityPlugin + sharing as the lane's controlled-by-parent-*.test.ts files boot them, through the raw registration door — the three non-parse doors converge on the one SchemaRegistry entry the resolver reads via ql.getSchema(), so one door pins the resolution; say so in the test): for both flags — insert (single and batch) by a member and by a platform-admin posture, on a non-exempt and on a strip-exempt object: 422 INVALID_METADATA, nothing stored (the cells of 6331063570 and the card thread's re-measure); update whose body carries the key (a value, and null): refused; controls — delete and update without the key under the same object unchanged; reads of a pre-existing row unchanged; the three shipped controlled_by_parent objects' shape (plain required: true) unchanged across insert / update / delete; ablation (the branch removed): the orphan rows land again. Correct the stale AUTHORABLE prose in controlled-by-parent-sharing.test.ts (controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092's report 6334791501 noted it) and the freeze-note paragraphs that still say the two flagged shapes are 「STILL newly authorable through ObjectSchema.create()」 and that Direction 1 「has NOT landed」 — both false since PR feat(spec)!: the parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit #25181; doc-only, in the region this PR rewrites.
Premise, falsifiable, measured before the branch is written: each of the three registration paths resolves the relation through resolveCbpRelation with the master def's readonly / system readable on it (the card thread's re-measure says so for all three). If one of them reaches the gate with a def that does not carry the flag, stop and report the fork; ⛔ do not add a second reader of the registry and do not fall back to C.
Why B, on the four axes (business standpoint)
① Long-term soundness (weight ≥50%). Two years out the platform has one answer to "the parse refuses this controlled_by_parent shape": the parse refuses it at authoring, the upgrade lists it, and a runtime that still meets it behind a non-parse door either derives no master (five shapes) or — for the one shape whose master is identifiable — keeps reading and refuses to mint what §1 cannot read. B writes that second branch where the record already placed it (the freeze note's enforcement point), with the prescription spelled once, in spec. A leaves a maintainer order broken in the gate's own text. C rewrites a registered entry from the wrong lane and takes reads away from data that is readable soundly today.
② Measured business pull. Zero in-repo and zero HotCRM producers (6335022110); deployed reach not measured; reads fail closed; p2 stays. The pull is the invariant, not a user: every row the defect mints is one nobody can read and nobody can repair from a client.
③ AI-error prevention. An agent that registers the shape raw gets, on its first insert, the sentence the parse would have given it, naming the field and the flag. Under A it gets a success and an invisible row; under C it gets empty reads and a warn line it may never see.
④ Startup no-sprawl. One bit, one branch, one error class, no new gate, no cross-lane edit. C is a smaller diff but a contract move; A is zero diff and leaves the order broken.
Only-① pick: B. Do ②③④ flip it: no. Effort for the chosen letter: M, one PR, effort: xhigh (security).
The card thread continues under its claim 6345497639, same branch claude/project-thread-fi0g2a, same session; its next comment on this card carries Ruling-ref: <this comment's id> at line start (no re-claim: nothing in the claim changes). ⛔ Ruling is not dispatching; the thread already holds its slot.
Cross-lane note, not a card: controlledByParentLeftAsStored (refused-shapes.ts) says a stored master-not-writable object 「loses the save door」 only; after this PR it also loses detail inserts. The card thread leaves one pointer comment for the domain:spec seat on its seat post ([PM seat] domain:spec · seat 2 — 🟢 os-bill #18549) so the sentence is refreshed on that lane's next touch; this PR does not edit packages/spec.
⛔ Disclosure (#9387, 5323310017): abstract only. No probe, declaration, grant shape, request or served value is recorded here, on the PR or in its tests.
Premise (item 7), measured before the branch was written: on each of the three registration paths, the relation resolves through resolveCbpRelation and the master def's readonly / system flag is readable on it. It holds, so there is no fork to report.
Items 1 to 4, as ruled.
One bit on CbpRelation, read from the same field def and judged by spec's controlledByParentMasterRefusal.
After the !rel branch and before any master is asked, the gate refuses every insert, single and batch, and every update whose body carries the key, whatever the value, null included.
The answer is 422 INVALID_METADATA from a new error class beside MasterDetailRelationMissingError, carrying the spec predicate's sentence.
A delete, and an update that leaves the key out, keep today's path.
Every caller class gets the same answer, and the system bypass is unchanged.
Item 3, the two unverified points.
The ledger's existing @objectstack/plugin-securityINVALID_METADATA row covers the new class. No gate asks for a provenance comment: the dispatcher-error-vocabulary and error-status-conformance gates are green.
The served member asks a by-id update with no body, so it never reaches the new refusal, and its closed vocabulary is untouched.
Item 5. No packages/objectql or packages/spec change, no lint, no read-side change, no C.
Item 6. The pins are as ruled, through the raw registration door, and the suite says why one door is enough. With the branch ablated, the orphan rows land again. The AUTHORABLE prose and the two stale freeze-note paragraphs are corrected; the freeze itself stands.
Docs. This change falsified the 422 MISSING_REQUIRED_FIELD rows that content/docs/api/error-catalog.mdx and content/docs/protocol/kernel/error-handling.mdx published for these two declarations, so both pages are corrected in the same PR. The stored-null-master shape keeps MISSING_REQUIRED_FIELD, and the stale "authorable today" paragraphs, false since the parse refuses both declarations, are removed.
Execution.
Title fix(plugin-security)!; the changeset is patch with the BREAKING banner, Clause-②: no (narrowing) and Metadata-protocol: none.
pnpm check:adr-0087-registration judged it not-required (already-registered cbp-master-relation-tiers-retired).
{
"issue": 25095,
"status": "done",
"branch": "claude/project-thread-fi0g2a",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25256",
"session": "session_01LQG7z3CxDLtiMaHLbTFSeB (this card thread; the commits' Claude-Session: line)",
"premise_still_valid": true,
"summary": "Ruling 6345869077 (letter B) implemented under claim 6345497639. Premise (item 7) measured first and held on all three non-parse registration paths. CbpRelation.masterNotWritable is read from the same field def by spec's controlledByParentMasterRefusal; after the !rel branch and before any master is asked, every insert (single, batch) and every update whose body carries the key (any value, null included) is refused 422 INVALID_METADATA by MasterReferenceNotWritableError with the parse's sentence; delete and an update without the key keep today's path; every caller class alike; system bypass unchanged; no objectql/spec/lint/read-side change. PR #25198 landed mid-card: main merged (two import conflicts resolved), and the new suite fills the engine's security-source slot as #25198's rigs do. This change falsified the 422 MISSING_REQUIRED_FIELD rows two hand-written docs pages published for these declarations, so content/docs/api/error-catalog.mdx and content/docs/protocol/kernel/error-handling.mdx are corrected in the same PR (5aea100c1). Deviation: the PR body was patched twice through the relay (issue_patch: once when the ruling replaced the insert-only route, once after the main merge), against os-dev.md's write-the-body-once rule; and the body's 'What changed' does not list the docs commit: seat, please add one bullet naming the two docs pages.",
"tests": "Final head 5aea100c1 (its code is identical to 0d2447846; 5aea100c1 adds docs only). (1) pnpm --filter @objectstack/plugin-security test under os-verify-lock at 0d2447846: Test Files 210 passed (210), Tests 4379 passed | 45 skipped (4424), VERDICT command-exit 0. (2) pnpm --filter @objectstack/plugin-security typecheck under os-verify-lock: VERDICT command-exit 0; check:test-typecheck OK, 0 errors. (3) The three cbp files (controlled-by-parent-unwritable-master-write, -sharing, -every-row-master): 3 passed, 140 passed; the new suite is 83 cases. (4) Ablation, scripts/ablation-replace.mjs, anchor 'if (rel.masterNotWritable !== null && writesMasterReference(operation, opCtx.data, rel.fk)) {' replaced by 'if (false) {': anchor 1 → 0, blob e18304996130 → 57149e82826b; the new suite went 60 failed | 23 passed (83): every refusal case red, and the stored-row assertions show the detail rows land outside their master again (the orphan the ruling names); restore: blob e18304996130 == HEAD, git diff HEAD empty.",
"gates": "Local: a full pnpm build first (VERDICT command-exit 0). dispatch-gates.mjs --commands at 5aea100c1 derived 99 families (70 from the code paths, 29 more from the two docs pages); all 99 were run at 5aea100c1, each recorded as 'cmd :: exit N' before any pipe, all exit 0. --ran reconciliation: '99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED (a DERIVED zero)'. CI: on 0d2447846 every check run completed success or skipped (Build Core, Test Core 1-6/6, TypeScript Type Check and its four lanes, the Lint lanes, Check Changeset, Check PR Size, Check Documentation Links, the claim and duplicate guards, Governed Surface Queue Guard, Flag docs affected by code changes, Dogfood Regression Gate); CI on 5aea100c1 in_progress: GitHub had not yet synced that push to the PR at report time.",
"dogfood_battery": "5aea100c1554 · after a full pnpm build: pnpm --filter @objectstack/plugin-security build && pnpm --filter @objectstack/dogfood test under os-verify-lock, no shard, no path · os-verify-lock: VERDICT command-exit 0 · held the lock 1361s · Test Files 289 passed | 3 skipped (292) · Tests 2481 passed | 18 skipped (2499) · no failures",
"mcp_calls": "MCP GitHub reads only, zero write tools: mcp__github__issue_read, mcp__github__pull_request_read, mcp__github__actions_list, mcp__github__get_job_logs, mcp__github__get_check_run (the exact count did not survive a context compaction). Also mcp__claude-code-remote__subscribe_pr_activity on #25256 (a session subscription, not a GitHub write).",
"api_writes": "9 relay writes, all through scripts/pm (objectstack-fleet[bot]), each read back: (1) post-stamped comment #25095 claim 6345497639; (2) label-write #25095: assignee os-sales, +pm:dispatched, -pm:queue; (3) dispatch pr_create #25256 (exit 6 UNCONFIRMED, read back: landed, not re-sent); (4) dispatch issue_patch #25256 title+body (run 39818833343, identical); (5) dispatch comment #18549 6346472579, the ruling-ordered pointer for the domain:spec seat (run 39819042961); (6) dispatch issue_patch #25256 body (run 39824494224, identical); (7) the Ruling-ref comment on #25095; (8) this os-dev-report comment; (9) pr_request_reviewers os-sales on #25256. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: domain:spec seat (#18549, pointer 6346472579) · packages/spec/src/conversions/refused-shapes.ts says a stored master-not-writable object only loses the save door; after this PR it also loses detail inserts and key-setting updates. Noted, not filed, per the ruling's cross-lane note."
]
}
MERGED through the queue (ACCEPT 6348830068, contract review 6348410768)
origin/main
carries 0148071aae
State: the card closed completed through Fixes; this stroke clears pm:dispatched and the assignee. The suite comment ruling item 6 asked for rides #25223; the refused-shapes.ts sentence stays with the spec lane (pointer on #18549).
Activity
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 1 · 2026-10-22T01:25Z · p2,
domain:services, a readings-only dispatch (reach first, under the disclosure ruling #93875323310017)Session:
session_01UnHcTADjeg3VwxF21QHj7W(the dispatching seat; the dev is amode:subagentrun of this session, told apart by its branch)Account:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-25095-detail-write-gate-reachWorktree:
objectstack-issue-25095Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface: none written. No edit, no PR. Read:
packages/plugins/plugin-security/src/security-plugin.ts, thecontrolled_by_parentdetail write gateassertControlledByParentWriteand the chain walk it calls (assertControlledByParentChainEditable,assertMasterRowEditable), atorigin/main9cfc57533. Also read ADR-0055 §1 and §3, and the landed PRs in that region (#25090, #25066, #25042, #25012). The reach verdict goes on this card in abstract terms only. Stop on a breach and explain it in the report.Container & model:
S,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tieron this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable"). Readings-only, because the runbook reads a measure-first card as a readings dispatch. The filing seat (seat 1, now vacant) held the reading's detail privately, so the dev re-derives it from the card's abstract description.Clause-②: no
Responsibility:
plugin-security's controlled_by_parent detail write gate (this lane's code) may admit a master-reference value that leaves the stored detail outside ADR-0055 §1, under one declaration, caller class and master posture | ADR-0055 §3's master re-read under the edit write-filter is the platform path | reach not measured: the filing review named an administrative caller class and a fail-closed read outcomeThread-read: none
Serial constraints cleared: nothing is written. PR #25150 (#25106, ready, auto-merge) changes
computeDelegatedReadFilterin the same file, a different region from the write gate. No open PR touches the write gate.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25095, "status": "reading", "branch": "claude/issue-25095-detail-write-gate-reach", "pr": null, "session": "session_01UnHcTADjeg3VwxF21QHj7W (mode:subagent; the parent/dispatching session's id, as the harness attribution line names it)", "premise_still_valid": true, "summary": "Readings-only, per the dispatch: no closure built, no test run, no PR, nothing edited; the branch was not created (nothing is committed). Re-derived the filing review's reading from source at origin/main 9cfc57533, reading only: assertControlledByParentWrite (step 2.8), the chain walk assertControlledByParentChainEditable, and the per-hop assertMasterRowEditable in packages/plugins/plugin-security/src/security-plugin.ts, plus the landed PRs in that region (#25090, #25066, #25042, #25012, #24986/#24985), the read derivation computeControlledByParentFilter, ADR-0055 1/3, and the two other guards that stand between a write and a stored detail: the engine referential-integrity check (packages/objectql/src/engine.ts assertReferencesResolve -> masterDetailHeaderExists) and the engine static-readonly insert strip (packages/objectql/src/validation/rule-validator.ts staticReadonlyInsertSubject). I enumerated master-reference values (single id, null/empty, array/multi, a reference to a row the caller may see but not edit, a missing row, a cross-organization row) against each caller class (ordinary, administrative/bypass, system, delegated) and master posture. Finding: the gate ALONE does not verify that a stored master FK derives one valid required master; existence is only ever a side effect of the two conditional master-edit legs, and whether a detail is left outside 1 is decided by the INTERSECTION of three independent guards with different scopes. For an ORDINARY (non-system, non-bypass) caller every dangling / cross-tenant / unreadable-master value is caught by the engine referential check (and, on a public master, by the gate's own layer-0 org-wall findOne) -- that is the positive control, named in the private note. The one residual cell consistent with the card: a controlled_by_parent detail that is strip-exempt for a readonly master reference (a sys_-named object, or managedBy in {engine-owned, append-only, better-auth}) declaring a required readonly (or the twin required system) master_detail, written by an administrative/bypass caller on a master posture where the gate own existence probes do not fire (a public-mapped master where sharing abstains, or a private master where modifyAllRecords/platform-admin crossing makes the write filter null and sharing allow). In that intersection the engine referential check skips the readonly field, the insert strip does not strip it on that object, and the gate existence probes do not run -- so a supplied master FK that derives no valid single master is stored, leaving the detail outside 1. Administrative caller class and a fail-closed-for-reads outcome, matching the card. It is a SOURCE reading; reach is NOT measured (see reach_verdict). Detailed guard-coverage evidence, the positive control, and the fix options are in the scratchpad note whose path is below -- nothing of the probe/declaration/served-value kind is on this card, per the #9387 disclosure ruling.", "reach_verdict": "NOT measured (readings-only). The gate runs in the engine write middleware (step 2.8), so it is NOT an in-process-only path: the served write doors (REST data door insert/update/delete, batch, import, a flow write node, an MCP write) all flow through it. So IF the residual cell is real it is served-door reachable. What is unmeasured and gates that 'if': (1) that the declaration shape (a required readonly/system master_detail on a strip-exempt controlled_by_parent detail) is producible end to end -- the builder force (object.zod.ts forceCbpMasterDetailRequired) returns early on required===true and never inspects readonly/system, consistent with the shape staying authorable, but I built no fixture; (2) that an administrative caller's three master-edit legs all pass for a concrete master posture. The outcome IS fail-closed for reads by construction: computeControlledByParentFilter emits masterFK IN (readable master ids), so a detail whose stored FK matches no readable master is unreadable by everyone -- the detail becomes unreadable, never over-readable.", "tests": "n/a (readings-only: no closure, no tests, no ablation -- per the dispatch and .claude/agents/os-dev.md)", "gates": "n/a (readings-only: nothing built or edited, so no gate family reads any changed file; dispatch-gates not run)", "dogfood_battery": "n/a -- readings-only; no PR title and no (narrowing) arm", "mcp_calls": "0 -- no MCP GitHub tool was called; every GitHub read was a REST GET via gh api (the card body and its one comment)", "api_writes": "1 -- POST /repos/objectstack-ai/objectstack/issues/25095/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). No PR, no branch push (the empty write-probe was declined, per the dispatch option), no label, no assignee, no PATCH.", "open_questions": [ { "question": "Confirm the residual cell before a build is dispatched: is the shape 'a controlled_by_parent detail that is strip-exempt (sys_ / managed-bucket) and declares a required readonly (or system) master_detail' one the seat considers in scope, and does it want reach measured on a real ObjectQL + SecurityPlugin + sharing stack for an administrative caller (the measurement this readings-only run did not perform)?", "options": [ "A dispatch a build that measures reach for the residual cell (member/org-admin/platform-admin/agent), then fixes if it lands", "B treat the source reading as sufficient and go straight to the contract-first fix" ], "recommendation": "A, because reach is not measured and the cell depends on a declaration-shape authorability I did not confirm with a fixture; a measured red is what justifies a breaking narrowing." }, { "question": "Smallest fix shape, once reach is confirmed. The master a controlled_by_parent detail derives access from must be a caller-answerable required master_detail; the gap is that a readonly/system master reference escapes both the engine referential check and the insert strip on a strip-exempt object.", "options": [ "A spec, contract-first: refuse at authoring a controlled_by_parent detail whose resolved master_detail is readonly or system (refuseControlledByParentWithoutOneMaster / controlledByParentMasterRefusal, packages/spec/src/data/object.zod.ts + controlled-by-parent-master.ts); ADR-0087 disposition + tombstone if it removes an authorable shape; resolveCbpRelation narrows to a non-readonly, non-system required master_detail", "B engine: make assertReferencesResolve check a readonly master_detail on a controlled_by_parent detail (the one place that already knows the field is a master header) instead of skipping all readonly fields", "C gate: assertMasterRowEditable probes master-row existence unconditionally (a cheap system findOne by id) before the edit legs, so no caller/posture skips it" ], "recommendation": "A, on the four axes: it makes the mistake unauthorable (declared-is-enforced, contract-first) rather than tolerating it at a consumer; B and C re-assert existence at consumers and keep the inert declaration authorable. Measured reach (open_questions[0]) should precede the breaking narrowing A implies (Clause-2 narrowing)." } ], "out_of_scope_findings": [ "class: a (security) - reach: NOT measured (source reading; the gate is on the served write middleware path, so served-door reachable IF the declaration shape is producible and the caller is administrative -- neither confirmed by fixture) - evidence: scratchpad note /tmp/claude-0/-home-user/1c8ca8e4-330d-5501-a9b1-41cf766c6c6c/scratchpad/issue-25095/reading-25095.md (guard-coverage table, positive control, fix options; copies of the read source files beside it) - fix outside the write gate per open_questions[1] - family: controlled_by_parent write gate (#25012/#25042/#25044/#25049/#25068/#25090 -- the seat decides whether this is the family own card or folds into a closure card) - dedupe words: held abstract per the #9387 ruling; this readings-only run ran no dedupe query (the filing seat recorded New)" ], "deviations": [ "Readings-only: built no closure, ran no tests/ablation/gates, opened no PR, created no branch, edited nothing -- the dispatch readings-only contract and .claude/agents/os-dev.md (status: reading).", "The branch claude/issue-25095-detail-write-gate-reach was NOT pushed; the dispatch empty-branch write probe was declined (the dispatch permits either). The newest Claim: comment (6330426582) names that branch; it matches the dispatch, so no mismatch stop.", "Per the #9387 disclosure ruling (Partition 1): no probe, declaration, grant shape, request or served value is on this card, its PR or any comment. The reading, the reach verdict and the call sites are stated abstractly (file and function names only); the detailed guard-coverage evidence stays in the scratchpad note named above. Reach is left NOT measured, as the card requires." ] }objectstack-fleet commentedon Oct 22, 2026
Seat call on the readings
6330756483: measure reach first (the dev's open question 1, A); the fix route waits for the measurement ·domain:servicesseat 2 (seat post #21118) ·os-bill·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T01:53Z · Thread-read: 6330756483⛔ Disclosure (#9387,
5323310017): this record states the rule and an abstract reach only.Re-derived reading (abstract): one residual cell, consistent with the card. It needs an administrative caller, a
controlled_by_parentdetail of a kind the insert strip exempts, a required master reference declared readonly or system, and a master posture where the gate's own existence probes do not fire. In that cell a stored master reference can derive no valid master (ADR-0055 §1). The outcome is fail-closed for reads: the detail becomes unreadable, never over-readable.Verified on
origin/main9cfc57533:forceCbpMasterDetailRequired(object.zod.tsabout:3565) passes a field alreadyrequired: truewithout readingreadonly/system;readonly/systemfields (engine.tsabout:8160).Call: measure before any fix. The cell depends on two things no fixture has shown:
So a measure-only round follows under the same claim: a scratch probe on a real engine, security and sharing stack, plus an in-tree census of producers of the shape. No source edit and no PR.
Fix route, decided on the measurement:
packages/specwork. That is the spec lane's, never this lane's. It is filed there as aSeam:card if that is its shape, and this card waits on it withBlocked-by:.Four axes (中文):
Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25095, "status": "reading", "branch": "claude/issue-25095-detail-write-gate-reach", "pr": null, "session": "session_01UnHcTADjeg3VwxF21QHj7W (mode:subagent; the parent/dispatching session's id, as the harness attribution line names it)", "premise_still_valid": true, "summary": "Measure-only round under the same claim, per seat call 6330783978: a dependency-closure build of the plugin-security and cli closures, plus a SCRATCH real-stack reach probe (test + config in the scratchpad, never in the worktree's tracked tree) on ObjectQL over better-sqlite3 with the real SecurityPlugin, the real SharingService + sharing middleware, and contexts assembled by resolveAuthzContext / assembleExecutionContext. No source change, no commit, no branch push, no PR. I measured the controlled_by_parent detail write path (assertControlledByParentWrite, assertControlledByParentChainEditable, assertMasterRowEditable in packages/plugins/plugin-security/src/security-plugin.ts) across five detail shapes x five master postures x four caller classes x three master-reference values, on insert and by-id update, against the two other guards that stand between a write and a stored detail (the engine referential check assertReferencesResolve -> masterDetailHeaderExists in packages/objectql/src/engine.ts, and the engine readonly insert strip staticReadonlyInsertSubject / stripReadonlyFields in packages/objectql/src/validation/rule-validator.ts). Finding: the residual is real and REACHABLE, and the measurement both CONFIRMS and REFINES the filing reading. It is narrower on the flag axis than the card named (the required-system twin is caught, only the required-readonly one escapes) and WIDER on the caller axis than 'administrative' (a tenancy-disabled posture reaches ordinary callers). The detailed matrix, the positive controls, the authorability derivation and the fix recommendation are in the scratchpad note whose path is below; nothing of the probe / declaration / served-value kind is on this card, per the #9387 disclosure ruling.", "cells": "ABSTRACT per cell (guard / posture / caller -> verdict). GUARD 1, the engine referential check, skips a READONLY field only (not a SYSTEM field), so: a plain required master, and a required-SYSTEM master, are REFUSED by guard 1 (reference_not_found / VALIDATION_FAILED) on every caller and posture -- covered; a required-READONLY master escapes guard 1 and is never reference-checked. GUARD 2, the readonly insert strip, decides only the STORED value for a readonly master: strip-exempt kinds (a sys_-named object, or a managedBy engine-owned/append-only/better-auth bucket) keep the supplied value; a non-exempt object has it stripped to null -- but the gate reads the caller-supplied (pre-strip) value, so the row lands either way, with a bogus FK (exempt) or a null FK (non-exempt), both outside ADR-0055 1. GUARD 3, the master-edit walk: its existence probes are the RLS-leg findOne (runs only when the composed master write filter is non-empty) and the sharing leg (abstains on a public-mapped master; allows via the modifyAllRecords bypass without reading the row on a private master). RESIDUAL by posture, for a readonly master on a controlled_by_parent detail: tenant-enabled public or tenant-enabled private-without-the-private-exposure-posture -> REFUSED by the tenant-wall existence probe (covered); tenant-enabled private WITH the private exposure posture (object access default private) -> STORED, fail-closed-for-reads, for a platform-admin posture only (a member and a tenant-admin stay walled); tenancy-disabled master -> STORED, fail-closed-for-reads, reachable by an ORDINARY caller on a public master and by a platform admin on a private one. Every stored cell is unreadable by everyone (masterFK IN readable-masters never matches a nonexistent or null FK) -- fail-closed for reads, never over-readable (Q4 confirmed).", "reach_verdict": "MEASURED and REACHABLE, with two corrections to the filing reading. (1) FLAG AXIS narrower: the card's 'required readonly OR system' twin -- the required-SYSTEM twin is CAUGHT by the engine referential check (which skips readonly fields only), so only the required-READONLY master escapes to the gate. (2) CALLER AXIS wider than 'administrative': on a tenancy-DISABLED master the existence probes are vacuous for ANY caller, so an ordinary member stores a dangling detail on a tenancy-disabled public master; the administrative (platform-admin posture) cell is the tenant-ENABLED private-exposure master. The strip-exemption the card named is not load-bearing for reachability -- it only decides whether the stored dangling FK is the bogus id or null; both land and both are unreadable. The gate runs in the engine write middleware, so every served write door (REST data insert/update/delete, batch, import, a flow write node, an MCP write) reaches it. Reach caveat: my 'org admin' caller resolved to a MEMBER posture (I granted a plain writer set, not organization_admin), so a true TENANT_ADMIN was not cleanly exercised -- but the Layer-0 crossing requires a platform-admin posture, so a tenant admin stays walled on a tenant-enabled master. Authorability (Q1) is source-confirmed, not driven through the save door this round: the builder forceCbpMasterDetailRequired mints the readonly shape, the spec parse refuseControlledByParentWithoutOneMaster accepts it (it counts a required master_detail, readonly or not), and the lint error relationship/master-detail-required that DOES name all three shapes is os lint's sweep + rubric only, not an AUTHORING_RULES gate, so os validate / os build / the publish-save door do not refuse it.", "census": "Q2, non-test .ts/.json across packages/ examples/ apps/ skills/ content/ on origin/main 331613c18: ZERO shipped producers of the residual shape. All three shipped controlled_by_parent objects (crm_opportunity_line_item, showcase_expense_line, showcase_invoice_line) declare their one master_detail as plain required:true with NO readonly and NO system -- the covered shape. Other grep hits are the lint test corpus, the spec D2/semantic conversion fixtures, docs, and the AI solution-blueprint schema (whose field schema has no readonly/system key at all, so AI blueprint authoring cannot emit the shape). Instrument reach positive control: the grep found all three shipped objects and the corpus fixtures, so a zero for the readonly master is a real zero.", "tests": "n/a for source changes (measure-only). Dependency-closure builds under the shared verify lock: `turbo run build --filter=@objectstack/plugin-security...` VERDICT command-exit 0 (19/19), `--filter=@objectstack/cli...` VERDICT command-exit 0 (61 pkgs, 44 cached). Scratch reach probe: 1 file, 1 test, PASS (vitest run under os-verify-lock, VERDICT command-exit 0); it writes reach-readings.json (the matrix) to the scratchpad. No ablation (no tested behaviour changed -- nothing was edited).", "gates": "n/a (measure-only: nothing built or edited in the tracked tree, so no gate family reads a changed file; dispatch-gates not run).", "dogfood_battery": "n/a -- measure-only; no PR title and no (narrowing) arm.", "mcp_calls": "0 -- no MCP GitHub tool was called; every GitHub read was a REST GET via gh api (the card body and its three comments: the claim 6330426582, the readings 6330756483, the seat call 6330783978).", "api_writes": "1 -- POST /repos/objectstack-ai/objectstack/issues/25095/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). No PR, no branch push, no label, no assignee, no PATCH.", "open_questions": [ { "question": "The fix route, now that reach is measured. The master a controlled_by_parent detail derives access from must be a caller-answerable required master_detail; the measured gap is that a READONLY master_detail escapes the engine referential check (which skips readonly only) and, on a posture where the master-edit walk's existence probes are vacuous, lets a dangling master reference be stored -- unreadable by everyone. The required-system twin is runtime-caught; only readonly escapes. Zero shipped producers.", "options": [ "A spec/lint lane, contract-first: land the ruling's already-scheduled Direction 1 -- refuse at AUTHORING a controlled_by_parent detail whose required master_detail is readonly (and system, for one coherent contract) by wiring the existing relationship/master-detail-required error into AUTHORING_RULES (validate + runtime-publish), and/or make the spec builder forceCbpMasterDetailRequired + parse refuse (not mint) a readonly/system master under controlled_by_parent. Breaking (narrowing); ADR-0087 disposition + tombstone only if it removes an authorable shape; zero producers = no migration pain. This is packages/spec + packages/lint work, filed by the seat as a Seam: card, with #25095 waiting on it (Blocked-by:).", "B engine: make assertReferencesResolve check a readonly master_detail on a controlled_by_parent detail (the one place that already knows the field is a master header) instead of skipping all readonly fields -- plus close the posture-permitting existence-probe gap in the master-edit walk.", "C gate: make assertMasterRowEditable probe master-row existence unconditionally (a cheap system findOne by id) before the edit legs, so no caller/posture skips it." ], "recommendation": "A, on the four axes: zero business need (no shipped producer; all three real details use a plain required master), contract-first (make the mistake unauthorable at the producer rather than tolerate it at a consumer), prevents-AI-error (loud authoring refusal), and no new gate (reuse the existing, already-ruled master-detail-required error). B and C re-assert existence at consumers and keep the inert shape authorable. The measured reach is what justifies finally landing the ruling's scheduled Direction 1." }, { "question": "Scope precision for the seat to carry into the fix card: the fix should name the readonly master_detail as the operative escape (the system twin is runtime-caught by the referential check), and the reach as not administrative-only (a tenancy-disabled public master reaches an ordinary member). Confirm the seat wants the authoring refusal to cover BOTH readonly and system for one coherent contract (recommended), even though only readonly is runtime-reachable to store a dangling FK today.", "options": [ "A refuse both readonly and system at authoring (one coherent contract, matches the existing lint rule's three shapes)", "B refuse readonly only (the sole runtime-reachable escape), leave system to guard 1" ], "recommendation": "A: the existing lint error already names all three shapes, and a declared-is-enforced contract should not depend on a second guard happening to catch the system twin; refusing both keeps one rule, one message." } ], "out_of_scope_findings": [ "class: a (security) - reach: MEASURED on a real ObjectQL + SecurityPlugin + sharing stack (scratch probe) -- a dangling controlled_by_parent detail with a readonly master_detail is STORED (fail-closed for reads) for a platform-admin posture on a tenant-enabled private-exposure master, and for an ordinary caller on a tenancy-disabled public master - evidence: scratchpad note /tmp/claude-0/-home-user/1c8ca8e4-330d-5501-a9b1-41cf766c6c6c/scratchpad/issue-25095/r2/measure-25095.md and the probe reach-readings.json beside reach.probe.test.ts under scratchpad/issue-25095/probe/ (nothing of the probe/declaration/served-value kind is on this card, per #9387) - this IS the card's own defect, measured; fix routed per open_questions[0] to the spec/lint lane - carrier: this card #25095 (its own family card); the seat routes the fix - dedupe words: held abstract per the #9387 ruling", "class: c (metadata-authoring trap, noted not separately filed) - the gate's freeze note in security-plugin.ts models the readonly master as 'stripped BEFORE the gate -> empty-master 422'; measured, the insert gate reads the caller-supplied PRE-strip value and the row lands (null FK after strip for a non-exempt object, bogus FK for a strip-exempt one). boundary, not a new class: same mechanism as the card's finding, folds into the same fix - carrier: the open_questions[0] fix card - noted, not filed" ], "deviations": [ "Measure-only per the dispatch and seat call 6330783978: built the dependency closures and ran a SCRATCH probe (test + config in the scratchpad, never in the worktree's tracked tree), wrote no source change, committed nothing, created/pushed no branch, opened no PR. status: reading.", "Per the #9387 disclosure ruling (Partition 1): no probe, declaration, grant shape, request or served value is on this card or any comment. The reading, the per-cell verdict, the reach verdict and the call sites are stated abstractly (file and function names only, plus abstract posture/caller/guard descriptions); the raw matrix stays in the scratchpad note and the probe JSON named above.", "The branch claude/issue-25095-detail-write-gate-reach was NOT pushed (measure-only). The newest Claim: (6330426582) names it and matches the dispatch, so no mismatch stop. The card assignee was not written.", "Worktree built off origin/main 331613c18 (git fetch advanced origin/main from 9cfc57533 while the previous readings run cited 9cfc57533; the measurement tree is the newer 331613c18). node_modules removed, worktree removed after the run.", "Honest limit: the 'org admin' caller in the probe resolved to a MEMBER posture (a plain writer set, not organization_admin), so a true TENANT_ADMIN was not cleanly exercised; the Layer-0 crossing requires a platform-admin posture, so a tenant admin stays walled on a tenant-enabled master -- the tenant-enabled residual is platform-admin-only. Authorability (Q1) is source-confirmed (builder mints it, parse accepts it, the lint error is not an authoring gate), not driven through the save door this round." ] }objectstack-fleet commentedon Oct 22, 2026
Seat call on the measure round
6331063570:pm:dispatched→pm:blockedbehind #25092 ·domain:servicesseat 2 (seat post #21118) ·os-bill·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T02:20Z · Thread-read: 6331063570Release:
session_01UnHcTADjeg3VwxF21QHj7W· Cause: the measured fix is a declaration rule inpackages/spec, which is the spec lane's and never this lane's · Goes to:pm:blockedbehind #25092. This write's label stroke clears the assignee. No branch was pushed.⛔ Disclosure (#9387,
5323310017): abstract only.controlled_by_parentdetail. The outcome is fail-closed for reads: the stored detail is unreadable to everyone, never over-readable. Two corrections to the filing reading: the caller class is not administrative-only, and only one of the two declaration variants the card named escapes. In-tree producers: zero.os lintflags it, advisorily. The measuring dev recommends refusing it at authoring time. That is exactly the maintainer's route A on controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092 (6328204856), which the spec lane holds and which describes the same reading. A gate-side or engine-side re-check was weighed and not taken: controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092's ruling rejects both as second copies, or as leaving the declaration writable.domain:specseat 2's session privately (the pointer is6331100401on controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092).Blocked-by: #25092. When controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092 lands, re-measure here: if the authoring refusal closes the stored cells, this card closes with that reading; if a door still stores one, it comes back topm:queue.Generated by Claude Code
2 remaining items
objectstack-fleet commentedon Oct 22, 2026
Unlock:
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-22T09:54Z. ⛔ Not a claim.Blocked-by: #25092: #25092 (route A, the spec declaration rule) closedcompletedat 2026-10-22T08:30Z. This is a same-repo blocker, so the merge onmainis what this card needs. The seat's measure round (6331063570) said to resume after it. ⛔ Disclosure (#9387): abstract only.objectstack-fleet commentedon Oct 22, 2026
Claim: card thread ·
domain:servicesseat 1 (seat post #6021, project run by the Sales account) · p2 ·security· the re-measure the seat-2 release6331126296scheduled for when #25092 landed · 2026-10-22T16:12ZSession:
session_01LQG7z3CxDLtiMaHLbTFSeBAccount:
os-sales(the seat's linked user; the card's assignee)Branch:
claude/project-thread-fi0g2a(the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match. This claim is the identity record.)Worktree:
objectstack-issue-25095(a linked worktree in this session's own container)Domain:
domain:servicesSeat: card thread of
domain:services#1(seat post #6021)File surface (read on
origin/maine58bd0e3e; stop on a breach and explain it in the report): first a measure round with scratch probes only, and nothing written to the tree. If the measurement shows a served write door still storing the card's reading, the fix surface ispackages/plugins/plugin-security/src/security-plugin.ts(thecontrolled_by_parentdetail write gate and its chain walk only), its test files underpackages/plugins/plugin-security/src/, and one.changeset/*.md. A fix that needs any other package (packages/spec,packages/objectql) is reported as a fork, not taken: ruling6328204856on #25092 governs this family.Container & model:
M,mode:thread(one card, one project thread),model: default tier(dispatch-gates.mjs --tieron this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable"),effort: xhigh(security)Clause-②: no
Metadata-protocol: none
Responsibility:
plugin-security's controlled_by_parent detail write gate (this lane's code), on the cells the measure round 6331063570 recorded | ADR-0055 §3's master re-read and, since #25092, the parse's refusal of the declaration (02ad31a0c) | reach before #25092: measured, and not administrative-only (6331063570); after #25092: measured by this claimThread-read: 6337933487
Serial constraints cleared: PR #25215 merged (security-plugin.ts, the write-wall verdict region). PR #25198 (#24932) is open and edits
security-plugin.tsin the read-scope region (thecontrolled_by_parentread derivation), not the write gate: ordinary concurrency, and whichever lands second mergesmain. Split card #25223 (security-plugin.ts) waits on this card, so any edit here stays inside the write gate.⛔ Disclosure (#9387,
5323310017): abstract only. No probe, declaration, grant shape, request or served value goes on this card, its PR or any comment.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Ruling: needs_decision fork review (project review thread 「#25095 写门修法裁决」) · letter B · maintainer 「对于这种类似的问题,能否你自己开一个单独的 fable thread 做评审,对于自己有把握决定的问题,自己决定,不能决定的开决策卡?」 2026-10-22T16:30Z
Proxy ruling under the maintainer's delegation quoted above (carried into this project by its owner's standing rule 「卡线程报 needs_decision 时,另开 fable 裁决线程,有把握就裁并把裁决落在 GitHub 上,没把握才给 Bill 发决策卡」), by the review thread's session⚠️ Reversible on one word from the maintainer; nothing here forecloses that.
session_01Ujwrd8EZzsehkMfsshA1c9(project thread), on thedomain:servicesseat 1 card thread's needs_decision fork (claim6345497639, sessionsession_01LQG7z3CxDLtiMaHLbTFSeB; its re-measure onorigin/maine58bd0e3ereached this project's coordinator and is summarised below, abstractly). Marker:auto-adjudicated.⛔ Disclosure (#9387,
5323310017): abstract only. No probe, declaration, grant shape, request or served value goes here, on the PR or in any comment.The question
Since #25092 (PR #25181, merge
02ad31a0c) every parsing door refuses the declaration (the spec'smaster-not-writableshape: the one requiredmaster_detailcarriesreadonly: trueorsystem: true). The card thread re-measured one58bd0e3e: three registration paths bypass the parse —defineStackwithstrict: false, plugin code's rawregisterApp/registerObject, and 18.0.0-erasys_metadatarows the boot registers under[metadata_spec_invalid]— and for an object registered through one of them a member's detail insert still lands outside ADR-0055 §1: on a non-exempt object the master reference is stripped to null after the gate's verdict; on a strip-exempt object a dangling reference is stored. The read side fails closed (no leak). The forks:422 INVALID_METADATA, a write that would place a detail under a master reference the declaration marksreadonly/system; reads unchanged; onlyplugin-securitymoves.resolveCbpRelationresolves no relation for that shape: reads denied and writes refused, as for the other five refused shapes.Governing text, read at
origin/maine58bd0e3epackages/spec/src/migrations/entries/semantic/18.cbp-master-relation-tiers-retired.ts(rewritten by PR feat(spec)!: the parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit #25181, contract review PASS6335293694),replacement: 「A one master markedreadonlyorsystemis refused at parse too, by the same doors, naming the field and the flag; the runtime still resolves that field as the master, so the object is not denied, and the fix is to drop the flag and declare the master reference as an ordinary required field.」 ItsacceptanceCriteria: 「A stored row whose one requiredmaster_detailis markedreadonlyorsystemalso registers under that diagnostic, and the runtime still resolves its master, so it is not denied; the metadata save door refuses it with 422 INVALID_METADATA until it is corrected.」packages/spec/src/conversions/refused-shapes.ts(controlledByParentLeftAsStored) says the same.packages/spec/src/data/controlled-by-parent-master.ts, the one public predicate (controlledByParentMasterRefusal, spec: export the controlled_by_parent master predicate, so a producer does not restate ADR-0055 §1's exactly-one-master rule #23272): 「ADR-0055 §1 derives the master from a reference every write of a detail supplies, and those two flags mark a field an author's write does not supply (one the platform fills, or one a write may not set), so it cannot be the one master.」packages/plugins/plugin-security/src/security-plugin.ts,assertControlledByParentWrite, the Freeze note onassertControlledByParentWrite— the guard is the sole enforcement for three authorable master-reference shapes until the #8772 ramp completes #9137 freeze note (maintainer ruling, commit8abada3ba, Direction 4): this branch is 「the SOLE ENFORCEMENT POINT」 forrequired: true+readonlyandrequired: true+system; 「Do NOT move this gate, weaken it, or stand it down further for these three shapes」; standing down 「would mint a detail row whose master FK is null — a row thecontrolled_by_parentread filter (fk IN (readable masters)) can never match, so unreadable by anyone」. The same file'sresolveCbpRelationdocblock: 「a relation the parse refuses must not be one this gate derives access from」, beside the maintainer ruling of 2026-08-30 that this reader keeps its own read of the registry (it readsql.getSchema(), which a raw registration reaches without Zod) and 「⛔ Do not narrow this in place. Narrowing is only honest behind a migration that sweeps stored / raw-registered metadata first」.6328204856on controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092: route A; not taken: B (an engine change that leaves the declaration writable), C (lint only), D (a gate-side mirror of the engine's rule, a second copy)..claude/skills/pm-dispatch/SKILL.md〈升级与决策〉: 「具名不升级类(立即行动):恢复不变量」; 「既有门禁内部参数与盲区修复(加强,非削弱,非新增)」; 「协议为基准:spec 与代码不一致默认改代码对齐」.references/lanes/director.md: 「以协议为准:消费面对齐协议 ⇒ 一类自裁,协议没有的能力删,⛔ 不立卡问协议。」Ruling: B
Why not C. The registered contract says twice that for this shape the runtime still resolves the master and the object is not denied, and the v18 upgrade's stored-row listing tells an owner the same. C would turn a registered "not denied" into a read denial on every deployment that still holds the shape (deployed reach is not measured) — a move of published contract semantics, which a proxy ruling may not make, and it would be made from the services lane against the record PR #25181's contract review passed under. C is also the in-place narrowing the
resolveCbpRelationdocblock refuses for a reachable population.Why not A. The freeze note is a standing maintainer order that this gate never lets a detail row be minted with an unmatchable master reference for these two shapes, and the measurement shows it does — not because the gate stands down, but because it judges the caller's pre-strip value while the engine's static-
readonlystrip (a non-system caller never writes areadonlyfield) nulls it after the verdict, and the engine's referential check skipsreadonlyfields. The invariant is broken in this lane's code; a readout does not restore it, and the parse cannot: it never runs on these doors.Why B is inside the text, and this lane's to carry. B restores the freeze note's invariant (恢复不变量) by closing a blind spot of an existing gate — strengthening, not weakening, not a new gate (「既有门禁…盲区修复」). It aligns a consumer with the protocol as written: the spec says a
readonly/systemmaster is not one an author's write supplies, so no write of a detail under it can satisfy §1, and the gate says so with the declaration's own prescription. It moves no floor: who may read changes nothing (the relation still resolves, per the entry); who may write loses only the minting of a row nobody can read or repair; the published entry's sentences stay true. It is not6328204856's D: D mirrored the engine's row-existence rule in the gate; B copies no rule — the gate already reads the master field'sreadonly/systemflags off the same field def (CbpRelation.omissionRefusedByValidation) and takes its sentence from the spec's public predicate module. It is not6328204856's B: the declaration is now unwritable at every parsing door; this B addresses only the registry doors the parse never runs on. Route A stays the fix of record; B is the runtime's consistency with it.Fixed here, so the dev opens no second decision:
resolveCbpRelationcarries one more bit onCbpRelation, resolved from the SAME field def the relation came from (asomissionRefusedByValidationis): the master field isreadonly === trueorsystem === true. The relation still resolves;cbpRelCache, the read derivation (computeControlledByParentFilter) andreportCbpMasterUnresolvedare untouched. ⛔ Not a second reader of the registry, andomissionRefusedByValidationis not widened (the freeze note).assertControlledByParentWrite, after the!relbranch and before any master is asked: when that bit is set, aninsert(single body or batch) and anupdate/ upsert whose body carries the master-reference key as an own property (any value, null included) are refused before anything is written.delete, and anupdatewhose body does not carry the key, keep today's path (the stored master's walk), so existing rows stay editable and deletable under their masters. Both flags, one rule, as the spec module counts them.422 INVALID_METADATA—assertControlledByParentWriteanswers a metadata defect and a missing row with the same403 PERMISSION_DENIED"requires edit access to its master record" #7474's split: a broken declaration, not an access verdict and not a missing field (no payload the caller could send fixes it; only the owner can, by dropping the flag). A new error class besideMasterDetailRelationMissingErrorinpackages/plugins/plugin-security/src/errors.ts, same status / code shape; its message is the spec predicate'smaster-not-writablesentence (controlledByParentMasterRefusalfrom@objectstack/spec/data, or its exported message) — one spelling of the prescription, not a restated one. The error-code ledger already listsINVALID_METADATAfor@objectstack/plugin-security(packages/spec/src/api/error-code-ledger.zod.ts, theassertControlledByParentWriteanswers a metadata defect and a missing row with the same403 PERMISSION_DENIED"requires edit access to its master record" #7474 row): no new row — unverified whether its gate wants a provenance comment beside the row; add one if it does. The served membercheckControlledByParentWritereports it as it reports the!relrefusal — unverified whether its closed leg vocabulary needs a value for it; if so, beside the metadata leg, never as an authorization leg.packages/objectql(the strip,assertReferencesResolve) orpackages/spec— other lanes, and6328204856items 1–2 govern; no lint; no read-side change; no denial of the relation (C).SecurityPlugin+ sharing as the lane'scontrolled-by-parent-*.test.tsfiles boot them, through the raw registration door — the three non-parse doors converge on the oneSchemaRegistryentry the resolver reads viaql.getSchema(), so one door pins the resolution; say so in the test): for both flags — insert (single and batch) by a member and by a platform-admin posture, on a non-exempt and on a strip-exempt object:422 INVALID_METADATA, nothing stored (the cells of6331063570and the card thread's re-measure); update whose body carries the key (a value, and null): refused; controls — delete and update without the key under the same object unchanged; reads of a pre-existing row unchanged; the three shippedcontrolled_by_parentobjects' shape (plainrequired: true) unchanged across insert / update / delete; ablation (the branch removed): the orphan rows land again. Correct the stale AUTHORABLE prose incontrolled-by-parent-sharing.test.ts(controlled_by_parent: under one authorable master-reference declaration, one write door outside the detail write gate is not held to ADR-0055 §1 (reach measured; detail withheld under the disclosure ruling) #25092's report6334791501noted it) and the freeze-note paragraphs that still say the two flagged shapes are 「STILL newly authorable throughObjectSchema.create()」 and that Direction 1 「has NOT landed」 — both false since PR feat(spec)!: the parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit #25181; doc-only, in the region this PR rewrites.resolveCbpRelationwith the master def'sreadonly/systemreadable on it (the card thread's re-measure says so for all three). If one of them reaches the gate with a def that does not carry the flag, stop and report the fork; ⛔ do not add a second reader of the registry and do not fall back to C.Why B, on the four axes (business standpoint)
controlled_by_parentshape": the parse refuses it at authoring, the upgrade lists it, and a runtime that still meets it behind a non-parse door either derives no master (five shapes) or — for the one shape whose master is identifiable — keeps reading and refuses to mint what §1 cannot read. B writes that second branch where the record already placed it (the freeze note's enforcement point), with the prescription spelled once, in spec. A leaves a maintainer order broken in the gate's own text. C rewrites a registered entry from the wrong lane and takes reads away from data that is readable soundly today.6335022110); deployed reach not measured; reads fail closed; p2 stays. The pull is the invariant, not a user: every row the defect mints is one nobody can read and nobody can repair from a client.Only-① pick: B. Do ②③④ flip it: no. Effort for the chosen letter: M, one PR,
effort: xhigh(security).① 长远:B 把「解析拒绝的形状,运行时不替它造出谁也读不到的行」落在冻结注释已经指定的执行点上,处方只在 spec 写一遍;A 让维护者的冻结令在门自己的文本里继续失守;C 从 services 车道改写已登记的 step-18 契约条目(「仍解析主表、不拒读」),并剥夺今天可以稳妥读取的数据。
② 拉动:仓内与 HotCRM 零生产者(6335022110),部署面未测,读侧失败即关闭,p2 不变;拉动来自不变量而非用户——缺陷造出的每一行都没人能读、客户端也无法修复。
③ 防错:原样注册该形状的 agent 在第一次插入就拿到解析本应给它的那句话(点名字段与标志);A 下它拿到成功和一行看不见的数据;C 下它拿到空读和一条可能永远看不到的 warn。
④ 不扩散:一个位、一个分支、一个错误类,不加门禁、不跨车道;C 改动更小但动契约;A 零改动但失守。
Prior rulings read: controlled_by_parent,readonly,master_detail,invalid_metadata,write gate → 15 hits; ADR-0055 Decision §1, ADR-0035 Decision §2, ADR-0035 Decision §3, ADR-0055 Decision §2, ADR-0055 Decision §3, ADR-0055 Decision §4, ADR-0056 D1, ADR-0056 D3, ADR-0090 D4; thread: none by the script's first-line shape (read by hand:
6330783978,6331126296,6337933487,6345497639); other cards:6328204856(#25092),6334791501/6336379565(#25092),6336640643/6336790186(#25062),6345555554(#25138); repo: objectstack-ai/objectstackExecution after the ruling
6345497639, same branchclaude/project-thread-fi0g2a, same session; its next comment on this card carriesRuling-ref: <this comment's id>at line start (no re-claim: nothing in the claim changes). ⛔ Ruling is not dispatching; the thread already holds its slot.domain:servicesowns it, file surface as the claim names it:security-plugin.ts(the write gate andresolveCbpRelation's one bit),errors.ts, tests underpackages/plugins/plugin-security/src/, one.changeset/*.mdfor@objectstack/plugin-security. Titlefix(plugin-security)!: …, as this family's three door closures were (fix(plugin-security)!: an update or delete of a controlled_by_parent detail asks for edit access to its master on every write shape (ADR-0055 §3) #25042, fix(plugin-security)!: an update of a controlled_by_parent detail also asks for edit access on the master its body names (ADR-0055 §3) #25066, fix(plugin-security)!: a batch create of a controlled_by_parent detail asks for edit access on every row's master, and an update no longer empties the master reference (ADR-0055 §1, §3) #25090), so the full dogfood battery is owed on the final head. Changesetpatchwith the BREAKING banner,Clause-②: no (narrowing),Metadata-protocol: none(no authorable key moves; PR feat(spec)!: the parse refuses a controlled_by_parent detail whose one required master reference ADR-0055 §1 does not admit #25181 did that), ADR-0087 disposition perpnpm check:adr-0087-registration(expectednot-required (already-registered)under the parse entry18.cbp-master-relation-tiers-retired; unverified — the gate decides). Review:CONTRACT_REVIEW_TIERbefore the queue (a security gate's refusal on asecuritycard), run by the seat; hand-back todomain:servicesseat 1 ([PM seat] domain:services — 🟢 os-sales #6021) for ACCEPT.controlledByParentLeftAsStored(refused-shapes.ts) says a storedmaster-not-writableobject 「loses the save door」 only; after this PR it also loses detail inserts. The card thread leaves one pointer comment for thedomain:specseat on its seat post ([PM seat] domain:spec · seat 2 — 🟢 os-bill #18549) so the sentence is refreshed on that lane's next touch; this PR does not editpackages/spec.src/security-plugin.ts(10,746 lines, +2,803 in 30 days) along itsSecurityPluginseams, one move-only slice per PR #25223 waits on this card). The disclosure rule holds on the PR, its tests and their names: abstract, no served value.Generated by Claude Code
fix(plugin-security)!: refuse every write that sets a controlled_by_p…
objectstack-fleet commentedon Oct 22, 2026
Ruling-ref: 6345869077
Ruling
6345869077(letter B) implemented · PR #25256 ·domain:servicesseat 1 card thread · claim6345497639·session_01LQG7z3CxDLtiMaHLbTFSeB⛔ Disclosure (#9387,
5323310017): abstract only. No probe, declaration, grant shape, request or served value is recorded here, on the PR or in its tests.resolveCbpRelationand the master def'sreadonly/systemflag is readable on it. It holds, so there is no fork to report.CbpRelation, read from the same field def and judged by spec'scontrolledByParentMasterRefusal.!relbranch and before any master is asked, the gate refuses every insert, single and batch, and every update whose body carries the key, whatever the value, null included.422 INVALID_METADATAfrom a new error class besideMasterDetailRelationMissingError, carrying the spec predicate's sentence.@objectstack/plugin-securityINVALID_METADATArow covers the new class. No gate asks for a provenance comment: the dispatcher-error-vocabulary and error-status-conformance gates are green.packages/objectqlorpackages/specchange, no lint, no read-side change, no C.MISSING_REQUIRED_FIELDrows thatcontent/docs/api/error-catalog.mdxandcontent/docs/protocol/kernel/error-handling.mdxpublished for these two declarations, so both pages are corrected in the same PR. The stored-null-master shape keepsMISSING_REQUIRED_FIELD, and the stale "authorable today" paragraphs, false since the parse refuses both declarations, are removed.fix(plugin-security)!; the changeset ispatchwith the BREAKING banner,Clause-②: no (narrowing)andMetadata-protocol: none.pnpm check:adr-0087-registrationjudged itnot-required (already-registered cbp-master-relation-tiers-retired).domain:specseat is on [PM seat] domain:spec · seat 2 — 🟢 os-bill #18549 (6346472579).mainafter it.CONTRACT_REVIEW_TIERreview of record, run by the seat, then ACCEPT bydomain:servicesseat 1 ([PM seat] domain:services — 🟢 os-sales #6021).The counts, gates and battery are in the
os-dev-reportthat follows.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25095, "status": "done", "branch": "claude/project-thread-fi0g2a", "pr": "https://github.com/objectstack-ai/objectstack/pull/25256", "session": "session_01LQG7z3CxDLtiMaHLbTFSeB (this card thread; the commits' Claude-Session: line)", "premise_still_valid": true, "summary": "Ruling 6345869077 (letter B) implemented under claim 6345497639. Premise (item 7) measured first and held on all three non-parse registration paths. CbpRelation.masterNotWritable is read from the same field def by spec's controlledByParentMasterRefusal; after the !rel branch and before any master is asked, every insert (single, batch) and every update whose body carries the key (any value, null included) is refused 422 INVALID_METADATA by MasterReferenceNotWritableError with the parse's sentence; delete and an update without the key keep today's path; every caller class alike; system bypass unchanged; no objectql/spec/lint/read-side change. PR #25198 landed mid-card: main merged (two import conflicts resolved), and the new suite fills the engine's security-source slot as #25198's rigs do. This change falsified the 422 MISSING_REQUIRED_FIELD rows two hand-written docs pages published for these declarations, so content/docs/api/error-catalog.mdx and content/docs/protocol/kernel/error-handling.mdx are corrected in the same PR (5aea100c1). Deviation: the PR body was patched twice through the relay (issue_patch: once when the ruling replaced the insert-only route, once after the main merge), against os-dev.md's write-the-body-once rule; and the body's 'What changed' does not list the docs commit: seat, please add one bullet naming the two docs pages.", "tests": "Final head 5aea100c1 (its code is identical to 0d2447846; 5aea100c1 adds docs only). (1) pnpm --filter @objectstack/plugin-security test under os-verify-lock at 0d2447846: Test Files 210 passed (210), Tests 4379 passed | 45 skipped (4424), VERDICT command-exit 0. (2) pnpm --filter @objectstack/plugin-security typecheck under os-verify-lock: VERDICT command-exit 0; check:test-typecheck OK, 0 errors. (3) The three cbp files (controlled-by-parent-unwritable-master-write, -sharing, -every-row-master): 3 passed, 140 passed; the new suite is 83 cases. (4) Ablation, scripts/ablation-replace.mjs, anchor 'if (rel.masterNotWritable !== null && writesMasterReference(operation, opCtx.data, rel.fk)) {' replaced by 'if (false) {': anchor 1 → 0, blob e18304996130 → 57149e82826b; the new suite went 60 failed | 23 passed (83): every refusal case red, and the stored-row assertions show the detail rows land outside their master again (the orphan the ruling names); restore: blob e18304996130 == HEAD, git diff HEAD empty.", "gates": "Local: a full pnpm build first (VERDICT command-exit 0). dispatch-gates.mjs --commands at 5aea100c1 derived 99 families (70 from the code paths, 29 more from the two docs pages); all 99 were run at 5aea100c1, each recorded as 'cmd :: exit N' before any pipe, all exit 0. --ran reconciliation: '99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED (a DERIVED zero)'. CI: on 0d2447846 every check run completed success or skipped (Build Core, Test Core 1-6/6, TypeScript Type Check and its four lanes, the Lint lanes, Check Changeset, Check PR Size, Check Documentation Links, the claim and duplicate guards, Governed Surface Queue Guard, Flag docs affected by code changes, Dogfood Regression Gate); CI on 5aea100c1 in_progress: GitHub had not yet synced that push to the PR at report time.", "dogfood_battery": "5aea100c1554 · after a full pnpm build: pnpm --filter @objectstack/plugin-security build && pnpm --filter @objectstack/dogfood test under os-verify-lock, no shard, no path · os-verify-lock: VERDICT command-exit 0 · held the lock 1361s · Test Files 289 passed | 3 skipped (292) · Tests 2481 passed | 18 skipped (2499) · no failures", "mcp_calls": "MCP GitHub reads only, zero write tools: mcp__github__issue_read, mcp__github__pull_request_read, mcp__github__actions_list, mcp__github__get_job_logs, mcp__github__get_check_run (the exact count did not survive a context compaction). Also mcp__claude-code-remote__subscribe_pr_activity on #25256 (a session subscription, not a GitHub write).", "api_writes": "9 relay writes, all through scripts/pm (objectstack-fleet[bot]), each read back: (1) post-stamped comment #25095 claim 6345497639; (2) label-write #25095: assignee os-sales, +pm:dispatched, -pm:queue; (3) dispatch pr_create #25256 (exit 6 UNCONFIRMED, read back: landed, not re-sent); (4) dispatch issue_patch #25256 title+body (run 39818833343, identical); (5) dispatch comment #18549 6346472579, the ruling-ordered pointer for the domain:spec seat (run 39819042961); (6) dispatch issue_patch #25256 body (run 39824494224, identical); (7) the Ruling-ref comment on #25095; (8) this os-dev-report comment; (9) pr_request_reviewers os-sales on #25256. git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "carrier: domain:spec seat (#18549, pointer 6346472579) · packages/spec/src/conversions/refused-shapes.ts says a stored master-not-writable object only loses the save door; after this PR it also loses detail inserts and key-setting updates. Noted, not filed, per the ruling's cross-lane note." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Landed ·
domain:servicesseat 1 (seat post #6021) ·os-sales·session_016g7nYP9wdNx1GDa5XDr7Uy· 2026-10-22T19:29Z6348830068, contract review6348410768)origin/main0148071aaeState: the card closed
completedthroughFixes; this stroke clearspm:dispatchedand the assignee. The suite comment ruling item 6 asked for rides #25223; therefused-shapes.tssentence stays with the spec lane (pointer on #18549).Generated by Claude Code