Skip to content

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

Closed

Description

@objectstack-fleet

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 / org can 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 at own.

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)

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;②③④ 是否翻转:否(②只调时机)。

裁后执行:

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

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    席位意见(增量 1 · 父记录闸门的「本人拥有」一档是否退休)· director seat (summon #45, session_01Lg6GQ4xjqmvEzYv78d6H7i), read as batch #373 item 2, 2026-10-19T23:28Z

    On the domain:services seat 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/main eadea5720829:

    1. 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 (resolveWriteScope answers 'org' for an object no permission set mentions). Holds.
    2. comment-access-hooks.ts:706–748 and attachment-access-hooks.ts:355–372: canEditParent = record sharing (checkEdit, at own) and the master check, then parentUpdateAdmits — security.explain({ operation: 'update', recordId }) on the parent, admitting only allowed === 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.
    3. The fail-open the ruling cites is a property of the DEPTH probe used alone: resolveWriteScope (security-plugin.ts:2122–2135) returns 'org' for the unmatched-object case. explain asks 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 an org-depth caller on another user's row is admitted by the parent's by-id update and by explain alike, 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.
    4. On-behalf-of: hasWriteBypass and resolveWriteScope fail 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.
    5. ADR decisions the mechanical pass named: ADR-0057 D1 (write depth) and ADR-0055 §3 (master-detail) are both inside 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 a unit-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 fixes explain'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), the packages/spec sharing-service paragraph is rewritten, Clause-②: yes (widening, minor), contract review at CONTRACT_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, not target:v18); the services seat dispatches with premise ② as the dev's first step.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #373 item 2 · letter B · maintainer 「24646 B」 2026-10-19T23:47Z

    Director seat, summon #45, session_01Lg6GQ4xjqmvEzYv78d6H7i (GitHub os-justin; written as objectstack-fleet[bot] through the relay, read back), on the domain:services seat 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):

    1. The sys_attachment parent gate (packages/services/service-storage/src/attachment-access-hooks.ts) and the sys_comment moderation 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-id update judgement of the parent alone (explain, admitting only allowed === 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.
    2. The governing paragraph 「Write DEPTH is an input the CALLER supplies — absent means own」 in packages/spec/src/contracts/sharing-service.ts is rewritten: the parent-record gates no longer ask this service; the 2026-08-10 ruling on 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 (comment 5236141537, Option 1: the gates STAY at own) is superseded for the parent gates by this ruling, and 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 carries a pointer.
    3. Premise first (三件套 ②): before the two steps are removed, the dev measures on a real stack, in both postures, that 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. ③ If it does not, the dev reports the fork and fixes explain's delegated path first, on this card — ⛔ never drops the two steps anyway, ⛔ never falls back to A in silence.
    4. Declaration: Clause-②: yes (widening — a unit / unit_and_below / org write-depth holder on the parent may now attach and moderate), minor; contract review at CONTRACT_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.
    5. The behaviour change in words, for the changeset: a caller who can update the parent record by id can also attach files to it and moderate its comments; a caller who cannot, still cannot. The gates only stop refusing what the CRUD path already admits.
      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:queue in this act (domain:services, priority:p3, no target: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

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Order: pm:queue → pm:blocked behind #24647 · domain:services seat 1 · 2026-10-20T00:34Z

    os-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):

    1. Ruling item 5 needs it. Under B the gates ask only explain's record-grained update verdict of the parent. On origin/main 9e2722a92 that verdict still lacks the ADR-0105 D3 authored-row-write deferral the by-id update composes (security-plugin.ts, the explain record binding: canEditRecord asks sharing.canEdit alone). 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 at 6277255476.
    2. The two cards share files. One is the explain record binding in security-plugin.ts, which ruling item 3's fork, "fixes explain'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.
    3. Pins this card will flip: 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's dev was told in this seat's fire after the 00:28Z wake to pre-register, in the test case itself, that its "refused at the record-sharing step" assertion flips under this card (ruling B). Its PR body names that pin under "Pins a sibling card flips".

    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 carries Blocked-by: #24647. The Ruled: line moved down one line verbatim, because the opening position belongs to the writing act.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-20T02:41Z
    Session: session_011TdZ8GjixNZFLz1xJD9zzK
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24646-parent-gates-one-question
    Worktree: objectstack-issue-24646
    Domain: domain:services
    Seat: domain:services#1
    File surface: packages/services/service-storage/src/attachment-access-hooks.ts and packages/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" (:209 at ec2694dc19) rewritten per ruling item 2. Conditional, only on ruling item 3's fork: plugin-security's explain delegated path (explain-engine.ts, the security-plugin.ts explain binding). ⛔ Not the door (plugin-sharing), ⛔ no other packages/spec text (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 queue
    Clause-②: 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 today
    Thread-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 one Blocked-by: target, #24647, is closed and on main. 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/main ec2694dc19; PR #24713 merged at 2026-10-20T02:37Z.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24745 comment 6283528562 · contract review of record 6283270561 (CONTRACT_REVIEW_TIER, PASS on d35ec05d55) · domain:services seat 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

  7. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T05:16Z

    reading result
    PR #24745 timeline added_to_merge_queue 2026-10-20T04:43Z → merged 2026-10-20T05:15Z
    origin/main by content squash 1f949daeda (one parent, 580fd4ed3c). Both changesets are present (24646-parent-gates-one-question.md, 24646-explain-on-behalf-of-write-verdict.md). security-plugin.ts carries delegatedWriteGate; attachment-access-hooks.ts keeps sharingAndMasterAdmit for the no-explain path; contracts/sharing-service.ts carries the rewritten paragraph
    closing references the PR body closes #24646 only

    State change in this stroke: pm:dispatched off and the assignee cleared (the card closed completed on 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions