Skip to content

fix(plugin-security)!: a delegated operation runs only in an organization the delegator's membership backs - #25101

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25063-obo-insert-stamp
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25063-obo-insert-stamp

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25063

Clause-②: no (narrowing) — on a walled posture, an operation of an on-behalf-of context whose active organization the delegator's membership does not back is refused 403 PERMISSION_DENIED, where it used to run.

Seat call of record: 6312131621 on the card (option P, narrowing confirmed). The claim 6311248489 was widened in place to the prologue and the three pins' files. ⛔ Disclosure (#9387 ruling 5323310017): this body states the rule and an abstract reach only.

The rule

ADR-0090 D10 bounds a delegated operation by the delegator's scope. The operation runs in the live principal's active organization, and resolveDelegatorContext resolves the delegator in that same organization, assuming the agent and its delegator are in the same organization. Nothing checked that assumption.

The delegated-principal prologue (resolveOperationPrincipals, packages/plugins/plugin-security/src/security-plugin.ts) now checks it. It sits beside the dangling-delegator refusal and runs before any gate. It refuses 403 PERMISSION_DENIED, for every verb, when all three hold:

  • the tenancy posture enforces the organization wall (isolated or group);
  • the on-behalf-of context carries a live active organization;
  • vetOrganizationClaim(liveOrganization, delegator.accessible_org_ids, posture) answers undefined. This is the one existing spelling of "a membership backs this organization", called rather than copied.

No error code is added. The message names the organization and the delegator, and the developer message carries the [ADR-0090 D10] tag. This PR implements ADR-0090 D10 and does not edit the ADR.

Reach, abstractly

Reach was shown in process under both walled postures. No public door builds the refused shape: the /mcp OAuth door, the one producer of an on-behalf-of context, vets the request's organization against the same membership. Runtime's resolve-execution-context.oauth-organization.test.ts, which drives that producer into the real SecurityPlugin, stays green against the rebuilt dist/.

Boundary, before and after

  • Before: under isolated or group, an on-behalf-of operation in an organization the delegator is not a member of ran, answered by the gates the middleware composed for the two principals.
  • After: that operation is refused 403 PERMISSION_DENIED at the prologue, every verb, and nothing runs.
  • Unchanged:
    • the single posture, because vetOrganizationClaim answers the claim unchanged there;
    • an on-behalf-of context with no active organization, which keeps ADR-0123 D2's answers;
    • a context without onBehalfOf;
    • a dangling delegator, still refused as before;
    • system contexts, which never reach the prologue;
    • every context the /mcp door builds.

Pins

  • New: packages/plugins/plugin-security/src/delegated-organization-membership.test.ts (25 cases). It runs on real ObjectQL (source), a real SqlDriver and a real SecurityPlugin, with the delegator rebuilt from stored sys_user / sys_member / sys_user_permission_set rows.
    • Refusal cells (16): group and isolated × a public and a private object × a single insert, an array insert, a read, and an update plus a delete. Each asserts code: 'PERMISSION_DENIED', status 403, the [ADR-0090 D10] developer-message tag and the two subjects the message names. Each also asserts nothing stored or changed. The delegator holds the private object's grant in every organization involved, so the D10 grant intersection never answers for it.
    • Preservation cells (9): per walled posture, a delegated insert and read in an organization the delegator belongs to; the same caller without the delegation link; a delegated context with no active organization (ADR-0123 D2's refusal, by its tag); and a system context. Plus the single posture.
    • The file passes a no-op hook, so kernel:ready never fires, as other real-stack files here do. Without it, the plugin starts its platform bootstrap un-awaited, and a short case destroys the engine under that bootstrap's reads (knex Acquire connection error: aborted noise; no case failed).
  • Rewritten, because each built a delegated context in an organization the delegator does not belong to:
    1. tenant-layer0-verdict-on-operation.test.ts, "the recorded verdict is the INTERSECTION — an organization set neither wall names alone". The delegator's memberships go from org-2, org-3, org-9 to org-1, org-3, org-9, so they include the live org-1. The asserted walls and the recorded intersection move with them, from org-2, org-3 to org-1, org-3. That is still a set neither wall names alone. Its control case ("the SAME fixture without the delegation link") takes the same memberships so it stays the same fixture.
    2. Same file, "a delegator with NO membership makes the recorded verdict deny". A delegator with no membership backs no organization, so this case cannot move to an in-set organization. The caller now carries no active organization instead: the prologue has nothing to vet, and the composed wall's deny is still the subject.
    3. objects/default-row-scope-declarations.test.ts, persona "mcp agent for the platform admin". The fixture gains a sys_member row for p_admin in org_x, the organization every agent persona's request runs in. The expected rows are unchanged.
  • No other fixture went red. The full plugin-security suite is listed below. The one dependent test that drives an on-behalf-of context into the real SecurityPlugin is runtime's OAuth-organization file, and it is green.

Ablation

  • Method: the check was disabled (false && on its first conjunct) through node scripts/ablation-replace.mjs, in WRAP mode with its own restore. The subject is imported from source, so there is no dist/ leg.
  • Predicted before the run: the 16 refusal cells go red, and the 9 preservation cells and both rewritten files stay green.
  • Observed: Tests 16 failed | 63 passed (79), and the 16 are exactly the refusal cells. Each became an admitted operation, except where the walls composed for the two principals already hid the row (RECORD_NOT_FOUND on a group update).
  • Restore: the blob after restore equals HEAD (2f6dfd3fd761), and git diff HEAD is empty.

Tests and gates (head 3da7379b5)

  • plugin-security full suite: Test Files 205 passed (205) · Tests 4202 passed | 45 skipped (4247), VERDICT command-exit 0.
  • plugin-security typecheck (tsc --noEmit, the scripts project, and check:test-typecheck): exit 0, with 0 files / 0 errors / 0 pinned signatures in the test-typecheck debt ledger.
  • runtime: resolve-execution-context.oauth-organization.test.ts and http-dispatcher.test.ts, against plugin-security's rebuilt dist/ (the rule's text was confirmed in dist/index.mjs): Test Files 2 passed · Tests 264 passed.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 69 commands from 5 paths against merge base 4feccc268. All 69 exit 0, each exit captured before any pipe. pnpm check:dual-build-cjs-loads and pnpm check:i18n first exited 3 (PREREQUISITE NOT MET: no full build). After a full turbo run build both exit 0. --ran reads "69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN" and exits 0.
  • Not run locally: the dogfood on-behalf-of files, which build agent contexts with no active organization, so the rule is inert there by construction. They run on main's battery.

Acceptance notes

  • The rule lives at the operation prologue. Paths that resolve a delegator on their own (explain, the admission probes, a diagnostic) do not carry the refusal. They are listed for the seat in the dev report, kept off this PR per the disclosure ruling. No door builds the shape.
  • resolveDelegatorContext's comment ("the agent and its delegator are, by construction, in the same org") could point at the prologue check. explain-engine.ts is outside this card's file surface, so it is left as is.
  • origin/main moved after the merge base (28c9a2cb6, spec and driver-turso). It shares no file with this diff, so it is not merged; dispatch-gates reports that none of those commits touched what this derivation reads.

Generated by Claude Code

…tion the delegator's membership backs

The delegated-principal prologue (`resolveOperationPrincipals`) refuses
403 PERMISSION_DENIED, for every verb, when a walled posture is in force,
the on-behalf-of context carries a live active organization, and
`vetOrganizationClaim` says the delegator's own membership does not back it.
This implements ADR-0090 D10: a delegated operation is bounded by the
delegator's scope. `single` and contexts with no active organization keep
today's answers; system contexts never reach the prologue.

Three existing pins built a delegated context in an organization the
delegator does not belong to; they are rewritten to in-set organizations.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
…ort the file already carries

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
…no un-awaited bootstrap outlives a case

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
…only in an organization the delegator belongs to

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 2 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/field-level-security.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/permissions/index.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/ui/forms.mdx (via SecurityPlugin (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/releases/v18/18-0.mdx (via SecurityPlugin (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8f59304ddc45825f1f4d63f6956e07a75df68c9d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bf646ae246020f488000003aef460c46c4b3dd13 — the merge of head 3da7379b584bd9321d8eeed08360567fbb838103 into base 8f59304ddc45825f1f4d63f6956e07a75df68c9d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bf646ae246020f488000003aef460c46c4b3dd13 && git checkout bf646ae246020f488000003aef460c46c4b3dd13
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8f59304ddc45825f1f4d63f6956e07a75df68c9d 3da7379b584bd9321d8eeed08360567fbb838103 && git checkout -B drift-repro 8f59304ddc45825f1f4d63f6956e07a75df68c9d && git merge --no-ff 3da7379b584bd9321d8eeed08360567fbb838103

node scripts/docs-audit/affected-docs.mjs --json 8f59304ddc45825f1f4d63f6956e07a75df68c9d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8f59304ddc45825f1f4d63f6956e07a75df68c9d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3da7379b584bd9321d8eeed08360567fbb838103
Local-runs: none
Scope: packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/delegated-organization-membership.test.ts, packages/plugins/plugin-security/src/tenant-layer0-verdict-on-operation.test.ts, packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts + packages/core/src/security/resolve-authz-context.ts (vetOrganizationClaim, resolveUserAuthzGrants, the session arm), packages/plugins/plugin-security/src/explain-engine.ts (resolveDelegatorContext, buildContextForUser), packages/core/src/security/assemble-execution-context.ts, packages/core/src/security/api-key.ts (posture classification), packages/runtime/src/security/resolve-execution-context.ts, packages/runtime/src/security/resolve-execution-context.oauth-organization.test.ts, packages/plugins/plugin-security/src/errors.ts, packages/plugins/plugin-security/src/tenant-layer.ts, packages/spec/src/security/tenancy-posture.ts, packages/services/service-analytics/src/plugin.ts, docs/adr/0090-permission-model-v2-concept-convergence.md (D10), docs/adr/0123-no-active-organization-session-semantics.md (D2)

Rendered at 2026-10-21T10:15Z, on card #25063's body and all five comments, PR #25101's body, file list and net diff from the merge base 4feccc268 (byte-identical to the three-dot diff against current main: 5 files, +379/−5), and the head's check-runs. Ruling of record: seat call 6312131621 (option P) and the widened claim 6311248489. Disclosure (#9387, 5323310017): this record states rules and abstract reach only.

① Derived judgments

(a) What is refused now that was accepted — confined to the ruled shape: right. The one code change is a branch in resolveOperationPrincipals, inside the existing guard (the principal resolved at least one set, and the context carries the delegation link), on the resolved arm after the dangling-delegator refusal. It refuses when the context carries a non-empty active organization (tenantId) and vetOrganizationClaim(liveOrganization, delegator.accessible_org_ids, this.tenancyPosture) answers undefined. Read in @objectstack/core: that function returns the claim unchanged when the posture is absent or does not enforce a wall (postureEnforcesWall is posture !== 'single'), and otherwise answers the claim only while it is in the set. The set is the delegator's own: buildContextForUser resolves it through resolveUserAuthzGrants, which builds accessible_org_ids from the delegator's current sys_member rows, independent of the organization the grants are resolved in, and resolveDelegatorContext deliberately does not inherit it (ADR-0105 D2). So the refusal fires exactly for: a walled posture, a live active organization, a delegator whose membership does not back it — the ruled shape, no wider. It reaches both callers of the prologue: the engine middleware (every verb; a system context exits before it) and checkControlledByParentWrite, the attachment/comment parent gates, where the throw propagates unchanged as "a refusal of the context". The envelope is the existing PermissionDeniedError (code: 'PERMISSION_DENIED', status and statusCode 403), its message opens with the [Security] Access denied: matcher prefix every transport maps to 403, and the developer message carries the [ADR-0090 D10] tag; no error code is added. What the rule refuses is what the card read: left to run, the delegator's leg could not bound the operation, because its own wall was built from the inherited organization (isolated) or excluded it (group) while a row that named no organization was placed in the live one. A delegated principal that resolves no set is not vetted here and is refused at object admission as before (ADR-0056 D2), so nothing the rule would refuse is admitted on that path. One precision note on the prose, not the code: "before any gate" is the file's idiom for the CRUD, capability, FLS, RLS and wall gates; the engine-owned write guard and the ADR-0090 D12 delegated-admin gate run ahead of the prologue, as they did.

(b) The one real producer — judged from the code: no context the /mcp door builds is refused at the shipped default; right. assembleExecutionContext sets the delegator to the authenticated user itself (onBehalfOf.userId = authz.userId) and the active organization to authz.tenantId. The runtime door hands the token's bound organization to the resolver as the synthetic session's active organization, and the session arm of resolveAuthzContext keeps tenantId only after vetOrganizationClaim(tenantId, grants.accessible_org_ids, posture) for that same user, with the posture classified by the same expression the plugin uses (normalizeTenancyPosture(posture) ?? (isolationActive ? 'isolated' : 'single'), core api-key.ts and security-plugin.ts alike). The prologue then asks the same function about the same user's membership-derived set, resolved fresh (buildContextForUser passes bypassGrantsCache: true). With the grants cache off, which is the shipped default (OS_AUTHZ_GRANTS_CACHE_TTL_MS=0), both answers come from the same sys_member read, so the door cannot build the refused shape; the producer's pin (resolve-execution-context.oauth-organization.test.ts, the real resolver into the real middleware under isolated) stays green by construction. The one exception is a non-default configuration: with the cache on, the door may answer from a cached envelope while the prologue answers fresh, so a membership that ended inside the TTL yields a context the door built and the prologue refuses. That is the fail-closed direction, and the seat call carries that window on this card ("refused by the same check"). The PR body's "every context the /mcp door builds" and the changeset's "never refused by this rule" hold at the default and are overstated by that window only.

(c) The check calls vetOrganizationClaim and inlines nothing: right. The import exists at the merge base (security-plugin.ts:88); the net diff adds no import, no helper, no membership test of its own and no posture test of its own. Only the consequence (refuse) is the prologue's, as the API-key arm's consequence is its own.

(d) The three rewrites are honest, and each still pins what it pinned. Pin 1 (tenant-layer0-verdict-on-operation.test.ts, the recorded intersection): the delegator's membership set now contains the live organization while still differing from the caller's; the two asserted walls and the recorded verdict moved consistently with that, and the verdict is still a set neither wall names alone, which is the subject (the recording line intersects, rather than recording the caller's half). Its control case takes the same memberships and drops only the link, so it is still the same fixture minus the link. Pin 2 (same file, the delegator with no membership): a delegator with no membership backs no organization, so no in-set move exists; the caller now carries no active organization, which the prologue has nothing to vet. Under group the caller's half of the wall reads the membership union, not the active organization, so that half is what it was, and the delegator's empty set still makes the composed wall deny: the subject (the composed wall fails closed, the caller's half does not) is intact. Keeping the organization and asserting the new refusal instead would have changed the subject and duplicated the new file. Pin 3 (default-row-scope-declarations.test.ts, the platform administrator's agent): the delegator gains membership in the organization every agent persona's request runs in; its leg is governed by the superuser bypass, which membership neither widens nor narrows, so the expected rows are unchanged and the letter-B reading (a ceiling's silence is not Layer 1 silence) is pinned as before. The fixture now models a platform administrator who is a member, where it modelled one who belonged to nothing acting in an organization: the shape the ruling refuses. Each rewrite is named in the PR body, as the seat call required.

(e) The negative pins are ablation-verified: right, on the dev's run (not re-run here). The method is sound: the first conjunct gates the whole refusal, so false && on it through scripts/ablation-replace.mjs is the rule removed, with the direction predicted first and the restore proven by blob equality with HEAD. The predicted and observed red count, 16, is the file's refusal cells by construction (two walled postures × a public and a private object × four cells), and the file's design makes every refusal attributable to this rule alone: the delegator holds the private object's grant in the live organization too, so the D10 grant intersection cannot be what refuses, and the nine preservation cells pin the single posture, the no-active-organization context by ADR-0123 D2's own tag, the undelegated caller, the in-set delegated operation and the system context. The no-op hook that keeps kernel:ready from firing is stated in the file with its reason, and nothing the file pins reads what the platform bootstrap sets.

② Semver level

.changeset/25063-delegated-organization-membership.md: '@objectstack/plugin-security': minor, a fix(plugin-security)!: title, the Clause-②: no (narrowing) line identical to the PR body's, exactly one ADR-0087 marker, not-required (no-migration-prescription), and the BREAKING block (FROM, TO, Unchanged, the fix) in the #24813 shape. Level: right. A context that ran is refused and nothing widens, so this is a narrowing, BREAKING under 「甲」 rule 2 as amended (6303865025: a narrowing no conversion carries ships now, in 18.x, as a BREAKING minor); skip-changeset does not apply. Sentence by sentence against the code: the rule paragraph (the live organization, the delegator resolved in it on a stated assumption, nothing checked it, the prologue now checks it with vetOrganizationClaim under isolated or group) is true; the FROM is the pre-PR behaviour read in resolveDelegatorContext and the wall; the TO is the new branch's envelope; the Unchanged list is exact (the single posture, the no-active-organization context, a context without the link, the dangling delegator, system contexts); the fix sentence matches the branch (set tenantId to a backed organization, or leave it unset). Two precision notes, neither changing the level: "refused before any gate runs" reads as the file's idiom (the engine-owned write guard and the D12 gate precede the prologue), and "never refused by this rule" for the door's contexts holds at the default and is bounded by the grants-cache window (①b). The ADR-0087 arm is the honest one: no spec key, authorable spelling, object definition or stored shape moves, and no export is removed or narrowed (the diff adds one branch to a private method); the package publishes (no private flag, version 18.0.0); no ADR-0087 id is named. The PR does not edit ADR-0090, as the seat call required; the ADR's D10 text already states the rule the branch enforces.

③ Boundary flags

  1. The dev's out-of-scope finding — paths that resolve a delegator without this refusal. Judged in the cited code. Six are probes, diagnostics or explanations and are parity only, for the seat to file: describeDelegationNarrowing (decides nothing, never throws outward), delegatedWriteContexts (feeds plugin-sharing's per-record gate on the by-id write path, which only narrows, under a middleware whose prologue now refuses first), resolveProjectionFieldMask and getReadableFields (a column projection; the rows come through the middleware), canReadObject, canWriteObject, canExport (admission probes whose consumers then run the operation through the engine) and the explain engine. One is a data path and is escalated: getReadFilter → computeDelegatedReadFilter, the raw-SQL read-scope provider the analytics service bridges to (ADR-0021 D-C). It mirrors the middleware layer for layer but not its prologue: the delegator's leg is composed from the context resolveDelegatorContext rebuilds, whose Layer 0 input is the inherited active organization (computeLayeredRlsFilter reads context.tenantId and context.accessible_org_ids). Under group the delegator's union wall is its own membership set, so the live organization's rows are excluded. Under isolated the delegator's wall is the inherited live organization, so the provider answers a scope that admits that organization's rows where the middleware now refuses: the mechanism the card read, on a sibling door. Reach, abstractly: in-process only, the same population as the card; the analytics service receives the request's context from a door, and no door builds the shape. It does not make this PR's rule unsound for what the rule states and the ruling scoped (the engine's operation prologue, every verb), and it is pre-existing and unchanged by this diff; but it is not probe parity, because it serves rows, and it breaks the lock-step the provider's own doc promises between the find path and the raw-SQL path. The seat files it as its own card, ahead of the probe items; the mirror is one ask of the same function answering the deny sentinel, at the point the provider already mirrors the dangling-delegator refusal. No further detail here, per the disclosure ruling.
  2. resolveDelegatorContext's same-organization comment (explain-engine.ts, outside the surface): a pointer to the prologue check, not a rule; carried with the parity card. Answered.
  3. Deviations. Pin 2's shape: judged honest in ①(d). origin/main moved after the merge base by four commits (spec, driver-turso, lint, the spec-shape gate); the diff of the merge base against current main over plugin-security, core/src/security and runtime/src/security is empty, so nothing this diff reads moved. Answered.
  4. The grants-cache window (round-1 finding 1): carried by the ruling on this card; the code confirms the refusal there is fail-closed (①b). Answered.
  5. Fixture-realism drift (round-1 finding 2): the new pin models the delegated principal as a user distinct from its delegator, which is the in-process population the rule targets, so it is apt here; the observation stands as noted, no card. Answered.
  6. open_questions: none in the round-2 report; the round-1 question was answered by the seat call, and the PR implements option P as ruled (the prologue, no step 3.7, no assertControlledByParentWrite, no objectql, no spec, no ADR edit).
  7. Disclosure: the card, the PR body, the changeset and this record carry rules and abstract reach only. The new pin file lands with the fix and pins the refusal, not the unfixed state.
  8. Check-runs on the head, read at 2026-10-21T10:11Z: 40 runs. Green (27): Check Changeset, Governed Surface Queue Guard, Lint & Repo Gates, Lint · PM dispatch-gates, Lint · ESLint, Lint · CI, release & engine gates, Lint · docs & code guards, Lint · repo & tooling gates, the three scoped lints, Type Check · source, consumer, debt ledger and workspace, TypeScript Type Check, Build Core, Dogfood Regression Gate, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Auto Label, filter, and the four claim/card guards. Skipped (7, conditional or opt-in): Temporal Conformance, Build Docs, Console Pin Gate, the Dogfood Regression Gate shards, Dogfood Verify CLI, Packed-tarball smoke, 18.x upgrade harness. pending (6): Test Core (1/6), (2/6), (3/6), (4/6), (5/6), (6/6), the derived test family that holds the plugin-security suite. This verdict is the code's; a red shard re-opens it.

Implemented-by: claude/issue-25063-obo-insert-stamp
Reviewed-by: session_01UmqzD97e14iws6hmVhgZ5y

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #25063 (option P) · domain:services seat 2 · os-support-ai · session_01UmqzD97e14iws6hmVhgZ5y · 2026-10-21T10:29Z · head 3da7379b58.

  1. Form:
  2. Premise and fix:
    • Reach was measured first, in process, under both walled postures. No door builds the shape.
    • On a walled posture, the delegated-principal prologue now refuses 403 PERMISSION_DENIED for every operation of an on-behalf-of context whose live active organization the delegator's membership does not back. It goes through vetOrganizationClaim, with no copy and no new code. This is ADR-0090 D10, enforced; the ADR is not edited.
    • single, contexts with no active organization, undelegated contexts and system contexts are unchanged.
    • The review read the one real producer (the /mcp door) and found that no door-built context is refused at the shipped default. The non-default grants-cache window is the only exception, and it fails closed, as ruled on this card.
    • Pins: 25 real-stack cases (16 refusals, 9 preservations). The three rewritten pins are judged honest, each named in the PR. Removing the rule's first conjunct turns exactly the 16 refusal cells red; restored to blob == HEAD.
  3. Gates:
    • CI on 3da7379b58, latest run per check name: 41 checks, 34 success, 7 skipped, 0 failed. Every skip is on the roster in scripts/pm/check-expected-skips.mjs: Temporal Conformance, Build Docs, Console Pin Gate, the Dogfood Regression Gate matrix, Dogfood Verify CLI, Packed-tarball smoke and the 18.x upgrade harness. All six Test Core shards, Build Core, TypeScript Type Check, Lint & Repo Gates, Check Changeset and the closing-target guards are success. mergeable_state is clean.
    • Local: 69 derived, 69 run, 0 NOT-MEASURED. The plugin-security suite: 205 files, 4202 passed. Typecheck and check:test-typecheck are OK. The runtime producer's pins and http-dispatcher (264 passed) ran against the rebuilt dist.
  4. Accepted deviations:
    • Pin 2 drops the active organization instead of moving it in-set, because an empty membership backs no organization. The review judged it honest.
    • The review's two precision notes: "before any gate" is the file's idiom, and "never refused" is bounded by the cache window.
    • origin/main moved after the merge base. The diff over plugin-security, core/src/security and runtime/src/security is empty.
    • The model-free commit trailers, per AGENTS.md.
  5. Out of scope:
  6. Landing: ordinary queue landing. Every check on this head is green or an expected skip, so the seat flips it on this record. It closes plugin-security / objectql: an on-behalf-of insert by a crossing platform operator that names no organization may be stamped outside the delegator's organization set (source reading only; measure reach first) #25063.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants