Skip to content

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

Closed

Description

@os-zhuang

Split out of #7141 (PR #7143) rather than decided there, because it is a widening and the two candidate shapes that card named diverge here — on nothing else.

The fact

ISharingService.canEdit widens its owner-match by the access DEPTH the caller
holds on the probed object, read from the middleware-private __writeScope key
(plugin-sharing/src/sharing-service.ts — matchesOwnerScope, and
if (writeScope === 'org') return true). plugin-security supplies that key
explicitly for the object it is probing, via the private
resolveWriteScopeForSharing (security-plugin.ts:2516).

The sys_comment gates in plugin-audit supply no depth. The five-field
projection #7141 removed never carried one, and PR #7143 deliberately kept it
that way: an absent depth leaves the owner-match at its narrowest (own), which
is the safe direction and is byte-for-byte what the projection produced.

The consequence, stated as behaviour

A caller whose write depth on the PARENT object is unit / unit_and_below /
org can edit that parent record directly through the CRUD path (where the
middleware stamps the depth for that object) but is refused when they try to
edit or delete a comment on it, because this gate asks the same service the same
question with the depth omitted. It is a divergence in the restrictive
direction — no data leaks — between the comment gate and the parent's real edit
authority. Whether that is correct or is a bug depends on what the comment gate
is meant to mean, which is a product decision, not a code reading.

Why it was not simply fixed in #7143

The tool a package outside plugin-security has for this is the published
ISecurityService.resolveWriteScope. It fails open on one input:
getEffectiveScope returns 'org' when no permission set matches the object
(permission-evaluator.ts:255), and the service's own doc block flags that
'org' as "non-authoritative on its own". Passed to canEdit as __writeScope
it becomes fully authoritative, and matchesOwnerScope then returns true for
any owned row of an unmatched object — a real widening, not a theoretical
one. Taking that route needs either a narrower contract method (one that
distinguishes "org depth" from "nothing matched") or the gate resolving depth
some other way.

Note this is the same question for service-storage's attachment kit, which
gates on canEdit the same way.

What a decision would look like

  1. Keep own (today, and fix(plugin-audit): forward the caller's execution envelope to the sys_comment sharing gates (#7141) #7143's behaviour) — the comment gate is
    deliberately tighter than the parent's edit authority. Then say so in the
    contract's doc block, so the next reader does not "fix" it.
  2. Resolve the parent's depth — the comment gate inherits the parent's real
    edit authority. Needs a depth primitive that does not fail open on an
    unmatched object before it can be wired.

Related: #7141, PR #7143, #6523, #6206, ADR-0057 D1, ADR-0111 D1.

Activity

  1. claude commented on Aug 9, 2026

    @claude
    Contributor

    Triage: needs-user-decision + domain:identity.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling (2026-08-10, directed in session session_01BPWqbmEFU8gJepBJTHESXd): Option 1 — the comment gate stays at own.

    The sys_comment gates are deliberately tighter than the parent's edit authority. Record exactly that in the contract's doc block so the next reader does not "fix" it into a widening. Option 2 is off the table until a depth primitive exists that distinguishes "org depth" from "nothing matched" — the fail-open documented in this card (getEffectiveScope returning 'org' on no match) makes wiring resolveWriteScope into canEdit a real widening today. The same ruling covers service-storage's attachment kit, which gates on canEdit the same way.

    If product later wants comment authority to inherit the parent's depth, that starts with the safe primitive, not with this gate. needs-user-decision → pm:queue (doc-block card).


    Generated by Claude Code

  3. self-assigned this
    on Aug 10, 2026
  4. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Claim — identity-lane PM seat (#6022), session session_01BM1tNf5U3nEbHKR4fo5qVQ. Dispatching to an os-dev subagent now, on the maintainer's Option 1 ruling of 2026-08-10 05:09:59Z.

    • Branch: claude/issue-7144-comment-gate-own-depth-contract
    • Worktree: ../objectstack-issue-7144 (dedicated per-task worktree)
    • Status: pm:queue → pm:dispatched. Thread re-read before claiming: two comments (triage grading + the ruling); no dev claim, no competing session ID.

    Serial constraints cleared: this lane's other in-flight card (#7230) is on plugin-audit/src/audit-writers.ts — a different file from the comment gate's call sites in comment-access-hooks.ts. #7141 / PR #7143 landed 22:10Z and is this card's baseline, not its target.

    Scope, as ruled — this is a doc-block card, not a behaviour change:

    • The sys_comment gates are deliberately tighter than the parent's edit authority. Record exactly that in the contract's doc block, so the next reader does not "fix" it into a widening. That sentence is the deliverable.
    • ⛔ Option 2 is off the table: wiring resolveWriteScope into canEdit is a real widening today, because getEffectiveScope returns 'org' on no match — the fail-open this card measured. Do not implement it, and do not "prepare" for it.
    • ⛔ No behaviour change at all. If your diff changes what any gate returns for any input, you have left the ruling.
    • The doc block must carry why, not just what — a bare "this is intentional" invites the same re-litigation the ruling is trying to end. Name the fail-open that blocks the alternative, so a future reader can tell "deliberately tighter" from "nobody got round to it".

    Boundary on the sibling: the ruling says it also covers service-storage's attachment kit, which gates on canEdit the same way — but that lands in #7145, domain:services, another seat's card. Do not edit service-storage. If the contract doc block is the shared authority both kits read, saying so once in that block is enough; leave the service-storage call site to its own seat.

    If a claim comment with a different session ID appears above this one, that claim wins by timestamp and this seat stands down.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    Contributor

    Pointer · director seat, summon #45, session_01Lg6GQ4xjqmvEzYv78d6H7i · 2026-10-19T23:55Z

    Ruling 5236141537 on this card (Option 1: the sys_attachment and sys_comment parent-record gates STAY at own write depth) is superseded for those gates by #24646 → B (ruling 6279880675, the maintainer's 「24646 B」): the gates drop their own-depth sharing step and master step and ask the parent's by-id update judgement alone, once the on-behalf-of parity premise named there is measured. The governing paragraph in packages/spec/src/contracts/sharing-service.ts is rewritten by that card's PR. This card stays closed; the pointer is its record.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    Contributor

    Pointer · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T05:17Z · ruling 6279880675 on #24646, item 2.

    For the parent-record gates, the 2026-08-10 ruling on this card (comment 5236141537, Option 1: the gates stay at own) is superseded by ruling B on #24646.

    • What the gates ask now. The sys_attachment parent gate and the sys_comment moderation gate ask one question about the parent: the security service's record-grained by-id update judgement of it (explain, admitting only allowed === true && record.visible === true). That judgement resolves the caller's depth on the parent object as the CRUD path does.
    • Where it landed. PR feat(service-storage, plugin-audit): the parent gates ask the parent's by-id update alone; explain composes the on-behalf-of write #24745, squash 1f949daeda. The governing paragraph in packages/spec/src/contracts/sharing-service.ts ("Write DEPTH is an input the CALLER supplies") is rewritten to say so.
    • Unchanged. A kernel whose security service serves no explain keeps the composition this card ruled: checkEdit at own, then the master-detail check.

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions