Skip to content

ISecurityService gains a fail-closed authored-row-write verdict (contract + plugin-security implementation) — #5493 ruling, step 1 #6735

Closed

Description

@os-zhuang

Part of #5493 (maintainer ruling 2026-08-08, recorded in #5493 comment 5226377985 and the chat ruling it links: Q1 = A, Q2 = A1).

Why (one measurement carries the whole card)

#5493's fix requires the sharing middleware to defer, before hard-refusing a by-id write, to "an app-authored RLS update-widener admits this row by declaration". No reachable surface answers that today, and the cheap proxy is measurably a security hole: probe E-A (#5493 comment 5226364929) shows a creator-but-no-longer-owner is admitted by the platform created_by floor and refused by sharing with an envelope byte-identical to #5493's — a deferral keyed on "composed RLS admits" would hand transferred records back to their former creators. Separating the two requires policy provenance (isPlatformOwnershipFloorPolicy), which is deliberately private to plugin-security (platform-ownership-policies.ts, ADR-0105 D3 keying).

What (ruled shape — Q1 = A, Q2 = A1)

packages/spec/src/contracts/security-service.ts (ISecurityService) gains a verdict-shaped, by-id method:

  • checkAuthoredRowWrite(object, recordId, operation, context) returning 'admit' | 'abstain' (exact name/signature per contract conventions — implementer's call, trade-offs in the PR body);
  • admit iff at least one applicable non-floor (app-authored) RLS policy matches the row for this operation;
  • abstain in every other case — including a missing method, an unresolvable probe, or a throw. Fail-closed: a deployment without this method behaves byte-for-byte as today. (Same discipline feat(sharing): ISharingService 的每行写判定补三态(放行/不表态/拒绝)(#6428) #6564 pinned for SharingWriteVerdict: a failure is never a widening.)
  • packages/plugins/plugin-security/src/security-plugin.ts implements it on the registered service, reusing the private provenance split fix(plugin-security): enforce both declared write-wideners; the platform baseline becomes explicit-allow (#5492, #5491) #6684 landed (computeRlsFilter internals / RlsFilterOptions.dropPlatformOwnershipFloor / platform-ownership-policies.ts) — no second implementation of RLS evaluation anywhere.
  • Semantic pin the implementation must carry (the E-A hole, as a test): a principal matching only the platform floor (creator of a since-transferred row, no authored policy) gets abstain, never admit.
  • Q2 note, ruled: verdict-shaped by-id ONLY. The bulk-path half of the family is deliberately excluded and tracked separately (see the sibling card filed alongside this one); do not widen this surface toward filters.

Ordering (contract-first, the #6428 chain shape)

This card is the head: this lands → #5493 step 2 (plugin-sharing consumption + the control-pin flip in row-write-widener-composition.test.ts) dispatches in the identity lane. #5493 carries Blocked-by: pointing here.

Boundaries

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    Triage: domain:spec + target:v17 added (pm:queue kept as filed), with a cross-domain exception designation.

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


    Generated by Claude Code

  2. self-assigned this
    on Aug 8, 2026
  3. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1 (domain:spec seat, #6017)
    Session: session_011sGk4SKHqGRgmmqUok1P8M
    Branch: claude/issue-6735-authored-row-write-verdict
    Worktree: objectstack-issue-6735
    Domain: domain:spec — cross-domain exception card, triage-designated single claimant (comment 5226530686, #6428 precedent; one card, one PR)
    File surface: packages/spec/src/contracts/security-service.ts (+ contract barrel/exports as needed); packages/plugins/plugin-security/src/security-plugin.ts + plugin-security tests (incl. the E-A semantic pin); generated api-surface artifacts as driven by regen. ⛔ No plugin-sharing edits — that is #5493 step 2, identity lane. (Stop on breach; explain in the report.)
    Serial constraints cleared: targeted in-flight check run per the exception path — domain:identity in-flight: none open; domain:spec in-flight: #6483 / PR #6608 (permission whitelist surface, disjoint) and #4001 (umbrella, no file work). Same-file deferral: #6622 (ISecurityService, different method) stays queued strictly behind this card — serial constraint recorded there.
    Container assessment: M, mode:subagent shared container (scoped filters, heavy phases under the verify lock).
    Premise re-verified at origin/main e1e7629: checkAuthoredRowWrite zero hits repo-wide (neighbor probe positive); #6684 internals (platform-ownership-policies.ts, dropPlatformOwnershipFloor) present.


    Generated by Claude Code

  4. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT → PR #6841 (flipping ready + auto-merge now; ESLint and TypeScript Type Check conclusions verified success by this seat, 23 success + 2 conditional-skip, zero failures).

    Reviewed against the diff and the ruled shape (Q1=A / Q2=A1): optional checkAuthoredRowWrite?(object, recordId, operation, context) → 'admit' | 'abstain' on ISecurityService (+ named types, api-surface regen carries exactly the two added type exports, 0 breaking); plugin-security implements it off the SAME computeLayeredRlsFilter + dropPlatformOwnershipFloor knob — no second RLS evaluator, per the mechanism constraint. The E-A semantic pin is present and was proven live in BOTH directions (floor re-included ⇒ pin red; fully-naive deferral ⇒ both E-A pins red) — a pin that can fail. OPTIONAL over required argued against the #6428 counter-precedent in the PR body: consistent with the contract's own feature-detect header and rest-server.ts's Partial typing — accepted. ⛔ plugin-sharing untouched, verified in the file list.

    Repricing consumed (recorded for the two waiting cards):

    Landing: released FIRST in this round's serial relay (v17 chain head); PR #6840 holds behind it.


    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