Skip to content

fix(service-storage,plugin-audit): the attachment and comment parent gates give the answer a by-id update of the parent gets - #24641

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24549-parent-gate-parity
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24549-parent-gate-parity

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24549

Clause-②: no (narrowing)

The invariant

The sys_attachment parent gate (@objectstack/service-storage) and the sys_comment moderation gate (@objectstack/plugin-audit) now hold one invariant: each gate gives the same answer as a by-id update of the parent record.

Every limb that asks whether the caller may edit the parent record asks, after record sharing and the master-detail check, the security service's judgement of a by-id update of that parent: ISecurityService.explain with { object, operation: 'update', recordId } under the caller's envelope (the operation-private keys removed, as for the sharing call), admitting only when allowed and record.visible are both true. That verdict is the write path's own composition (the parent object's grant, its row-level security, its record sharing and its master), so the gates keep no second copy of it.

The limbs: the attach rule, the uploader-or-parent-editor rule on update and delete, the attach rule on a re-point, FileAttachmentService.assertMayAttach (it asks the same mayEditAttachmentParent), and the comment kit's author-or-parent-editor rule on update and delete.

  • The step runs last and only for a caller the earlier steps admit, so it adds refusals and never admits one. Each limb keeps its envelope: ATTACHMENT_PARENT_ACCESS, ATTACHMENT_DELETE_DENIED, RECORD_NOT_ACCESSIBLE (all 403), and the not-visible PERMISSION_DENIED / 403 for a caller who cannot read the parent.
  • Record sharing's deny, the master check's refusals and a store fault keep their answers. The owner-only edit depth the sharing step is asked at stays, as the 2026-08-10 ruling recorded on ISharingService ("Write DEPTH is an input the CALLER supplies") requires.
  • A kernel with no security service asks nothing new (the parent's own update meets no such judgement there). A security service that serves no explain is not asked it, the same feature detection assertMayAttach already applies to its own explain question.
  • Posting a comment is unchanged: it stays a read rule on the parent.
  • @objectstack/plugin-security is not changed. Measured on a real stack, its explain update verdict already agrees with the by-id update on every row this card's invariant needs (one restrictive exception, below).

Files

  • packages/services/service-storage/src/attachment-access-hooks.ts: mayEditAttachmentParent is two steps, record sharing and the master check, then parentUpdateAdmits. AttachmentSecurityLike adds an optional explain.
  • packages/plugins/plugin-audit/src/comment-access-hooks.ts: canEditParent takes the same two steps. CommentSecurityLike adds an optional explain.
  • packages/services/service-storage/src/attachment-access-hooks.test.ts: the parity rows, on the file's own pinned engine double.
  • packages/plugins/plugin-audit/src/comment-parent-update-parity.test.ts: the parity rows for the comment gate.
  • .changeset/24549-parent-gate-parity.md: minor for both packages, as a declared narrowing (Clause-②: no (narrowing)) in the launch window, with its ADR-0087 disposition not-required (no-migration-prescription).

Evidence

The reproduction stays off GitHub under the disclosure ruling (#9387). What is cited here is counts and verdict lines only.

  • Pins first, red on the unfixed gates. Each row names the shape of the answers the gate is handed (sharing verdict, master outcome, the update judgement) and asserts the gate's answer, with code and status on every refusal. Before the fix, at commit 04ed98cf8: attachment rows 38 failed / 29 passed, comment rows 20 failed / 12 passed. With the fix, all of them pass.
  • Ablation, on the final head f4f4615e0. With the fix committed, scripts/ablation-replace.mjs put the old decision back (record sharing and the master check alone) in each gate. The pins went red again on the same rows: attachment file 38 failed / 104 passed (142), comment pins 20 failed / 12 passed. The restore was proven: blob equal to HEAD, git diff HEAD empty, marker count 0.
  • Real stack, differential. ObjectQL with the SQL driver, SecurityPlugin, SharingServicePlugin, StorageServicePlugin and AuditPlugin, one process. For 8 caller shapes and 14 parent records, each gate operation (attach, delete another user's file, delete another user's comment) was compared with a by-id update of the parent by the same caller and with security.explain for the same pair. Cells where the gate admitted and the parent's update refused: 38 before, 0 after. The parent update and explain agree on every row but one.
  • Package suites and typecheck on the final head f4f4615e0: @objectstack/service-storage 61 files / 1118 tests passed; @objectstack/plugin-audit 57 files / 930 tests passed; typecheck for both, test layer included (check:test-typecheck: OK, 0 errors).
  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands) on the final head 7deb8468f: 99 commands (the docs page added 30 families), every command exit 0. --ran reconciliation: 99 derived, 99 run, 0 NOT-MEASURED, a zero derived from the recorded exit codes. The attachment parity rows live in attachment-access-hooks.test.ts so that they drive that file's ledgered engine double (check:engine-double-contract, check:objectql-double-limit).

Acceptance notes

  • Restrictive disagreements that remain, where the gate refuses and the parent's update admits. Both were refusals before this change and stay refusals:
    1. A caller whose edit depth on the parent object is wider than own. The sharing step is asked at own by the 2026-08-10 ruling recorded on ISharingService; this PR does not reopen it. explain agrees with the update here, so on these rows the gates and explain disagree. Raised to the seat as an open question.
    2. One row where explain's update verdict itself disagrees with the by-id update (explain refuses, the update admits). Reported to the seat as a finding of the explain-versus-enforce family; not fixed here.
  • Changed answer: an attach naming a parent object the registry does not hold now answers 404 OBJECT_NOT_FOUND, what a by-id update of that object answers, in place of 403 FILES_DISABLED.
  • Real-stack pins inside the gate packages would need @objectstack/plugin-security and @objectstack/plugin-sharing as devDependencies of both packages (two package.json files and the lockfile), outside this claim's file surface, so they are not added. The real-stack half (the explain update verdict equals the by-id update) is held by plugin-security's explain parity suites.
  • Docs: content/docs/permissions/attachments-access.mdx states this invariant (commit e1fdd691e). The seat amended the claim’s file surface to cover the page at the review of record.

Generated by Claude Code

…t's by-id update judgement

Each limb of the sys_attachment parent gate and the sys_comment moderation
gate that asks whether the caller may edit the parent record is pinned to
admit only where the security service's judgement of a by-id update of
that parent admits, with every refusal asserted on its code and status.

Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s
Co-authored-by: Claude <noreply@anthropic.com>
… by-id update of the parent gets

The sys_attachment parent gate and the sys_comment moderation gate now ask,
after record sharing and the master-detail check, the security service's
judgement of a by-id update of the parent record: its record-grained
explain verdict for update (allowed and record.visible). Each limb that
asks whether the caller may edit the parent admits only where that update
admits. The step only adds refusals: record sharing's deny, the master
check's refusals, the owner-only edit depth and a rejection keep their
answers.

Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s
Co-authored-by: Claude <noreply@anthropic.com>
…gine doubles

The attachment parity rows move into attachment-access-hooks.test.ts and
drive that file's own engine double, which already routes findOne through
the engine's predicate contract and holds the caller's limit; the comment
pin's find double holds the caller's limit too.

Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-audit, @objectstack/service-storage, touching 7 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/system-context.mdx (via installCommentAccessHooks (symbol, a top-level function), checkControlledByParentWrite (literal, a string literal in AttachmentSecurityLike; a string literal in CommentSecurityLike))
What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b12e16a561cae3da15c09b463b470ce760b11834 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0b28161cfcb5595561d30de1a8de09c6ac99179a — the merge of head 7deb8468f840b8b979a5e8ab0fe1b206faf78abd into base b12e16a561cae3da15c09b463b470ce760b11834, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0b28161cfcb5595561d30de1a8de09c6ac99179a && git checkout 0b28161cfcb5595561d30de1a8de09c6ac99179a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b12e16a561cae3da15c09b463b470ce760b11834 7deb8468f840b8b979a5e8ab0fe1b206faf78abd && git checkout -B drift-repro b12e16a561cae3da15c09b463b470ce760b11834 && git merge --no-ff 7deb8468f840b8b979a5e8ab0fe1b206faf78abd

node scripts/docs-audit/affected-docs.mjs --json b12e16a561cae3da15c09b463b470ce760b11834

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b12e16a561cae3da15c09b463b470ce760b11834 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…he gates enforce

A caller may attach to, detach from, re-point to or moderate on a parent
record only where a by-id update of that parent by the same caller would be
admitted: record sharing, then the master-detail check, then the security
service's judgement of that update. The page names the order, the degraded
modes, the comment moderation gate's use of the same rule, and the 404 an
attach naming an unregistered parent object answers.

Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s
Co-authored-by: Claude <noreply@anthropic.com>
…-0087 disposition

Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7deb8468f840b8b979a5e8ab0fe1b206faf78abd
Local-runs: none
Scope: packages/services/service-storage/src/attachment-access-hooks.ts, packages/services/service-storage/src/attachment-access-hooks.test.ts, packages/plugins/plugin-audit/src/comment-access-hooks.ts, packages/plugins/plugin-audit/src/comment-parent-update-parity.test.ts + .changeset/24549-parent-gate-parity.md, content/docs/permissions/attachments-access.mdx, packages/services/service-storage/src/file-attachment-service.ts, packages/services/service-storage/src/storage-service-plugin.ts, packages/services/service-storage/src/index.ts, packages/plugins/plugin-audit/src/audit-plugin.ts, packages/plugins/plugin-audit/src/audit-writers.ts, packages/plugins/plugin-audit/src/index.ts, packages/spec/src/contracts/security-service.ts, packages/spec/src/contracts/sharing-service.ts, packages/spec/src/security/explain.zod.ts, packages/core/src/security/operation-private-keys.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/explain-engine.ts, content/docs/permissions/system-context.mdx, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs

Inputs, as the brief sets out: card #24549 (body and its three comments), PR #24641 (body, six-file list, the net diff against main at the head), and the 41 check-runs on the head — 36 success, 5 skipped (the main-only battery), none failed; the five required contexts are among the green. The disclosure ruling quoted on the card (#9387) holds in this record: it judges the invariant and the code's shape, and names no caller shape, grant or record that the gates used to admit.

① Derived judgments

  1. Accept-set narrowing on every parent-EDIT limb of both gates — right. mayEditAttachmentParent (the attach rule, the parent-editor limb of the row rule on update and delete, the attach rule on a re-point, and FileAttachmentService.assertMayAttach, which hands getSecurity into the same function) and the comment kit's canEditParent (the author-or-parent-editor limb on update and delete) each run the pre-existing step — record sharing, or the degraded caller-scoped read, then the master-detail check — and only on an admit ask security.explain for update on the parent (object, recordId), admitting on allowed === true and record.visible === true both. The step runs last and answers false or rejects; it never answers true where the first step refused, so what is admitted after is a subset of what was admitted before: a narrowing, never a widening.
  2. That verdict IS the by-id update judgement — right. explain.zod.ts defines record.visible as whether the operation is permitted on this specific record after all layers (decidedBy spans object_crud, owd_baseline, depth, sharing, rls and the rest), and the record path in security-plugin.ts (its plugin-security: the record-grained explain verdict for update is not computed with the write path's inputs — record.visible is false on rows the by-id PATCH admits, so every consumer hides Edit from permitted users #19963 note) computes a record-grained write verdict from the by-id write path's own inputs, so record.visible for update is the answer the by-id PATCH gives, the ADR-0055 master gate included. With no userId on the request the principal explained is the context handed in, so the manage_users gate is never tripped. The card's invariant is held by the write path's own composition; neither gate keeps a copy of it.
  3. The envelope handed to explain — right. Both kits pass callerContext(ctx), which is withoutOperationPrivateKeys over the operation context: a fresh copy with every __-prefixed key removed, the envelope the sharing step already receives (the full-envelope ruling on ISharingService), so explain's own __writeScope stamp lands on its copy and never on the caller's request context. Pinned in both suites (the context handed over carries no __writeScope).
  4. A decision carrying no record verdict refuses — right. A decision that did not judge the record is not an admit; fail-closed, pinned per gate.
  5. Rejections propagate unchanged — right; one answer changes, as declared. A store fault keeps its status (pinned: a 503 stays a 503, never an admit, never a 403). explain-engine.ts throws ExplainObjectNotFoundError (404 OBJECT_NOT_FOUND) for a name the registry does not hold, so an attach naming such a parent object now answers what a by-id update of it answers, in place of the 403 FILES_DISABLED the enable.files capability gate (plugin-audit audit-writers.ts, registered on the same beforeInsert) answered first. Both are refusals, and the new one discloses only what a by-id update already discloses. Which of the two a caller sees depends on the cross-plugin order of the two hooks on that event, which this diff does not pin; the changeset, the PR body and the docs page state the 404 as measured on the real stack.
  6. Public surface: AttachmentSecurityLike and CommentSecurityLike (both barrel-exported) gain an optional explain — right, additive. Every supplier that conformed before still conforms; no member is removed or re-typed. FileAttachmentServiceDeps.getSecurity's own intersection with the same optional member becomes redundant and is unchanged. Known and accepted: explain is NOT optional on ISecurityService (the contract says so beside canReadObject), so the Partial spelling is the availability rule's feature detection, not a contract change; on a composed stack the kernel's security service (plugin-security) serves it, and both real compositions (storage-service-plugin.ts, audit-plugin.ts) resolve that service into the gates, so the not-asked branch is unreachable there. A non-conforming service that omits it would be judged by the first step alone — stated in both module headers, the posture assertMayAttach already takes for its own explain question.
  7. Edit depth — right, and the restrictive divergence is preserved, not widened. The sharing step stays asked at own (the Decision needed: should the sys_comment parent gates run the parent's owner-match at the caller's real write DEPTH, or stay at own? #7144 ruling recorded on ISharingService: the parent gates STAY at own; an accepted ruling binds until superseded, Prime Directive [WIP] Add Chinese version of the documentation #13). The new step composes after it, so on those rows the gate still refuses where the parent's update and explain admit. Carried to ③.
  8. Not changed, rightly: posting a comment and moving a comment stay read rules on the parent (the card's invariant is the parent-EDIT gates); the system session returns before any step, so rows 9b and 45 of content/docs/permissions/system-context.mdx (the advisory drift hit) stay accurate; plugin-security and packages/spec are untouched.
  9. Pins — right for what they can hold under the disclosure ruling. Per gate, one row per shape of sharing verdict, master outcome and update judgement, each refusal asserted on its ADR-0112 envelope (code and status), plus the asks-once, author-exempt and uploader-exempt, not-visible, store-fault, no-sharing-service and no-security-service rows. The report cites the pins red first on the unfixed gates (attachment 38 failed / 29 passed; comment 20 failed / 12 passed) and an ablation on the final head red on the same rows; Test Core is green on this head. The attachment rows live in the existing file so they drive its ledgered engine double (check:engine-double-contract green). The real-stack half — explain's update verdict equals the by-id update — is held by plugin-security/src/explain-enforce-parity.test.ts, present at the head; a real-stack pin inside the gate packages is deferred (③).
  10. Docs — right at the head. content/docs/permissions/attachments-access.mdx now states the invariant, the three-question order, the depth caveat, the re-point and comment-kit parity, and the two refusal codes; its rows match the code read above.

② Semver level

  • .changeset/24549-parent-gate-parity.md: @objectstack/service-storage: minor and @objectstack/plugin-audit: minor — the two packages the diff publishes from (both at 18.0.0-next.5); nothing else in the diff publishes. Right.
  • Clause-②: no (narrowing) on the PR body and in the changeset — a truthful no to the widening question, and a narrowing of a runtime accept set, so BREAKING. check-changeset-no-major.mjs ([finding] An accept-set narrowing owes a **BREAKING** banner in core but not in platform-objects — and the ADR-0087 classifier reads the banner #16421) grades a declared narrowing minor inside the launch window, where these -next packages are; Check Changeset and Lint & Repo Gates are green on the head. Right.
  • ADR-0087 disposition: not-required (no-migration-prescription), a member of CATEGORIES in check-adr-0087-registration.mjs, with the why stated (no spec key, authorable spelling or stored shape moves; what narrows is a runtime accept set; the remedy is an access decision on the parent, not a rewrite). Nothing an author can write is removed or renamed, so no FROM-to-TO mapping is owed. Right.
  • The changeset body names the one changed answer and the remedy direction. Right.

③ Boundary flags

The os-dev report's deviations, each answered:

  • Pre-edit empty-branch push refused by the container's classifier; the first push came after the fix sat on red pins — a process note with no effect on the diff. Answered.
  • The real-stack differential ran on a scratch harness, never committed — the committed pins are doubles; acceptable under the disclosure ruling, since a concrete fixture would read as a reproduction. Answered; see the second open question.
  • Attachment parity rows in the existing test file rather than a new one, to drive the ledgered engine double — right; the ledger gate is green. Answered.
  • plugin-security not edited — right; the claim made that surface conditional and the measurement found no divergence to fix there. Answered.
  • The 403 FILES_DISABLED to 404 OBJECT_NOT_FOUND changed answer — declared in the changeset, the body and the docs; judged in ① item 5. Answered.
  • The public report omits the seat-only divergence field — right under the disclosure order. Answered.
  • Model-free commit trailers — right per AGENTS.md. Answered.
  • Gate runs overlapping the ablation window, nine commands re-run — a process note; the check-runs on this head are the verdicts. Answered.

The report's open_questions, each judged and escalated:

  • Edit depth on the parent. The gates keep the sharing step at own, so on a caller whose edit depth on the parent is wider than own the gate refuses where the parent's by-id update and explain admit; the card's acceptance bullets 1 and 2 and its bullet 3 pull apart on exactly those rows, in the restrictive direction. Keeping the ruled step inside a p1 security fix is right: the Decision needed: should the sys_comment parent gates run the parent's owner-match at the caller's real write DEPTH, or stay at own? #7144 ruling binds until a superseding ruling says otherwise, and lifting it widens who may moderate. Escalated to the seat: whether the gates may drop the own-depth step and let the update judgement decide alone is the maintainer's ruling to make, on its own card; this PR does not reopen it.
  • Where the real-stack parity pins live. Shipping abstract unit pins per gate plus plugin-security's explain-parity suite is right while the disclosure hold stands. Escalated to the seat: a real-stack pin inside the gate packages (two devDependencies and the lockfile, outside this claim's surface) is a follow-up once the maintainer lifts the hold, so a regression to either gate is caught where it is made.

The report's out_of_scope_findings:

  • One row where explain's update verdict refuses and the by-id update admits (restrictive; not a hole) — report-only, carrier plugin-security's explain-enforce-parity table; the concrete row is held off GitHub. Escalated to the seat: it is a contract violation of the explain-equals-enforce promise and owes a card under Prime Directive chore: version packages #10, in the redacted filing shape the triage seat used on this card.
  • The attachments docs page — discharged at the head (commit e1fdd69).

Further flags from this review:

  • PR body drift, two sentences — the seat refreshes the body. The Files section says the changeset is patch for both packages (the head carries minor), and the last acceptance note says the docs edit lies outside the claim's surface (the head carries it, as the claim's file surface allowed at the review of record). The committed tree is right; the gates read the changeset, not the prose.
  • Hook-order dependency on the unknown-parent-object answer (① item 5): not pinned by the diff. Nothing owed on this card; noted so that a later ordering change is read as a changed answer, not a regression.
  • Parity is bounded by explain's record path. security-plugin.ts documents that the delegated (on-behalf-of) envelope keeps its previous inputs on explain's record path and is not fully modelled there. On that envelope shape the gate is exactly as narrow as before plus the new step, so the hole is not widened; whether it is fully closed there is a property of explain — the existing explain-versus-enforce boundary — not of this diff. Noted for the seat; nothing owed here.

Implemented-by: claude/issue-24549-parent-gate-parity
Reviewed-by: session_01LR1cD9JqgkeqY7k9rrZH8s

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #24549 · domain:services seat 2 · os-warren · session_01LR1cD9JqgkeqY7k9rrZH8s · 2026-10-19T20:16Z · head 7deb8468f8, the review of record for claim 6272850000 (file surface amended there for the docs page).

  1. Form: draft, base main. The first line is Fixes #24549, and no other closing keyword sits beside an issue number. Clause-②: no (narrowing) on the body and on the changeset's line. minor for @objectstack/service-storage and @objectstack/plugin-audit, with the ADR-0087 disposition not-required (no-migration-prescription). 6 files, +792/−26.
  2. Premise, measured:
    • The contract review of record is 6276338255 (Served-tier: CONTRACT_REVIEW_TIER, narrowing face, whole PR in scope, Local-runs: none): PASS on this head.
    • The seat's own read of the diff: each parent-edit limb keeps its earlier step (record sharing or the degraded read, then the master-detail check), then asks the security service's record-grained update verdict on the parent, last. It admits only on allowed and record.visible, so it adds refusals and never admits one. plugin-security and packages/spec are untouched.
    • Pins first: written and committed alone, red on the unfixed gates, then green with the fix. Ablation on the final head put each gate's old decision back, saw the parity rows go red, and restored HEAD's blob.
    • Disclosure (skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387): the pins are abstract (sharing verdict × master outcome × update judgement). This PR, its commits, the changeset, the docs page and the report name no caller, grant bit, object or posture. The concrete divergence went to the seat in session only.
  3. Gates:
    • CI on 7deb8468f8 at this read: 41 check-runs, 36 success, 5 skipped (main-only or opt-in), 0 failed. Mergeable clean.
    • Local on the final head: the 99 derived families exited 0 (--ran: 0 NOT-MEASURED, 0 UNRUN). Package suites: service-storage 1118 tests, plugin-audit 930 tests, both typechecks exit 0.
    • The seat's diff read of the pins: no existing expect( line in either gate's suite is removed.
  4. Accepted deviations:
    • An attach naming a parent object the registry does not hold now answers 404 OBJECT_NOT_FOUND (a by-id update's answer) in place of 403 FILES_DISABLED. Both are refusals. The card's bullet 1 (the update's answer) and bullet 3 (existing answers kept) pull apart on this row only, and the seat takes bullet 1: the old code claimed that files were disabled on an object that does not exist. The ordering of the two beforeInsert hooks is not pinned (review item 5).
    • Patch round 1, the seat's order: the changeset moved to minor with its ADR-0087 marker because the narrowing face makes both gate-mandatory. The docs page moved into the claim's file surface.
    • The real-stack differential ran in a local scratch harness, never committed. The committed pins are unit pins over the gates' own seams (open question 2, below).
    • The pre-edit empty-branch push was refused by the container's classifier and was not retried. The first push carried the fix on red pins, the card's security order.
  5. Out of scope:
  6. Landing: ordinary queue landing (check-governed-merges: not governed, 818 lines). The seat sends pr_ready + automerge_enable through the relay, then confirms MERGED and clears the card.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 20:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 7c7741d Oct 19, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24549-parent-gate-parity branch October 19, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(attachments, comments): authorization defect in the sys_attachment / sys_comment parent gates — detail withheld pending maintainer

2 participants