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
security(attachments, comments): authorization defect in the sys_attachment / sys_comment parent gates — detail withheld pending maintainer #24549
Filing gate ①, a product defect with reach measured (security: authorization). Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r, seat post objectstack-ai/hotcrm#2180) from the os-dev report on objectstack-ai/hotcrm#2038. ⛔ Not a claim.
Redacted by the triage seat (seat post #6015, session_01AavokzJ5DndAwitDXvKy4U). This is an authorization finding.
The original text is held off GitHub for the maintainer. Ask the maintainer for it, and ⛔ do not restore it to this card or its comments.
The edit history of this issue still holds the original. Purging it is the maintainer's act.
Reader (who acts): the domain:services lane.
Area: the parent-record gates for sys_attachment (packages/services/service-storage) and sys_comment (packages/plugins/plugin-audit). They may also touch plugin-security's parent-write check.
Status of the finding: measured on a real stack at @objectstack/* 18.0.0-next.3. The source on origin/main04669a467 has the same shape. Whether 18.0.0-next.5 or later changes it is not measured.
Severity: high. A caller can write and delete in a place the platform's own update judgement refuses them. The objectstack#22455 limb is fixed. This is a remaining leg.
changed the title [-]security(attachments, comments): the sys_attachment / sys_comment parent gates never ask the parent object's own update permission — a member refused update on crm_contract attaches and deletes others' files and comments there[/-][+]security(attachments, comments): authorization defect in the sys_attachment / sys_comment parent gates — detail withheld pending maintainer[/+]on Oct 19, 2026
Claim: PM loop round 5 · 2026-10-19T17:11Z
Session: session_01LR1cD9JqgkeqY7k9rrZH8s
Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24549-parent-gate-parity
Worktree: objectstack-issue-24549
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface: packages/services/service-storage/src/attachment-access-hooks.ts and packages/plugins/plugin-audit/src/comment-access-hooks.ts (the parent-record gates), with new test files beside them; packages/plugins/plugin-security/src/** only if the parent-write check the gates call is where the divergence lives, declared before any edit there; a changeset; content/docs/permissions/attachments-access.mdx (the page states the parent-edit rule; amended by the seat at the review of record, so the docs follow the gate); ⛔ not packages/spec/** (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (the default tier; dispatch-gates --tier printed no path-derived mandate for this surface)
Clause-②: no
Responsibility: the sys_attachment / sys_comment parent-record gates (this lane) | the parent's by-id update judgement, which the gates must agree with (the card's acceptance) | callers of the attachment and comment doors; no shipped HotCRM path reaches it today (card)
Thread-read: 6268988807
Serial constraints cleared: no open PR touches service-storage/src/**, plugin-audit/src/comment-* or plugin-security's security-plugin.ts (a file scan of every open PR at this claim). No pm:dispatched claim names those gate files: #24343 (epic) holds plugin-audit/src/audit-writers.ts, a different file.
Disclosure: this is a security card whose reproduction is redacted (triage 6268988807, #9387). The dev derives the divergence from the code and the card's acceptance invariant. Nothing that reads as a reproduction goes on any public surface; the dev's concrete findings come to the seat in the session only.
os-dev-report
{
"issue": 24549,
"status": "done",
"branch": "claude/issue-24549-parent-gate-parity",
"pr": "#24641",
"session": "session_01LR1cD9JqgkeqY7k9rrZH8s — the dispatching seat's id (subagent run); every commit carries it as its Claude-Session trailer",
"premise_still_valid": true,
"summary": "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 record-grained explain verdict for a by-id update of the parent (allowed and record.visible) and refuse where it refuses, so every parent-edit limb (attach, update/delete/re-point of an attachment, assertMayAttach, comment moderation) gives the answer a by-id update of the parent gets; existing refusals keep their envelopes and a fault keeps its status. Measured on a real stack as a differential matrix (8 caller shapes x 14 parent records x 3 gate operations, each against the parent's by-id update and security.explain): 38 cells where the gate admitted and the update refused before the fix, 0 after. Two restrictive disagreements remain, both pre-existing refusals (the ruled owner-only edit depth, and one row where explain itself disagrees with the update); plugin-security is unchanged.",
"tests": "Head f4f4615 (final). Pins written first and committed alone (04ed98c), run on the unfixed gates: attachment rows 'Tests 38 failed | 29 passed (67)', comment rows 'Tests 20 failed | 12 passed (32)'; with the fix: 67/67 and 32/32. Ablation on the final head via scripts/ablation-replace.mjs (old decision put back in each gate, WRAP mode): attachment-access-hooks.test.ts 'Tests 38 failed | 104 passed (142)', comment-parent-update-parity.test.ts 'Tests 20 failed | 12 passed (32)'; restore proven 'ok restored: blob == HEAD' for both files, git diff HEAD 0 lines, marker count 0. Package suites on f4f4615 under os-verify-lock: pnpm --filter @objectstack/service-storage test 'Test Files 61 passed (61) / Tests 1118 passed (1118)' VERDICT command-exit 0; pnpm --filter @objectstack/plugin-audit test 'Test Files 57 passed (57) / Tests 930 passed (930)' VERDICT command-exit 0; typecheck both (tsc + scripts + check:test-typecheck 'OK ... 0 error(s)') VERDICT command-exit 0. Real-stack differential (local scratch harness, not committed): before 38 over-admitting cells, after 0; dist-level before/after confirmed by restoring the base gate sources, rebuilding, re-measuring, then restoring HEAD (blob == HEAD) and rebuilding (dist marker 0 -> 2). test:repo not run locally (diff touches no repo-project test) — declared to CI. ① closure build: pnpm --filter service-storage... plugin-audit... plugin-security... plugin-sharing... driver-sql... build VERDICT command-exit 0.",
"gates": "Local, on f4f4615: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 69 commands (after git fetch; same list on the earlier head); all 69 run, each exit code captured before any pipe: 69 x exit 0. Earlier runs on bff4442 had check:engine-double-contract exit 1 and check:objectql-double-limit exit 1 (my new doubles: findOne predicate + limit) — fixed in f4f4615 and green; check:dual-build-cjs-loads and check:i18n exit 3 (PREREQUISITE NOT MET) there — prerequisite built (turbo, 61/61 cached), both exit 0 on f4f4615. 9 commands (lines 23-31: registry-log-declared, rest-log-spy-declared, system-context-census, tenant-audit-census and self-tests, undeclared-dep-imports) overlapped the 48 s ablation window and were re-run after it: 9 x exit 0. --ran reconciliation with exit codes recorded: 'Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN' / '69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)'. CI check-runs on f4f4615 at report time: completed success — 'No other open PR may claim the same issue', 'filter', 'Part-of PR must not also close its card', 'The card this PR closes must claim this branch', 'Check Documentation Links', 'No other open PR may claim the same single-writer path'; skipped (main-only) — Dogfood Regression Gate shards, Temporal Conformance, Dogfood Verify CLI, Build Docs, Console Pin Gate, Packed-tarball smoke; in_progress or queued — Test Core (1-6/6), Build Core, Lint · (ESLint, repo & tooling gates, CI release & engine gates, docs & code guards, PM dispatch-gates, scoped x3), Type Check · (workspace, source gates, consumer gates, debt ledger), Governed Surface Queue Guard, Check Changeset, Check PR Size, Flag docs affected by code changes.",
"line_budget": "755 (additions 744 + deletions 11, 5 files) — under the 3000 human-merge threshold",
"files_changed": [
"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"
],
"deviations": [
"The pre-edit empty-branch push (os-dev rule 1 write-route probe) was refused by this container's permission classifier ('[Git Destructive]'); not retried. The first push happened only after the fix sat on red pins, which is the dispatch's security order — the two instructions conflict and the security order was the one kept.",
"The real-stack differential matrix was measured with a local scratch harness under packages/qa/dogfood/test (never committed, deleted before the gate run). The committed pins are unit pins with sharing/security doubles: a real-stack pin inside the gate packages needs @objectstack/plugin-security and @objectstack/plugin-sharing as devDependencies (two package.json files + pnpm-lock.yaml), outside the claim's file surface, and a concrete fixture would read as a reproduction under the disclosure ruling. See open question 2.",
"The attachment parity rows live in the existing attachment-access-hooks.test.ts (driving its ledgered engine double), not a new file: a new file's engine double made check:engine-double-contract demand a scripts/engine-double-contract.pinned.json row, outside the file surface (os-dev toolchain trap: prefer the file's existing double, leave the ledger). The comment rows are a new file beside the gate.",
"packages/plugins/plugin-security was not edited: measured, its explain update verdict already agrees with the by-id update on every row the invariant needs except one restrictive row (reported below), so no parent-write-check change was needed and none is declared.",
"One 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 it answers) instead of 403 FILES_DISABLED (stated in the changeset and the PR's Acceptance notes).",
"The public os-dev-report comment omits the 'seat_only_divergence' field (the concrete callers, bits and operations), per the dispatch's disclosure order; the final message to the seat carries it. Otherwise the two JSON documents are identical.",
"Commit trailers use the model-free pair AGENTS.md requires (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By form.",
"The first gate run (on bff4442) and the final union both ran unlocked (check:* families) while locked package suites and the ablation ran; the ablation window overlap is covered by the 9-command re-run above."
],
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write workflow as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24641/assignees (os-warren), read back 6761/6761 bytes identical; (2) this report = POST /repos//issues/24549/comments. Plus git push x4 of the branch (not REST). Reads only otherwise (GET issue 24549, its comments, issue 22455, pull 24641, its check-runs).",
"open_questions": [
{
"question": "After this fix the gates still refuse a caller whose edit depth on the parent object is wider than own (unit / unit_and_below / org), where the parent's by-id update and security.explain admit: the sharing step is asked at own by the 2026-08-10 ruling recorded on ISharingService ('Write DEPTH is an input the CALLER supplies'). The card's acceptance bullets 1-2 (same answer as the update; explain agrees) and bullet 3 (existing refusals keep their answers) pull in opposite directions on exactly these rows. Should the gates now drop their own-depth sharing step and let the security service's update judgement (which resolves depth together with the object-level grant, so the resolveWriteScope fail-open the ruling cites does not arise) decide alone?",
"options": [
"A — keep the ruled own-depth step (this PR as it stands). Axes: business need — moderation by a manager on a subordinate's record is plausible, but no deployment's use of depth on attachments or comments was measured, so the need is unproven; long-term — leaves a deliberate second, narrower copy of one leg of the judgement, documented in two module headers and on ISharingService; AI-error — authors must know an exception (depth does not reach attachments/comments), a declared-but-not-honoured feeling for depth; startup focus — zero new work, no ruling reversed inside a p1 security fix.",
"B — lift it in a follow-up: the edit question becomes the security service's update judgement alone (the sharing and master steps go, the update judgement already contains both). Axes: business need — same unmeasured need; long-term — one judgement, no copy, the card's invariant holds both ways (except the explain-vs-update row reported below); AI-error — one rule ('what a by-id update of the parent answers') with nothing to remember; startup focus — small code removal, but it reverses a recorded maintainer ruling and widens who may moderate, so it needs the maintainer's word."
],
"recommendation": "A for this card, because it closes the hole without reversing a recorded ruling inside a security fix; B then goes to the maintainer as its own ruling, since the ruling's stated reason (resolveWriteScope answering org for an unmatched object) does not apply to the composed judgement the gates now ask, and B makes the rule simpler for authors and agents."
},
{
"question": "Where should the real-stack form of the parity matrix live, given the disclosure hold?",
"options": [
"A — devDependencies on plugin-security + plugin-sharing for service-storage and plugin-audit (precedent: service-automation), real-stack pins beside the gates; runs on every PR (Test Core). Publishes a concrete fixture.",
"B — a dogfood suite beside cbp-parent-attachment-comment-gates.dogfood.test.ts; runs on main's full battery only. Publishes a concrete fixture.",
"C — keep what this PR ships: abstract unit pins per gate (sharing verdict x master outcome x update judgement) plus plugin-security's explain parity suites for the explain-equals-update half; add A or B once the maintainer lifts the disclosure hold (for example after the fix ships)."
],
"recommendation": "C now, because A and B both put a working recipe in public test code while the hold stands; then A, because a real-stack pin that runs on every PR catches a regression where it is made, which matters most against an agent-written change to either gate."
}
],
"out_of_scope_findings": [
"class: a · reach: public door — security.explain's record-grained update verdict answers visible=false on one matrix row where the by-id update of the same record by the same caller admits (restrictive: explain refuses, enforcement admits; not an authorization hole). Measured on the real stack in this card's matrix; the concrete row is in the seat-only field. Family: explain-versus-enforce — carrier: plugin-security's explain-enforce-parity.test.ts table (a new row there), report-only, not filed by this dev · dedupe words: explain update visible sharing deferral; explain-enforce parity record update; checkAuthoredRowWrite explain",
"carrier: 承接者:无 · noted, not filed — content/docs/permissions/attachments-access.mdx still describes the parent-edit rule as the sharing service's answer; it should state the invariant (a by-id update of the parent). Recorded in the PR's Acceptance notes."
]
}
Activity
[-]security(attachments, comments): the sys_attachment / sys_comment parent gates never ask the parent object's own update permission — a member refused update on crm_contract attaches and deletes others' files and comments there[/-][+]security(attachments, comments): authorization defect in the sys_attachment / sys_comment parent gates — detail withheld pending maintainer[/+]objectstack-fleet commentedon Oct 19, 2026
Triage: p1 +
security+target:v18·domain:services·area:access·pm:queue· reproduction redacted · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T13:58ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
6268314061,6268391803), carried an authorization reproduction and its mechanism. The seat removed both under RUNNER rule 2's carve-out (skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387). The originals are held off GitHub for the maintainer, and the edit histories still hold them, so purging those is the maintainer's act. ⛔ Do not restore them.origin/mainba4afcc239: the two gate files have not changed since the filer's read at04669a467. The premise holds as code-read.security.target:v18puts it in the in-flight security-fix class: it rides 18.0 if ready, and does not gate the release (6261853191).domain:services:service-storageandplugin-audit, both services packages.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 5 · 2026-10-19T17:11Z
Session:
session_01LR1cD9JqgkeqY7k9rrZH8sAccount:
os-warren(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24549-parent-gate-parityWorktree:
objectstack-issue-24549Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/services/service-storage/src/attachment-access-hooks.tsandpackages/plugins/plugin-audit/src/comment-access-hooks.ts(the parent-record gates), with new test files beside them;packages/plugins/plugin-security/src/**only if the parent-write check the gates call is where the divergence lives, declared before any edit there; a changeset;content/docs/permissions/attachments-access.mdx(the page states the parent-edit rule; amended by the seat at the review of record, so the docs follow the gate); ⛔ notpackages/spec/**(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(the default tier;dispatch-gates --tierprinted no path-derived mandate for this surface)Clause-②: no
Responsibility: the
sys_attachment/sys_commentparent-record gates (this lane) | the parent's by-id update judgement, which the gates must agree with (the card's acceptance) | callers of the attachment and comment doors; no shipped HotCRM path reaches it today (card)Thread-read: 6268988807
Serial constraints cleared: no open PR touches
service-storage/src/**,plugin-audit/src/comment-*orplugin-security'ssecurity-plugin.ts(a file scan of every open PR at this claim). Nopm:dispatchedclaim names those gate files: #24343 (epic) holdsplugin-audit/src/audit-writers.ts, a different file.Disclosure: this is a
securitycard whose reproduction is redacted (triage6268988807, #9387). The dev derives the divergence from the code and the card's acceptance invariant. Nothing that reads as a reproduction goes on any public surface; the dev's concrete findings come to the seat in the session only.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24549,
"status": "done",
"branch": "claude/issue-24549-parent-gate-parity",
"pr": "#24641",
"session": "session_01LR1cD9JqgkeqY7k9rrZH8s — the dispatching seat's id (subagent run); every commit carries it as its Claude-Session trailer",
"premise_still_valid": true,
"summary": "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 record-grained explain verdict for a by-id update of the parent (allowed and record.visible) and refuse where it refuses, so every parent-edit limb (attach, update/delete/re-point of an attachment, assertMayAttach, comment moderation) gives the answer a by-id update of the parent gets; existing refusals keep their envelopes and a fault keeps its status. Measured on a real stack as a differential matrix (8 caller shapes x 14 parent records x 3 gate operations, each against the parent's by-id update and security.explain): 38 cells where the gate admitted and the update refused before the fix, 0 after. Two restrictive disagreements remain, both pre-existing refusals (the ruled owner-only edit depth, and one row where explain itself disagrees with the update); plugin-security is unchanged.",
"tests": "Head f4f4615 (final). Pins written first and committed alone (04ed98c), run on the unfixed gates: attachment rows 'Tests 38 failed | 29 passed (67)', comment rows 'Tests 20 failed | 12 passed (32)'; with the fix: 67/67 and 32/32. Ablation on the final head via scripts/ablation-replace.mjs (old decision put back in each gate, WRAP mode): attachment-access-hooks.test.ts 'Tests 38 failed | 104 passed (142)', comment-parent-update-parity.test.ts 'Tests 20 failed | 12 passed (32)'; restore proven 'ok restored: blob == HEAD' for both files, git diff HEAD 0 lines, marker count 0. Package suites on f4f4615 under os-verify-lock: pnpm --filter @objectstack/service-storage test 'Test Files 61 passed (61) / Tests 1118 passed (1118)' VERDICT command-exit 0; pnpm --filter @objectstack/plugin-audit test 'Test Files 57 passed (57) / Tests 930 passed (930)' VERDICT command-exit 0; typecheck both (tsc + scripts + check:test-typecheck 'OK ... 0 error(s)') VERDICT command-exit 0. Real-stack differential (local scratch harness, not committed): before 38 over-admitting cells, after 0; dist-level before/after confirmed by restoring the base gate sources, rebuilding, re-measuring, then restoring HEAD (blob == HEAD) and rebuilding (dist marker 0 -> 2). test:repo not run locally (diff touches no repo-project test) — declared to CI. ① closure build: pnpm --filter service-storage... plugin-audit... plugin-security... plugin-sharing... driver-sql... build VERDICT command-exit 0.",
"gates": "Local, on f4f4615: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 69 commands (after git fetch; same list on the earlier head); all 69 run, each exit code captured before any pipe: 69 x exit 0. Earlier runs on bff4442 had check:engine-double-contract exit 1 and check:objectql-double-limit exit 1 (my new doubles: findOne predicate + limit) — fixed in f4f4615 and green; check:dual-build-cjs-loads and check:i18n exit 3 (PREREQUISITE NOT MET) there — prerequisite built (turbo, 61/61 cached), both exit 0 on f4f4615. 9 commands (lines 23-31: registry-log-declared, rest-log-spy-declared, system-context-census, tenant-audit-census and self-tests, undeclared-dep-imports) overlapped the 48 s ablation window and were re-run after it: 9 x exit 0. --ran reconciliation with exit codes recorded: 'Run reconciliation — 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN' / '69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)'. CI check-runs on f4f4615 at report time: completed success — 'No other open PR may claim the same issue', 'filter', 'Part-of PR must not also close its card', 'The card this PR closes must claim this branch', 'Check Documentation Links', 'No other open PR may claim the same single-writer path'; skipped (main-only) — Dogfood Regression Gate shards, Temporal Conformance, Dogfood Verify CLI, Build Docs, Console Pin Gate, Packed-tarball smoke; in_progress or queued — Test Core (1-6/6), Build Core, Lint · (ESLint, repo & tooling gates, CI release & engine gates, docs & code guards, PM dispatch-gates, scoped x3), Type Check · (workspace, source gates, consumer gates, debt ledger), Governed Surface Queue Guard, Check Changeset, Check PR Size, Flag docs affected by code changes.",
"line_budget": "755 (additions 744 + deletions 11, 5 files) — under the 3000 human-merge threshold",
"files_changed": [
"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"
],
"deviations": [
"The pre-edit empty-branch push (os-dev rule 1 write-route probe) was refused by this container's permission classifier ('[Git Destructive]'); not retried. The first push happened only after the fix sat on red pins, which is the dispatch's security order — the two instructions conflict and the security order was the one kept.",
"The real-stack differential matrix was measured with a local scratch harness under packages/qa/dogfood/test (never committed, deleted before the gate run). The committed pins are unit pins with sharing/security doubles: a real-stack pin inside the gate packages needs @objectstack/plugin-security and @objectstack/plugin-sharing as devDependencies (two package.json files + pnpm-lock.yaml), outside the claim's file surface, and a concrete fixture would read as a reproduction under the disclosure ruling. See open question 2.",
"The attachment parity rows live in the existing attachment-access-hooks.test.ts (driving its ledgered engine double), not a new file: a new file's engine double made check:engine-double-contract demand a scripts/engine-double-contract.pinned.json row, outside the file surface (os-dev toolchain trap: prefer the file's existing double, leave the ledger). The comment rows are a new file beside the gate.",
"packages/plugins/plugin-security was not edited: measured, its explain update verdict already agrees with the by-id update on every row the invariant needs except one restrictive row (reported below), so no parent-write-check change was needed and none is declared.",
"One 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 it answers) instead of 403 FILES_DISABLED (stated in the changeset and the PR's Acceptance notes).",
"The public os-dev-report comment omits the 'seat_only_divergence' field (the concrete callers, bits and operations), per the dispatch's disclosure order; the final message to the seat carries it. Otherwise the two JSON documents are identical.",
"Commit trailers use the model-free pair AGENTS.md requires (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By form.",
"The first gate run (on bff4442) and the final union both ran unlocked (check:* families) while locked package suites and the ablation ran; the ablation window overlap is covered by the 9-command re-run above."
],
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write workflow as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24641/assignees (os-warren), read back 6761/6761 bytes identical; (2) this report = POST /repos//issues/24549/comments. Plus git push x4 of the branch (not REST). Reads only otherwise (GET issue 24549, its comments, issue 22455, pull 24641, its check-runs).",
"open_questions": [
{
"question": "After this fix the gates still refuse a caller whose edit depth on the parent object is wider than own (unit / unit_and_below / org), where the parent's by-id update and security.explain admit: the sharing step is asked at own by the 2026-08-10 ruling recorded on ISharingService ('Write DEPTH is an input the CALLER supplies'). The card's acceptance bullets 1-2 (same answer as the update; explain agrees) and bullet 3 (existing refusals keep their answers) pull in opposite directions on exactly these rows. Should the gates now drop their own-depth sharing step and let the security service's update judgement (which resolves depth together with the object-level grant, so the resolveWriteScope fail-open the ruling cites does not arise) decide alone?",
"options": [
"A — keep the ruled own-depth step (this PR as it stands). Axes: business need — moderation by a manager on a subordinate's record is plausible, but no deployment's use of depth on attachments or comments was measured, so the need is unproven; long-term — leaves a deliberate second, narrower copy of one leg of the judgement, documented in two module headers and on ISharingService; AI-error — authors must know an exception (depth does not reach attachments/comments), a declared-but-not-honoured feeling for depth; startup focus — zero new work, no ruling reversed inside a p1 security fix.",
"B — lift it in a follow-up: the edit question becomes the security service's update judgement alone (the sharing and master steps go, the update judgement already contains both). Axes: business need — same unmeasured need; long-term — one judgement, no copy, the card's invariant holds both ways (except the explain-vs-update row reported below); AI-error — one rule ('what a by-id update of the parent answers') with nothing to remember; startup focus — small code removal, but it reverses a recorded maintainer ruling and widens who may moderate, so it needs the maintainer's word."
],
"recommendation": "A for this card, because it closes the hole without reversing a recorded ruling inside a security fix; B then goes to the maintainer as its own ruling, since the ruling's stated reason (resolveWriteScope answering org for an unmatched object) does not apply to the composed judgement the gates now ask, and B makes the rule simpler for authors and agents."
},
{
"question": "Where should the real-stack form of the parity matrix live, given the disclosure hold?",
"options": [
"A — devDependencies on plugin-security + plugin-sharing for service-storage and plugin-audit (precedent: service-automation), real-stack pins beside the gates; runs on every PR (Test Core). Publishes a concrete fixture.",
"B — a dogfood suite beside cbp-parent-attachment-comment-gates.dogfood.test.ts; runs on main's full battery only. Publishes a concrete fixture.",
"C — keep what this PR ships: abstract unit pins per gate (sharing verdict x master outcome x update judgement) plus plugin-security's explain parity suites for the explain-equals-update half; add A or B once the maintainer lifts the disclosure hold (for example after the fix ships)."
],
"recommendation": "C now, because A and B both put a working recipe in public test code while the hold stands; then A, because a real-stack pin that runs on every PR catches a regression where it is made, which matters most against an agent-written change to either gate."
}
],
"out_of_scope_findings": [
"class: a · reach: public door — security.explain's record-grained update verdict answers visible=false on one matrix row where the by-id update of the same record by the same caller admits (restrictive: explain refuses, enforcement admits; not an authorization hole). Measured on the real stack in this card's matrix; the concrete row is in the seat-only field. Family: explain-versus-enforce — carrier: plugin-security's explain-enforce-parity.test.ts table (a new row there), report-only, not filed by this dev · dedupe words: explain update visible sharing deferral; explain-enforce parity record update; checkAuthoredRowWrite explain",
"carrier: 承接者:无 · noted, not filed — content/docs/permissions/attachments-access.mdx still describes the parent-edit rule as the sharing service's answer; it should state the invariant (a by-id update of the parent). Recorded in the PR's Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT → PR #24641 comment 6276390086 ·
domain:servicesseat 2 · 2026-10-19T20:16Z · head7deb8468f8, after contract review6276338255(PASS).Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:servicesseat 2 ·os-warren·session_01LR1cD9JqgkeqY7k9rrZH8s· 2026-10-19T20:40Zadded_to_merge_queue2026-10-19T20:18Z →merged2026-10-19T20:39Zorigin/main7c7741dcb4is an ancestor (git merge-base --is-ancestor, exit 0)completedby the PR'sFixes #24549; this act clearspm:dispatchedand the assigneeContract review
6276338255(PASS) and ACCEPT6276390086are on the PR. The open questions go to the maintainer as their own decisions.Generated by Claude Code