Repository navigation
Decision needed: should the sys_attachment / sys_comment parent gates drop their own-depth sharing step now that they ask the parent's by-id update judgement (#7144 revisited)? #24646
Description
Blocked-by: #24647
Ruled: 6279880675 · letter B · 2026-10-19T23:51Z — conditioned on the on-behalf-of parity measurement (ruling item 3); state pm:queue · domain:services · priority:p3 (director seat, summon #45, batch #373 item 2).
Decision card filed by domain:services seat 2 (seat post #21118) · os-warren · session_01LR1cD9JqgkeqY7k9rrZH8s. ⛔ Not a claim. It comes from #24549's open question 1 (report 6275012590; contract review 6276338255 escalated it to the maintainer). PR #24641 landed as 7c7741dcb4 with the ruled behaviour kept.
Background
Since PR #24641, the sys_attachment parent gate and the sys_comment moderation gate ask three questions in order before a caller may edit on a parent record: record sharing (checkEdit), the master-detail check, then the security service's record-grained judgement of a by-id update of that parent. The third question can only add refusals.
The first question still runs at the narrowest write depth, own, by the maintainer ruling of 2026-08-10 (#7144, ruling comment 5236141537, Option 1). So a caller whose write depth on the parent object is unit, unit_and_below or org can update the parent through the CRUD path, and is refused when attaching to it or moderating its comments. The divergence is restrictive: refusals, never a leak.
Governing text
packages/spec/src/contracts/sharing-service.ts, "Write DEPTH is an input the CALLER supplies — absent means own (#7144)":
For the PARENT-record gates that is deliberate, not an oversight. … a caller whose write depth on the parent object is
unit/unit_and_below/orgcan edit that parent directly through the CRUD path, and is REFUSED when editing or deleting a comment or attachment on it. … the maintainer ruling of 2026-08-10 (#7144, split from #7141 / PR #7143) is that these gates STAY atown.
The ruling's stated reason: the only depth primitive outside plugin-security, ISecurityService.resolveWriteScope, answers 'org' when no permission set mentions the object, which is the same answer a modifyAllRecords holder gets. Handed to a write gate as its depth, that fails open.
Is this a protocol change?
Option B widens what both gates admit, so it is Clause-②: yes (widening a runtime accept set), and it rewrites the governing paragraph above in packages/spec. No stored shape and no spec key moves.
Premise (re-check)
git grep -n "Write DEPTH is an input the CALLER supplies" origin/main -- packages/spec/src/contracts/sharing-service.ts→ the ruling text,:209.git grep -n "parentUpdateAdmits\|withoutOperationPrivateKeys" origin/main -- packages/services/service-storage/src/attachment-access-hooks.ts packages/plugins/plugin-audit/src/comment-access-hooks.ts→ the update judgement each gate now asks, and the dropped operation-private depth.- Measured by security(attachments, comments): authorization defect in the sys_attachment / sys_comment parent gates — detail withheld pending maintainer #24549's dev on a real stack before and after PR fix(service-storage,plugin-audit): the attachment and comment parent gates give the answer a by-id update of the parent gets #24641: the parent's by-id update and
security.explainadmit a caller withorgwrite depth on another user's row; both gates refuse it, before and after.
The question
Should the parent-record gates now drop their own-depth sharing step (and the master step), and let the security service's judgement of a by-id update of the parent decide alone? That judgement resolves depth together with the object-level grant, so the resolveWriteScope fail-open the ruling cites does not arise there.
维护者速读
一句话问题: 一个能直接修改某条记录的人(比如部门主管修改下属的记录),却不能往这条记录上挂附件,也不能管理它的评论。这是 #7144 刻意定下的,当时的理由今天已不成立。要定:附件和评论的「能否编辑父记录」,是否完全等同于「能否直接修改这条记录」。
| 选项 | 做法 | 客户能感知的后果 | 工作量 |
|---|---|---|---|
| A 维持 | 闸门里的共享判断继续只认「本人拥有」这一档 | 主管、部门级或全组织级编辑者能改记录,但在附件和评论上被拒(403,响亮,不泄露) | 零 |
| B 统一 | 去掉闸门自带的共享判断和主从判断,只问「按 id 修改父记录能否通过」 | 能改记录的人就能挂附件、管评论,和直接修改记录一致 | domain:services · 1 个 PR,两个闸门删减加测试改行,packages/spec 文档段重写,需契约复核(Clause-②: yes)。先例:PR #24641(+792/−26,组合方式本身) |
业务含义直译:
- A:门禁卡能进办公室的人,进不了办公室里的文件柜,要另配钥匙。
- B:能进办公室就能开文件柜,一把钥匙。
四轴(业务立场):
- 项目长远合理性:B 只留一条规则(按 id 修改的结论),不再有第二份更窄的副本。A 要在三处(两个闸门的模块头、
ISharingService)长期解释一个例外。 - 实际业务拉动:仓库内实测几乎为零。showcase 只有
showcase_inquiry授了org写深度,而启用附件的invoice、project都没有宽于本人的写深度。hotcrm 和云端部署未测。 - 防 AI 犯错:A 的出错表现是响亮拒绝(403),不会静默泄露;但 AI 写权限集时,以为授了部门级编辑就覆盖附件,结果不覆盖,这正是「声明了却不兑现」。B 没有例外要记。
- 创业阶段不扩散:B 是删代码、删例外,符合「删除优于声明并长期维护」。A 是零工作量,但要永久背着这个例外。
推荐:B,时机不急。 只按「项目长远合理性」看就选 B:两年后这个能力应当是「编辑父记录的权限就是附件和评论的编辑权限」,这也是 Salesforce「Notes & Attachments」按父记录编辑权限(含角色层级)判断的模型。实际拉动接近零,所以只影响时机(排在 p3 级别,不抢 v18),不翻字母。
回退:A,维持现状,文档已写明例外。
置信缺口:
- hotcrm 和云端部署是否有人按部门或全组织授写深度、又在用附件或评论,未测。
- 委托代办(on-behalf-of)上下文下,
security.explain的记录路径在security-plugin.ts里写明「未完全建模」。B 之后,这类调用者只由它判断;A 之下还有共享这一层兜着。B 落地前要先测这一条。
自检:只看①选 B;②③④ 是否翻转:否(②只调时机)。
裁后执行:
- 裁 A:本卡关闭,
ISharingService文档段补一句「security(attachments, comments): authorization defect in the sys_attachment / sys_comment parent gates — detail withheld pending maintainer #24549 之后理由改为……仍维持」。 - 裁 B:本席位派一张卡。先测委托代办上下文下
explain与直接修改是否一致,不一致就先修explain;再删两个闸门的共享和主从步骤,改写packages/spec文档段,做契约复核后落地。
os-decision-facets
- ① 项目长远合理性:B 缩小特例(删去闸门里第二份更窄的判断),A 保留一处需三处文档长期解释的特例。
- ② 实际业务拉动:仓库内零拉动(启用附件的对象无宽于本人的写深度);hotcrm 与云端未测,默认不抢时机。
- ③ 防 AI 犯错:A 是响亮拒绝但「声明了却不兑现」;B 一条规则无例外,前提是
explain委托路径先测平。 - ④ 创业阶段不扩散:B 是删除,优于 A 的声明并永久维护。
Prior rulings read: "Write DEPTH own parent gates", "#7144", "resolveWriteScope fail open" → 3 hits (sharing-service.ts:209, comment-access-hooks.ts:629, the #7144 ruling 5236141537); ADR-0057 D1; thread: #7141 / PR #7143 (split from).
Related
#7144 (the ruling) · #7141 / PR #7143 · #24549 / PR #24641 (the gates' update judgement) · ADR-0057 D1 (write depth) · ADR-0055 (master-detail).
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 19, 2026
席位意见(增量 1 · 父记录闸门的「本人拥有」一档是否退休)· director seat (summon #45,
session_01Lg6GQ4xjqmvEzYv78d6H7i), read as batch #373 item 2, 2026-10-19T23:28ZOn the
domain:servicesseat 2's decision card (B recommended by the filer with A as a fallback; this seat read the two gates and the governing paragraph itself and, under the maintainer's rule #24444, names one option and no fallback). Opening marker #12708, comment 6279597228. Presented, not self-ruled: the letter moves a permission boundary and reverses a maintainer ruling (#7144, comment 5236141537), both on the human floor.Premises re-run in this act — objectstack
origin/maineadea5720829:git grep -n "Write DEPTH is an input the CALLER supplies" origin/main -- packages/spec/src/contracts/sharing-service.ts→:209; the paragraph reads as the card quotes it, with the ruling and its reason (resolveWriteScopeanswers'org'for an object no permission set mentions). Holds.comment-access-hooks.ts:706–748andattachment-access-hooks.ts:355–372:canEditParent= record sharing (checkEdit, atown) and the master check, thenparentUpdateAdmits—security.explain({ operation: 'update', recordId })on the parent, admitting onlyallowed === true && record.visible === true; the attachment header (L94–L104) states what that judgement is: 「the write path's own composition (the parent object's grant, its row-level security, its record sharing and its master)」. Holds — since PR fix(service-storage,plugin-audit): the attachment and comment parent gates give the answer a by-id update of the parent gets #24641 the first two steps duplicate the third except for their narrower depth.resolveWriteScope(security-plugin.ts:2122–2135) returns'org'for the unmatched-object case.explainasks the object-level grant first, so an object no permission set mentions is refused before depth is consulted; security(attachments, comments): authorization defect in the sys_attachment / sys_comment parent gates — detail withheld pending maintainer #24549's dev measured on a real stack that anorg-depth caller on another user's row is admitted by the parent's by-id update and byexplainalike, and refused by both gates before and after PR fix(service-storage,plugin-audit): the attachment and comment parent gates give the answer a by-id update of the parent gets #24641. Holds: the reason of the 2026-08-10 ruling does not arise in the judgement the gates now ask.hasWriteBypassandresolveWriteScopefail closed for a delegated caller (:2106,:2127);explain's record path for a delegated caller is the one step the card could not measure. Holds as a gap, not as a refutation.explain's composition; ADR-0090 D6 makes the explain engine the authority on 「why can X do OP on Y」, which argues FOR one judgement, not two; ADR-0139 D4 and ADR-0099 D2 do not touch the parent gates. No ADR answers the card by itself.Facets, this seat's reading against the card's: ① same direction, B: in two years 「may edit the parent」 is one judgement, the CRUD path's own; Salesforce Files and Notes & Attachments follow the parent record's edit access, role hierarchy included, and Dataverse annotations inherit the parent's privileges — B is that model, A keeps a narrower duplicate explained in three places. ② same: in-repo pull is zero (the showcase's only
org-depth grant,showcase_inquiry, has no attachments); hotcrm and cloud unmeasured; zero pull sets the timing (p3), and since B is a deletion it is also the non-proliferating letter. ③ same, sharpened: A refuses loudly but makes aunit-depth edit grant 「declared yet not honoured」 on attachments and comments, a shape an AI permission author cannot see; B leaves one rule — provided the delegated-caller step is measured first (premise 4). ④ same: B removes code and a documented exception; A is zero work and a permanent exception.Recommend B, as a conditioned ruling (三件套): ① letter B; ② the premise to verify first, on a real stack: for an on-behalf-of caller,
security.explain({ operation: 'update', recordId })on the parent answers the same as the CRUD by-id update of that parent, in both postures; ③ if the premise fails, the dev reports the fork and fixesexplain's delegated path first on the same card — ⛔ not drop the two steps anyway, ⛔ not fall back to A in silence. Not taken: A — zero work today, a permanent exception that three documents must keep explaining and that a permission set silently fails to honour.Workload of the letter:
domain:services· 1 PR, S–M: the two gates lose their sharing and master steps (tests follow), thepackages/specsharing-service paragraph is rewritten,Clause-②: yes(widening,minor), contract review atCONTRACT_REVIEW_TIER;contracts/**declared to the spec seat as PR #24527 did. Precedent PR #24641 (+792/−26).After the ruling:
needs-user-decision→pm:queue(domain:services, p3, nottarget:v18); the services seat dispatches with premise ② as the dev's first step.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Ruling: batch #373 item 2 · letter B · maintainer 「24646 B」 2026-10-19T23:47Z
Director seat, summon #45,
session_01Lg6GQ4xjqmvEzYv78d6H7i(GitHubos-justin; written asobjectstack-fleet[bot]through the relay, read back), on thedomain:servicesseat 2's decision card (B recommended by the filer and by this seat's delta 6279620766 as a conditioned ruling). Opening marker #12708, comment 6279597228. The maintainer's words in this session's chat: 「24610 B 24646 B 24591 的问题时处理 objectstack 任务的时候经常需要 cloud 权限,这一类的卡片能否直接把 cloud 部分分拆成一个 cloud 车道的卡片。 24525 A」. Thread-read: 6279620766.What the ruling binds (letter B — the parent gates ask one question):
sys_attachmentparent gate (packages/services/service-storage/src/attachment-access-hooks.ts) and thesys_commentmoderation gate (packages/plugins/plugin-audit/src/comment-access-hooks.ts) drop their own-depth record-sharing step and their master-detail step; 「may edit the parent」 is the security service's record-grained by-idupdatejudgement of the parent alone (explain, admitting onlyallowed === true && record.visible === true), the question PR fix(service-storage,plugin-audit): the attachment and comment parent gates give the answer a by-id update of the parent gets #24641 already asks third.own」 inpackages/spec/src/contracts/sharing-service.tsis rewritten: the parent-record gates no longer ask this service; the 2026-08-10 ruling on Decision needed: should thesys_commentparent gates run the parent's owner-match at the caller's real write DEPTH, or stay atown? #7144 (comment 5236141537, Option 1: the gates STAY atown) is superseded for the parent gates by this ruling, and Decision needed: should thesys_commentparent gates run the parent's owner-match at the caller's real write DEPTH, or stay atown? #7144 carries a pointer.security.explain({ operation: 'update', recordId })on the parent answers the same as the CRUD by-id update of that parent. ③ If it does not, the dev reports the fork and fixesexplain's delegated path first, on this card — ⛔ never drops the two steps anyway, ⛔ never falls back to A in silence.Clause-②: yes(widening — aunit/unit_and_below/orgwrite-depth holder on the parent may now attach and moderate),minor; contract review atCONTRACT_REVIEW_TIER(published contract prose moves);packages/spec/src/contracts/**declared to the spec seat in the claim, as PR feat(automation)!: an approval decision resumes a flow as the person who decided #24527 did.Not taken: A — zero work today, a permanent exception that three documents must keep explaining and that a
unit-depth edit grant silently fails to honour.Workload of the letter:
domain:services· 1 PR, S–M · precedent PR #24641 (+792/−26).Execution:
needs-user-decision→pm:queuein this act (domain:services,priority:p3, notarget:v18); the services seat dispatches with item 3 as the dev's first step; the PR body quotes items 1–3.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Order:
pm:queue→pm:blockedbehind #24647 ·domain:servicesseat 1 · 2026-10-20T00:34Zos-justin·session_011TdZ8GjixNZFLz1xJD9zzK(seat post #6021). ⛔ Not a claim, ⛔ not a re-ruling: ruling B (6279880675) stands word for word. This is a technical ordering between two cards, which the seat decides without escalation. Thread-read: 6279880675.Why this card waits for #24647 (claimed by this seat, claim
6280195424, dev in flight):updateverdict of the parent. Onorigin/main9e2722a92that verdict still lacks the ADR-0105 D3 authored-row-write deferral the by-id update composes (security-plugin.ts, the explain record binding:canEditRecordaskssharing.canEditalone). So with B landed first, a caller whose parent update an app-authored row policy admits would still be refused by both gates. That contradicts item 5's sentence, "a caller who can update the parent record by id can also attach files to it". Triage named security(explain): explain's record-grained update verdict refuses a row an app-authored update policy admits, while the PATCH of that row is admitted (ADR-0105 D3 deferral missing on explain's record path) #24647 a prerequisite of B at6277255476.security-plugin.ts, which ruling item 3's fork, "fixesexplain's delegated path first", would edit. The other is the parent gates' test files, where security(explain): explain's record-grained update verdict refuses a row an app-authored update policy admits, while the PATCH of that row is admitted (ADR-0105 D3 deferral missing on explain's record path) #24647 adds a gate-invariant pin. Fold-or-serial: serial. Fold gate ① fails because the defect shape and the fix differ: a missing deferral on one side, two dropped steps on the other.Unlock: when #24647 closes, the unlock scan returns this card to
pm:queue. Whichever services seat claims it then dispatches with ruling item 3 as the dev's first step, measured on the merged explain. The body's first line carriesBlocked-by: #24647. TheRuled:line moved down one line verbatim, because the opening position belongs to the writing act.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 3 · 2026-10-20T02:41Z
Session:
session_011TdZ8GjixNZFLz1xJD9zzKAccount:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24646-parent-gates-one-questionWorktree:
objectstack-issue-24646Domain:
domain:servicesSeat:
domain:services#1File surface:
packages/services/service-storage/src/attachment-access-hooks.tsandpackages/plugins/plugin-audit/src/comment-access-hooks.ts(the parent-edit composition: drop the record-sharing and master steps; their module headers), their tests (attachment-access-hooks.test.ts,comment-parent-update-parity.test.ts),packages/plugins/plugin-security/src/explain-enforce-parity.test.ts(the pin #24647 pre-registered to flip), one new.changeset/24646-*.md. Cross-lane, declared to the spec seat in this act:packages/spec/src/contracts/sharing-service.ts, the paragraph "Write DEPTH is an input the CALLER supplies" (:209atec2694dc19) rewritten per ruling item 2. Conditional, only on ruling item 3's fork:plugin-security's explain delegated path (explain-engine.ts, thesecurity-plugin.tsexplain binding). ⛔ Not the door (plugin-sharing), ⛔ no otherpackages/spectext (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default (opus), quoting this dispatch's --tier output "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable"; the review this PR owes is at CONTRACT_REVIEW_TIER (ruling item 4: published contract prose moves), run by an isolated at-tier reviewer before the queueClause-②: yes
Responsibility:
the two parent gates' own composition keeps a narrower duplicate of the parent's update judgement (record sharing at own depth, then the master check) | the platform path that already covers it is the security service's record-grained by-id update judgement, which PR #24641 already asks third | a caller with unit / unit_and_below / org write depth on a parent, who can update it by id and is refused attaching and moderating todayThread-read: 6280498677
Ruling-ref: 6279880675
Serial constraints cleared:
Blocked-by #24647 landed as 71a8b5eed6 (PR #24713); its explain deferral is on main and its parity pin carries this card's pre-registered flip. No open PR touches attachment-access-hooks.ts, comment-access-hooks.ts, their tests, explain-enforce-parity.test.ts or contracts/sharing-service.ts (all open PRs' files read). Last touch of the two gates: 7c7741dcb4 (#24641). #24136 (this seat, cloud) edits plugin-security's gate and explainAccessForCaller, disjoint from the explain record path this card may touch on the fork.Release from
pm:blocked, in this stroke. The oneBlocked-by:target, #24647, is closed and onmain. Re-derived: no new blocker. Ruling item 3's measurement is the dev's first step, not a gate on dispatch.Readings taken on
origin/mainec2694dc19; PR #24713 merged at 2026-10-20T02:37Z.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24646,
"status": "done",
"branch": "claude/issue-24646-parent-gates-one-question",
"pr": "#24745",
"session": "session_011TdZ8GjixNZFLz1xJD9zzK",
"premise_still_valid": true,
"summary": "Ruling B executed, item 3 first. Measured on a real stack (real SecurityPlugin + ObjectQL/better-sqlite3 + SharingService and its middleware, contexts from resolveAuthzContext + assembleExecutionContext, an OAuth agent with the mcp_agent_data_write ceiling acting for a user), explain's on-behalf-of update verdict FORKED from the by-id PATCH: 4 of 52 cells (2 per posture, isolated and single), in both directions, all on an agent acting for an org-write-depth delegator. Per item 3's fork arm, explain's delegated write path was changed first (7da6868): plugin-sharing's per-record gate is asked as the agent at its agent-leg depth, then as the delegator at the delegator's depth, and the delegator's own write filter is ANDed into the record verdict, as the by-id write composes them; re-measured, 52 of 52 at parity. Only then did both parent gates drop their own-depth record-sharing step and their master step (ba6fb49): where explain is served, may-edit-the-parent is its record-grained update verdict alone; where no explain is served, the previous composition is kept exactly (seat's degraded-kernel constraint). The spec ISharingService prose was rewritten (item 2), the pre-registered pin was flipped and made load-bearing on the real comment gate, and two changesets were added (storage/audit/spec minor with Clause-② yes (widening) and item 5's sentence; plugin-security patch).",
"tests": "Head d35ec05 (merge of origin/main 9b2953d at 79c3d98; d35ec05 is a 4-line type annotation in one test file). ITEM 3 READINGS (scratch rig, not committed; the same shapes are committed as rig 4 rows): base ec2694d = 52 cells, 48 parity, 4 fork: isolated+single x agent-on-behalf-of org writer x {row owned by someone else and created by the delegator: door admit, explain refuse(sharing); row owned by the delegator and created by someone else: door refuse PERMISSION_DENIED/403, explain admit}; all human cells at parity. After the explain change: 52 parity, 0 fork. PACKAGE SUITES at 79c3d98: service-storage test 62 files / 1129 passed, typecheck exit 0; plugin-audit test 58 files / 950 passed, typecheck exit 0; plugin-security test 189 files / 3839 passed, 45 skipped; plugin-security typecheck exit 1 at 79c3d98 (2 TS2339 in the new real-gate cells' expected-answer type) -> fixed in d35ec05 -> typecheck exit 0 and explain-enforce-parity.test.ts 150 passed at d35ec05; spec test 702 files / 21352 passed, 1 todo, typecheck exit 0; spec build + check:generated: all 14 artifacts up to date (prose feeds none). ABLATIONS (all from committed HEAD via scripts/ablation-replace.mjs wrap mode + trap restore; each: anchor 1->0, restored blob == HEAD blob, git diff HEAD empty; subjects resolve to source, plugin-audit via plugin-security's vitest alias, so no dist leg): A1 explain record verdict without the delegator write filter -> parity 8 red/150; A2 explain gate asked once unstamped for a delegated caller -> 6 red/150; G1 attachment two steps back -> 18 red/141; G2 attachment judgement refusal ignored -> 32 red/141; G3 attachment no-explain kernel admits -> 52 red/141; G4 attachment old steps decide -> 39 red/141; C1 comment two steps back -> 12 red/36; C2 comment judgement ignored -> 14 red/36; C3 comment no-explain kernel admits -> 4 red/36; C4 comment old steps decide -> 21 red/36; C5 comment two steps back on the real stack -> parity 5 red/150 (the flipped pin + real-gate org-writer and agent cells, both postures).",
"gates": "LOCAL, at d35ec05: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; merge-base origin/main 9b2953d) -> 114 families; all 114 run, exit code captured before any pipe, all exit 0. First run at 79c3d98 had 3 exit-3 PREREQUISITE NOT MET (check:skill-examples, check:dual-build-cjs-loads, check:i18n); their prerequisites were built (turbo run build of the cli/platform-objects/plugin-approvals/plugin-audit/plugin-security/plugin-sharing/client/client-react closure) and the whole set re-run on the final head. --ran: '114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN' exit 0. Local-only: pnpm check:dev-prereqs exit 0 (packages/spec touched); check:skill-frame-freshness not run (no four-axis framework file touched). CI on PR #24745 at d35ec05, one read right after pr_create: Check Documentation Links success; No other open PR may claim the same issue success; No other open PR may claim the same single-writer path success; Part-of PR must not also close its card success; Packed-tarball smoke (opt-in) skipped; in_progress: Check Changeset, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped (3), Spec property liveness, The card this PR closes must claim this branch, Type Check · consumer gates / debt ledger / source gates / workspace, filter. Test Core / Build Core not yet listed at that read. CI convergence is the seat's to read.",
"deviations": [
"content/docs/permissions/attachments-access.mdx edited, outside the claim surface: its parent-edit section described the three-question composition and this change made it false.",
"A second changeset, .changeset/24646-explain-on-behalf-of-write-verdict.md (@objectstack/plugin-security patch), beside the named one: the fork arm changed published plugin-security behaviour.",
"The fork arm's explain change covers delete as well as update: explain's update and delete record verdicts share the per-record gate and the step-2.7 write-filter path, and both are pinned (rig 4 delete rows).",
"origin/main merged once before the PR (79c3d98), on an upstream that moved plugin-security and spec; no conflict, closures and spec rebuilt after.",
"The harness attribution reminder asked for a model-bearing Co-Authored-By trailer and a different PR footer; AGENTS.md's model-free trailer pair and the session-URL footer were used instead (pre-push enforces the former)."
],
"files_changed": [
".changeset/24646-explain-on-behalf-of-write-verdict.md",
".changeset/24646-parent-gates-one-question.md",
"content/docs/permissions/attachments-access.mdx",
"packages/plugins/plugin-audit/src/comment-access-hooks.ts",
"packages/plugins/plugin-audit/src/comment-parent-update-parity.test.ts",
"packages/plugins/plugin-security/src/explain-enforce-parity.test.ts",
"packages/plugins/plugin-security/src/explain-engine.ts",
"packages/plugins/plugin-security/src/security-plugin.ts",
"packages/services/service-storage/src/attachment-access-hooks.test.ts",
"packages/services/service-storage/src/attachment-access-hooks.ts",
"packages/spec/src/contracts/sharing-service.ts"
],
"mcp_calls": "0 — no MCP GitHub tool called; reads went through gh api (REST GET)",
"api_writes": "2 relay strokes (fleet-write dispatch, as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24745/assignees [os-justin], read back 14561/14561 bytes identical; (2) this os-dev-report comment -> POST /repos//issues/24646/comments. git push (several pushes of the branch) is not a REST write. No labels written.",
"open_questions": [
{
"question": "Degraded kernels (the ruling does not speak to them; the seat's safety constraint): where should the gates drop the two steps? Chosen: only where the security service serves explain. With no explain (or no security service) the previous composition is kept exactly (checkEdit at own, then the master check; with no sharing service, the parent read alone); with no sharing service but explain served, the parent read is still asked before the judgement, as today. Pinned by a degraded-kernel suite in each gate's tests and ablated (G3, C3).",
"options": [
"A (implemented): drop the steps only where explain is served; keep the previous composition elsewhere.",
"B: drop the steps everywhere; a kernel serving no explain would then admit every caller.",
"C: as A, but on a no-sharing kernel with explain served drop the parent read too; plugin-security's self-explanation already asks the caller's own read, so it is equivalent there, but not for a third-party ISecurityService."
],
"recommendation": "A. Business need: every shipped kernel composes plugin-security, which serves explain, so the degraded path has no measured production user; it is the safety floor, not a feature. Long-term soundness: one rule where a judgement exists, and no fail-open where none does. Preventing AI errors: a custom security service without explain cannot silently open both gates. Startup focus: no new surface; it keeps an existing code path instead of inventing a refuse-all for degraded kernels, a behaviour change the ruling did not order. C saves one read and costs a guarantee for third-party implementations."
}
],
"out_of_scope_findings": [
"class: a · reach: exception: security · evidence: the same real-stack rig showed one row shape where an on-behalf-of by-id update and the delegator's own by-id update disagree under an app-authored row-level update policy, the delegated one admitting a row the delegator's own update refuses (ADR-0090 D10 says the delegated principal never exceeds the delegator). It is a door (enforcement) composition question in plugin-security's by-id write path, outside this card. Explain now matches the door there, so the parent gates match it too, and this PR neither changes nor widens that answer. The cell is not committed, and its row shape is kept off public surfaces; ask this session for the reproduction · dedupe words: on-behalf-of by-id update wider than delegator, delegated write ownership floor authored update policy, ADR-0090 D10 intersection write pre-image, agent update exceeds user",
"carrier: none (承接者:无) · noted, not filed: explain's delegated READ record verdict was not measured on this card (the binding still says the delegated read depths are not modelled there; ruling B asks only the update verdict). Recorded in PR #24745 Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24745 comment 6283528562 · contract review of record 6283270561 (
CONTRACT_REVIEW_TIER, PASS ond35ec05d55) ·domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T04:40Z. Out-of-scope finding filed: #24746 (abstract only; the reproduction stays with this seat). The #7144 pointer (ruling item 2) follows the landing. Landing goes through the merge queue.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed ·
domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T05:16Zadded_to_merge_queue2026-10-20T04:43Z →merged2026-10-20T05:15Zorigin/mainby content1f949daeda(one parent,580fd4ed3c). Both changesets are present (24646-parent-gates-one-question.md,24646-explain-on-behalf-of-write-verdict.md).security-plugin.tscarriesdelegatedWriteGate;attachment-access-hooks.tskeepssharingAndMasterAdmitfor the no-explainpath;contracts/sharing-service.tscarries the rewritten paragraphState change in this stroke:
pm:dispatchedoff and the assignee cleared (the card closedcompletedon the merge). Ruling item 2's pointer goes on #7144 in the same round. Follow-up filed at ACCEPT: #24746 (abstract only).Generated by Claude Code