Skip to content

feat(platform-objects)!: the identity objects declare their default row scope, and the registry refuses an extend that sets one - #24623

Merged
objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-24341-identity-default-row-scope
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/issue-24341-identity-default-row-scope

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24341
Clause-②: no (narrowing)

Under ruling E′ (6250748677 on #24164) and ruling A′ (6261536443 on #24449). This resumes the branch from 1bf6312ed9 (release 6259391225), with main merged three times (merge commits only, no rebase; the last at bfc1539fe), under claim 6267997774. Fix round 1 builds ruling B (6278784335).

What changes

  • @objectstack/platform-objects and @objectstack/plugin-audit. The 23 identity objects ruling E′ item 3 names now declare defaultRowScope. Each value is the policy the shipped permission sets carry for that object, moved verbatim. sys_audit_log's declaration lives in plugin-audit, which owns the ledger (ADR-0029 K2).
  • @objectstack/objectql registry. An extend contribution that carries defaultRowScope is refused at registerObject, whatever its value and whatever its target. The answer is INVALID_METADATA, status 422, and the error is named ExtensionDefaultRowScopeRefusedError. A contribution without the key registers as before.
  • @objectstack/plugin-security. Under ruling 6278784335 (letter B), collectRLSPolicies does not add an object's declared default on a delegated principal's own (ceiling) leg; the delegator's leg collects it as any principal's does, and a principal that acts for no one keeps ruling E′ item 2 whole. The test is the read door's own (onBehalfOf.userId), in an unexported module function; the method takes the context it collects for as a private parameter, threaded from its six callers. No export moves.
  • The declarations' comments now say that the collection applies the key. plugin-security: collectRLSPolicies applies an object's declared default row scope when no held set carries a policy for it — the platform identity objects stop depending on every set copying the same literals (#24164 ruling E′) #24340's reader is on main, so the old "not read yet" was false.
  • Changesets. Three changesets. Two are minor and BREAKING under the launch-window convention, each with Clause-②: no (narrowing) and an adr-0087: not-required (no-migration-prescription) marker, which check-adr-0087-registration accepts. The third, 24341-delegated-ceiling-no-object-default.md, covers plugin-security, patch, Clause-②: no: measured against 18.0.0-next.5, which shipped the reader, it only restores to a delegated agent the rows and explain it had before any object declared a default.
    • 24341-identity-objects-default-row-scope.md covers platform-objects and plugin-audit.
    • 24341-extend-default-row-scope-refused.md covers objectql.

Measured on the merged tree

1. The branch's pins after merging main. The enumerating pin (28 tests) and the refusal pin (6 tests) did not move. The plugin-security suite then had 3 tests red out of 3,827. Two pins outside the branch moved, and each was corrected to stay load-bearing:

2. sys_user's one || policy on every user-context producer (service-automation/src/sys-user-default-row-scope.integration.test.ts).

  • The stack. The real ObjectKernel, ObjectQL with driver-sql, the identity objects as declared, the real SecurityPlugin, and the real AutomationEngine. No baseline is composed. usr_me holds one authored '*' read set with no row policy, so the declared default is what applies. Explain names sys_user_self_or_org_members as the object default.
  • The fixture. usr_me is in organisations A and B and acts in A. usr_peer is in A. usr_other is in B only. usr_loner is in none.
  • The check. Each producer must read the user's own row plus A's members. Each read's captured context must carry org_user_ids. Each must read exactly the rows the two shipped literals admit together on that same context.
producer acting in A acting in no organisation
request (resolveUserAuthzGrants, as resolveAuthzContext copies it) usr_me, usr_peer usr_me
runAs: 'user' flow run (#24495) usr_me, usr_peer usr_me
runAs: 'user' hook fired from a hand-built { isSystem: true, userId } (#24518) usr_me, usr_peer usr_me

In every row the captured org_user_ids is an array (['usr_me'] with no organisation), and the literals' union equals the served rows.

The one edge is a grants-resolver failure. A flow run suspended before #24495 shipped is in the same class. Either one leaves the context with no org_user_ids. The merged predicate then admits [], own row included. The two literals apart would admit [u_me]. The unit pin on the branch measures this divergence. Record 6265937412 ① (hooks) and review 6263414965 ③ (flows) accepted it. It bites only where the default applies, never for a holder of the shipped baseline (see 3).

No escalation. I found no other context that reads sys_user with no org_user_ids. The producers of isSystem: false contexts are core's assembleExecutionContext, the sharing plugin's { ...authz }, explain's buildContextForUser, resolveRunDataContext and withRunAs('user') with its seeding. Each one carries org_user_ids from the core resolver, or seeds it.

3. #24340's review ③.11: a deployment with the shipped baseline, on the real declarations (plugin-security/src/objects/default-row-scope-declarations.test.ts).

  • Setup. The real SecurityPlugin with the shipped sets and the platform baseline composed, booted twice: once with all 23 declarers as declared, and once with the key dropped. The fixture binds a security catalog of the shipped sets, and the delegators hold real grants (the viewer viewer_readonly, the organisation admin organization_admin, the platform administrator admin_full_access), so each delegator leg resolves as in production. Nine personas: the five humans (a member, a viewer, an organisation admin, a user in no organisation, the platform admin), and an MCP agent (mcp_agent_data_read, the producer's shape) acting for the member, the viewer, the organisation admin and the platform administrator.
  • Rows. Every persona reads every declarer byte-for-byte as before. For each delegated agent the served ids are also asserted positively on both trees; the platform administrator's agent reads every row of sys_user, sys_session and sys_scim_user on both.
  • Explain. read explain is byte-for-byte as before for every persona, the four delegated agents included.
  • Census. No persona collects a declared default on any of the 23 objects for any operation: each human holds member_default, which carries a policy for each declarer on every operation, and a delegated agent's own leg collects none (letter B). CONTROL: the same agent context with no delegator (a hand-built shape no producer emits) collects each declarer's default on exactly the operations it names (all four on 8 objects, select on the other 15).
  • The superuser bypass is decided before the collection, so the platform admin collects nothing.
  • Cost. The comparison takes 6.3 s of the file's 6.6 s test time on the shared box. Explain is compared for read only, because the census covers the collection for all four operations, and the collection is the one source every door and every explain composes from.

4. The extend rule on the real tree.

  • Census by grep. Of every objectExtensions entry and every registerObject(..., 'extend') caller in packages/ and examples/, the one extend of a platform object is @objectstack/plugin-auth's sys_sso_provider extension. The other two extend showcase_account and dc_account.
  • Booted. The integration test boots the composition and lists every extend contributor the registry holds: exactly com.objectstack.plugin-auth → sys_sso_provider, folded, with oidc_client_secret present.
  • Refused by name. A contribution that sets defaultRowScope on sys_user in that same booted registry is refused: INVALID_METADATA, 422, ExtensionDefaultRowScopeRefusedError. sys_user's declared default stands, and the contributor is not recorded.

5. Public surface.

  • No new symbol on either objectql entry. The built dist/index.js and dist/core.js expose neither EXTENSION_DEFAULT_ROW_SCOPE_REFUSED_CODE nor ExtensionDefaultRowScopeRefusedError, with SchemaRegistry present as the positive control. Both .d.ts entries name neither.
  • The error class is unexported. The code constant is a module-level export of registry.ts, read by its own pin only, and no entry re-exports it.
  • No new public member on an exported class.

Ablations (each committed first, then restored with git checkout HEAD, blob and porcelain checked)

  1. sys_user's declaration with the peer arm removed. using became id == current_user.id && id != null. I rebuilt platform-objects, and ablation-dist-preflight found the marker in 6 dist files. The integration pin went 4 failed / 6 passed: the premise test, plus the acting-in-A case for each of the three producers. The no-organisation cases stayed green, as expected. After the restore and rebuild, the marker was absent from all 66 dist files and the run was 10 / 10.

  2. The ③.11 pin's teeth. I deleted the sys_scim_user_self literal from scimProjectionRowScope, which simulates a spread removed while the default carries the row scope. The declarations file went 3 failed / 27 passed: the literal pin, the byte-for-byte rows, and the census. After the restore, the blob matched HEAD, the porcelain was empty, and the run was 30 / 30.

  3. The B guard removed (isDelegatedPrincipalLeg dropped from the empty-collection return). The declarations file went 3 failed / 28 passed: the byte-for-byte rows and explain, the delegated-agent pin (mcp agent for the platform admin on sys_user, after: expected [ 'p_admin' ] against all six), and the census. Restored: blob d31b3ba9c4 == HEAD, porcelain empty, 31 / 31.

The refusal's own ablation is the branch's (release 6259391225): 4 red and 2 green, then restored.

Pin sweep

Verification record

Fix round 1, head f1049494f (after merging main at bfc1539fe):

  • plugin-security 189 files / 3,785 tests (+45 skipped); platform-objects 78 / 1,192; plugin-audit 57 / 930; objectql 449 / 8,453.
  • The service-automation integration (the new pin and both twins): 19 / 19, against a rebuilt plugin-security dist.
  • Core test:repo 6 / 82.
  • Typecheck exit 0 for plugin-security, platform-objects, objectql, plugin-audit and service-automation.
  • Gates: 87 derived, 87 run, all exit 0; --ran reports a derived zero.
  • main has moved since bfc1539fe; git merge-tree against it is clean.

Round 0, head efaf66a51 (after the second merge of main at 5a871e4e4):

  • Builds. pnpm turbo run build --concurrency=2 --filter='!@objectstack/docs': 76 / 76.
  • Typecheck, exit 0 for each of objectql, platform-objects, plugin-audit, plugin-security and service-automation. service-automation's tsc --listFiles includes the new integration test. plugin-security and objectql exclude tests from tsc, and their check:test-typecheck passed inside the same script.
  • Tests, exit 0:
    • objectql: 447 files, 8,405 tests;
    • platform-objects: 77 files, 1,184 tests;
    • plugin-audit: 56 files, 898 tests;
    • plugin-security: 189 files, 3,784 tests, 45 skipped;
    • service-automation: 192 files, 2,556 tests;
    • pnpm --filter @objectstack/core test:repo: 6 files, 82 tests.
  • Gates. node scripts/pm/dispatch-gates.mjs --commands with no paths derives 87 families, and all 87 ran on efaf66a51, each exit 0, captured before any pipe. --ran reconciles: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero. On the first head, two families exited 3 (PREREQUISITE NOT MET), check:dual-build-cjs-loads and check:i18n. After the full build both exit 0.
  • Ratchets fixed on this branch before the PR.
    • check:query-options-erasure, test surface 236 to 237: one as any on a find options bag in the new test.
    • check:slot-lookup: one getService lookup typed any.
    • Both are now typed, and both ratchets hold at their baselines.

Acceptance notes

  • Contract review. CONTRACT_REVIEW_TIER record 6273333026 was FAIL on efaf66a51 (a delegated platform administrator's agent narrowed, undeclared). The maintainer ruled B (6278784335). Delta record 6280315230 is PASS on f1049494f.
  • Packages beyond the claim's two. @objectstack/plugin-audit is bumped in the declarations changeset, because sys_audit_log's declaration ships there.
  • Pin locations outside the claim's file surface. These are pins only, and no source outside the fence moved:
    • the new integration test in service-automation, which is the package that composes request, flow and hook on one stack, like its two twins;
    • the two corrected plugin-security pins in 1 above.
  • private-credential-row-scope.test.ts also carries an agent persona with userId: 'agent_x', a shape no producer emits. It passes, because no row is readable there under either reading, so it was left as is.
  • The second merge of main brought in Changesets pre-mode exit (c46b5f14c). The changesets' "pre mode" aside was dropped to keep them true.

Generated by Claude Code

claude added 10 commits October 19, 2026 03:23
…w scope

The 23 identity objects carry the row policy plugin-security's shipped
permission sets spread for them today as ObjectSchema.defaultRowScope:
each literal moves verbatim, and sys_user's two (self, organization
members) become one select policy joined with ||. sys_audit_log lives in
plugin-audit and declares there. No reader yet: the sets keep their
spreads, so no served row moves.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJVtc4cmB5EEgMNC6Si9db
…ies defaultRowScope

An object's default row scope is the owner's declaration alone; an extend
contributor that sets or loosens it is refused at registerObject, before
the contributor list opens, as INVALID_METADATA 422 named
ExtensionDefaultRowScopeRefusedError. A contribution that leaves the key
alone registers as before.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJVtc4cmB5EEgMNC6Si9db
… scope against the shipped literals

Enumerates the 23 declarers against defaultPermissionSets (same name,
operation and predicate per object; platform-global objects declare
none), pins sys_user's merged predicate to the union of its two literals'
rows, and records the measured divergence: with org_user_ids unresolved
the merged policy fails closed whole.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJVtc4cmB5EEgMNC6Si9db
…own declarations

object-default-row-scope.test.ts: the undeclared shape now drops the key
the identity objects declare, so its CONTROL still measures no default.

scim-projection-row-scope.test.ts: the MCP agent persona carries the
delegating human's userId, as core's assembleExecutionContext sets it for
an OAuth agent, so current_user resolves to the delegator on the ceiling's
leg, where each SCIM table's declared default now applies.

Claude-Session: https://claude.ai/code/session_01FatVyDAFEST48jZDQcbheU
Co-authored-by: Claude <noreply@anthropic.com>
…the baseline unchanged on the real declarations

- service-automation: a booted stack with no baseline and one authored
  '*' read set reads sys_user under its declared default on a request, a
  runAs:'user' flow run and a runAs:'user' hook fired from a hand-built
  system context: its own row plus the acting organisation's members, and
  exactly the rows the two shipped literals admit on that same context.
  On the same composition, plugin-auth's sys_sso_provider extension (the
  one shipped extend of a platform object) still registers, and an extend
  that sets defaultRowScope is refused by name.
- plugin-security: with the shipped baseline, every persona (member,
  viewer, organization admin, a user in no organisation, the platform
  admin, and an MCP agent for each of three delegators) reads all 23
  declarers exactly as without the declarations; the census names where
  a default is now collected: on the agent ceilings' legs only.
- The declarations' comments now say the collection applies the key.

Claude-Session: https://claude.ai/code/session_01FatVyDAFEST48jZDQcbheU
Co-authored-by: Claude <noreply@anthropic.com>
…nd the registry's extend refusal

Claude-Session: https://claude.ai/code/session_01FatVyDAFEST48jZDQcbheU
Co-authored-by: Claude <noreply@anthropic.com>
…ns and security service lookup

Claude-Session: https://claude.ai/code/session_01FatVyDAFEST48jZDQcbheU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/objectql, @objectstack/platform-objects, @objectstack/plugin-audit, @objectstack/plugin-security, touching 61 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-security/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 92ce3af404a9b6ca16881a89f2e841d29da52175.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-security/vitest.config.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: packageId (4 routes)
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 39 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 35 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 92ce3af404a9b6ca16881a89f2e841d29da52175 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d6bdf57d6d9781b35f6b1b15fb7ee12d48226586 — the merge of head f1049494f97c57495388bab9282c65c3e6aa0581 into base 92ce3af404a9b6ca16881a89f2e841d29da52175, 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 d6bdf57d6d9781b35f6b1b15fb7ee12d48226586 && git checkout d6bdf57d6d9781b35f6b1b15fb7ee12d48226586
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 92ce3af404a9b6ca16881a89f2e841d29da52175 f1049494f97c57495388bab9282c65c3e6aa0581 && git checkout -B drift-repro 92ce3af404a9b6ca16881a89f2e841d29da52175 && git merge --no-ff f1049494f97c57495388bab9282c65c3e6aa0581

node scripts/docs-audit/affected-docs.mjs --json 92ce3af404a9b6ca16881a89f2e841d29da52175

⚠️ 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 92ce3af404a9b6ca16881a89f2e841d29da52175 → 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: efaf66a5138a35245ce0dc2ccb7b5eea0e02d794
Local-runs: none
Scope: .changeset/24341-extend-default-row-scope-refused.md, .changeset/24341-identity-objects-default-row-scope.md, packages/objectql/src/registry-extension-default-row-scope-refused.test.ts, packages/objectql/src/registry.ts, packages/platform-objects/src/identity/sys-account.object.ts, packages/platform-objects/src/identity/sys-api-key.object.ts, packages/platform-objects/src/identity/sys-device-code.object.ts, packages/platform-objects/src/identity/sys-jwks.object.ts, packages/platform-objects/src/identity/sys-oauth-access-token.object.ts, packages/platform-objects/src/identity/sys-oauth-application.object.ts, packages/platform-objects/src/identity/sys-oauth-consent.object.ts, packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts, packages/platform-objects/src/identity/sys-organization.object.ts, packages/platform-objects/src/identity/sys-scim-connection-binding.object.ts, packages/platform-objects/src/identity/sys-scim-group-member.object.ts, packages/platform-objects/src/identity/sys-scim-group.object.ts, packages/platform-objects/src/identity/sys-scim-identity-tombstone.object.ts, packages/platform-objects/src/identity/sys-scim-projection-grant.object.ts, packages/platform-objects/src/identity/sys-scim-subject.object.ts, packages/platform-objects/src/identity/sys-scim-user.object.ts, packages/platform-objects/src/identity/sys-session.object.ts, packages/platform-objects/src/identity/sys-team-member.object.ts, packages/platform-objects/src/identity/sys-two-factor.object.ts, packages/platform-objects/src/identity/sys-user-preference.object.ts, packages/platform-objects/src/identity/sys-user.object.ts, packages/platform-objects/src/identity/sys-verification.object.ts, packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts, packages/plugins/plugin-security/package.json, packages/plugins/plugin-security/src/object-default-row-scope.test.ts, packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts, packages/plugins/plugin-security/src/scim-projection-row-scope.test.ts, packages/plugins/plugin-security/vitest.config.ts, packages/services/service-automation/src/sys-user-default-row-scope.integration.test.ts, pnpm-lock.yaml + packages/plugins/plugin-security/src/security-plugin.ts (collectRLSPolicies at :9018, admitCollectedPolicy, computeLayeredRlsFilter at :7143 with its derive-from-select branch at :7369, the read door's two-leg $and at :4171–:4205, resolveOperationPrincipals at :8291, resolvePermissionSetsForContextUnmemoized at :6504, getObjectSecurityMeta at :9116), packages/plugins/plugin-security/src/object-default-row-scope.ts, packages/plugins/plugin-security/src/platform-tenant-policies.ts (isPlatformTenantPolicy, provenance key), packages/plugins/plugin-security/src/rls-compiler.ts (getApplicablePolicies), packages/plugins/plugin-security/src/permission-evaluator.ts (resolveObjectPermission, hasSuperuserReadBypass), packages/plugins/plugin-security/src/explain-engine.ts (buildContextForUser, resolveDelegatorContext, the 2026-09-08 depth rule docblock at about :805–:830, the delegated composition at about :2098–:2120), packages/plugins/plugin-security/src/objects/default-permission-sets.ts (every row policy of organization_admin, member_default, viewer_readonly, the three literal families, BETTER_AUTH_MANAGED_OBJECTS, the three MCP ceilings), packages/plugins/plugin-security/src/private-credential-row-scope.test.ts (the agent_x persona), packages/spec/src/identity/eval-user.zod.ts (ADMIN_FULL_ACCESS_CAPABILITIES), packages/spec/src/security/rls.zod.ts (DefaultRowScopeSchema, RLSOperation), packages/spec/src/data/object.zod.ts (ObjectExtensionSchema at :3974), packages/objectql/src/engine.ts (registerApp's objectExtensions copy at :8442–:8458, ScopedContext[SEED_USER_GRANTS] at about :21849–:21890), packages/objectql/src/hook-wrappers.ts (installRunAsApi, the failure arm), packages/objectql/src/index.ts and packages/objectql/src/core.ts (the two entries), packages/objectql/package.json (exports), packages/core/src/security/resolve-authz-context.ts (resolveAuthzContextInScope, resolveUserAuthzGrants, grantAppliesInTenant), packages/core/src/security/assemble-execution-context.ts (entryFields, the agent principal at :316–:339), packages/mcp/src/plugin.ts (resolveStdioExecutionContext), packages/services/service-automation/src/engine.ts (resolveRunContext at :6395, the seed and its catch at :6440–:6485, resume at :7212), packages/services/service-automation/src/runtime-identity.ts, packages/services/service-automation/src/plugin.ts (the grants bridge), packages/plugins/plugin-sharing/src/sharing-plugin.ts (:1011), packages/plugins/plugin-auth/src/manifest.ts (authObjectExtensions), examples/app-showcase/src/data/extensions/account.extension.ts, packages/qa/downstream-contract/src/additional-domains.fixtures.ts, packages/plugins/plugin-audit/package.json, packages/platform-objects/package.json, .changeset/pre.json, scripts/check-adr-0087-registration.mjs (the marker parse at :1990–:2013), .github/workflows/pr-automation.yml (WHICH LEVEL at :754, the launch-window guard at :1077), .github/workflows/ci.yml (the Test Core shard job at :505), scripts/ci/select-shard-packages.sh, AGENTS.md (:460–:463, attribution), .claude/agents/os-dev.md (:50–:62, out-of-scope findings).

Net diff judged: git diff 5a871e4e41b110d5d4b0cdcc325a75723740bb31 efaf66a5138a35245ce0dc2ccb7b5eea0e02d794 (the merge base with main), 34 files, +1452 −2, matching the PR's file list. Inputs read at 2026-10-19T17:26Z: card #24341 (body and all 17 comments, the eight the brief binds included; both os-dev-reports read as claims), ruling E′ 6250748677 and rider 6255156624, ruling A′ 6261536443, #24340's review of record 6260875372, PR #24495 (#24448) with its review 6263414965 and seat check 6263430213, PR #24518 (#24497) with records 6265937412, 6267161870 and the seat's ACCEPT 6267178743, #24484 with its triage 6261582931, PR #24623's body, file list and bot comment 6271971271, and the 41 check-runs on this head, all completed at the read, none running.

The head's check-runs. 34 success, 5 skipped (Dogfood Regression Gate matrix, Dogfood Verify CLI, Console Pin Gate, Packed-tarball smoke, Temporal Conformance), 2 failure: Test Core (1/6) and the aggregate Test Core. The shard's log ends Process completed with exit code 143 and The runner has received a shutdown signal at 2026-10-19T16:28Z, inside the turbo build step and before any test ran; the aggregate's annotations read 1 of 6 declared shard(s) of test published no positive attestation (test-1-of-6) and a declared negative is never overridden by a full roster. The shard's plan, printed in its own log, scheduled spec, core, plugin-email, service-datasource, connector-rest, types, knowledge-memory, account, example-multi-package, setup and studio; none of the five packages this diff edits is on it, and all five are in the run's scope, so their suites ran on shards 2 to 6, each success (5/6 is the objectql shard: 448 files, 8420 tests passed, which includes the new registry pin). On the PR's previous head 5a871e4e4, whose tree differs from this one by two changeset lines only, all six shards were success. The gate verdict on THIS head is nonetheless red, and this record does not override it: see ③. Lint & Repo Gates, every Type Check · job, TypeScript Type Check, Build Core, Check Changeset (both ADR-0087 exemption notices printed) and Governed Surface Queue Guard are success.

① Derived judgments

1. The 23 declarations — RIGHT on the set, RIGHT on 22 operations, and sys_user's select is the seat's accepted exception. The declarers are exactly ruling E′ item 3's list: the seven sys_scim_* projections, sys_verification and sys_jwks, the thirteen of the _self / _org_members list, and sys_audit_log (in plugin-audit, which owns the ledger under ADR-0029 K2). Each of the 22 non-merged declarations is member_default's literal for the object with object dropped, read against default-permission-sets.ts at this head: select with user_id == current_user.id on the four SCIM _self tables, sys_account, sys_team_member, sys_oauth_access_token and sys_oauth_refresh_token; select with id == null on sys_scim_group, sys_scim_group_member, sys_scim_connection_binding, sys_verification and sys_jwks; all with user_id == current_user.id on sys_session, sys_two_factor, sys_user_preference, sys_api_key, sys_device_code, sys_oauth_consent and sys_oauth_application; all with id == current_user.organization_id on sys_organization; select with tenant_id == current_user.organization_id on sys_audit_log. organization_admin carries the same literal for each, and viewer_readonly the same predicate under select (pinned per object). No identity object is missing and no platform-global object declares one: the pin enumerates every object @objectstack/platform-objects and AuditPlugin register and asserts the declarer set equals the 23 and that sys_metadata, sys_job, sys_migration and sys_platform_setting carry none (sys_presence and sys_flow_dispatch live outside its import set, as the first report said; the tree-wide grep of defaultRowScope finds no declarer outside the 23 and the fixtures). sys_user is select with id == current_user.id || id in current_user.org_user_ids, where member_default spells sys_user_self as all; the release 6259391225 accepted select on the dev's reasoning (the peer arm must not reach a write). One reading to put on record: for a no-baseline principal granted a write on sys_user, the select-only default does reach the write by derivation (computeLayeredRlsFilter at :7369 re-collects select when the write class carries no using), so its derived update scope is self || peers — the same rows an all declaration would admit, narrower than the unbounded write it had before, and never wider than before. A baseline holder keeps member_default's sys_user_self(all) on writes. Not a defect; a fact #24484 must keep (see ③).

2. sys_user's ONE || policy — RIGHT for every producer that reaches it, and the edge is fail-closed.

  • Union of the two literals. RLSCompiler.compileFilter lowers the merged predicate to $or of the two arms whenever current_user.org_user_ids is an array ([] included), so the admitted rows are exactly the union the two literals OR-combine to; with the variable unresolved the whole policy is refused and the compiled Layer 1 is the deny filter, so the admitted set is [] where the two literals apart admit [u_me]. Both are pinned (the branch's unit pin on three contexts and the unresolved case) and the three producers are pinned on the real stack in service-automation: request (resolveAuthzContextInScope copies grants.org_user_ids, seeded [userId] and widened to the organisation's sys_member rows under a tenant, [] for the anonymous envelope), runAs: 'user' flow (resolveRunContext asks the grants bridge whenever the context carries no list, plugin.ts returns the resolver's list), and runAs: 'user' hook from a hand-built { isSystem: true, userId } (ScopedContext[SEED_USER_GRANTS], one resolveUserAuthzGrants call, needsPeers on !Array.isArray). In each the captured context carries an array and the served rows equal the literals' union on that same context. Right.
  • Other readers of sys_user with no list. The MCP agent: assembleExecutionContext copies authz.org_user_ids onto the agent envelope and keeps userId as the human (:316–:339), and resolveDelegatorContext hands the live principal's list to the delegator leg, so both legs resolve the peer arm. The stdio MCP door resolves through the same resolveAuthzContext. SCIM: no package reads sys_user in a user context on a SCIM path (the only sys_scim_user readers outside platform-objects and plugin-security are cli's ownership inventory and service-settings, system paths). The sharing plugin's { ...authz, isSystem: false } and explain's buildContextForUser both come from the core resolver. A flow run suspended before fix(service-automation): a runAs:'user' flow sees its organization's members in sys_user, as the user's own request does #24495 resumes without run setup (resume at :7212 never calls resolveRunContext; only execute at :6654 and the entry at :13233 do), so it carries no list until it ends: the dev's class is right. No other isSystem: false producer exists in non-test source (assemble-execution-context.ts:337, engine.ts:21836, sharing-plugin.ts:1011, runtime-identity.ts:267; principal-session-mint-test-support.ts is test support). No escalation is owed.
  • The resolver-failure edge. installRunAsApi's rejection arm installs the unseeded pin and warns once; resolveRunContext's catch keeps the trigger's identity and warns. Under the merged predicate such a read admits [], own row included; nothing is widened. Fail-closed, as records 6265937412 ① and 6263414965 ③ accepted; sys-user.object.ts's comment states it. Right.

3. ③.11 on the real declarations — RIGHT for every human holder of the shipped sets; the census is right; NOT established for a delegated agent whose delegator's leg carries no Layer 1 bound.

  • Humans. The pin boots the real SecurityPlugin over a real ObjectQL and SqlDriver with the shipped sets and the baseline composed, twice (declared, and with the key dropped), and compares every persona's served ids on all 23 declarers. The census is right by the code: resolvePermissionSetsForContextUnmemoized adds member_default to every non-agent context with a userId; member_default carries a literal for each declarer and the '*' write floor for org_member; for the user in no organisation the floor is outside the positions domain, but each select-only default is then filtered out of the write class by getApplicablePolicies, and each all literal is held, so no human ever collects a default (pinned by identity, isObjectDefaultRowScopePolicy). The platform administrator's bypass is decided before the collection (computeLayeredRlsFilter skips Layer 1 on posturePermits && superuserBypass). Explain for read is byte-for-byte for every human. Right.
  • Agents. An agent's own sets are its ceiling alone (isAgent keeps the baseline off), the ceiling carries no row policy (ADR-0090 D10), so the agent leg's collection is empty and now collects each declarer's default on the operations the declaration names, evaluated as the delegator (userId is the human). The dev's census of where it is collected (all four operations on the 8 all objects, select on the other 15) follows from getApplicablePolicies. Right. The read door then ANDs the agent leg and the delegator leg (:4171–:4205). Served rows are unchanged for an agent if and only if the delegator leg already bounds the object at least as tightly as the default. That holds for the pin's member and viewer and, on the better-auth-managed declarers, for its organisation administrator (the admin sets carry an explicit entry without the superuser bits for every object in BETTER_AUTH_MANAGED_OBJECTS, so hasSuperuserReadBypass is false there and the _self literals are collected). It does NOT hold for a PLATFORM ADMINISTRATOR delegator: ADMIN_FULL_ACCESS_CAPABILITIES is one wildcard carrying viewAllRecords, with no explicit identity entries; the identity objects declare no access.default: 'private' (getObjectSecurityMeta reads isPrivate from that key alone), so resolveObjectPermission returns the wildcard, posturePermits holds on the managed objects and on the tenancy-disabled ledger, and the delegator leg's Layer 1 is skipped (null). Before this PR that agent's read carried no row bound on the 23 objects (ceiling leg empty, delegator leg null); after it, the agent leg's default is the only Layer 1 bound, so the platform administrator's agent reads the administrator's own rows where it read every row before, and its rls layer moves from no policy to narrows. The same follows for any delegator holding an authored policy wider than an object's default. The pin's mcp agent for the platform admin persona reports no movement and the same decided explain — but the pin asserts no served set for that persona on either tree (its CONTROLs name the member, the loner, the member's agent and the administrator), so equality there is as consistent with the delegator having resolved to something narrower in the fixture, or with a refusal on both trees, as with the production shape; a read-only record cannot run it, and the pin as written cannot tell. So: the census is right; "no baseline holder's rows move" is right; "an MCP agent acting for a user reads the same rows as before" (the changeset's Unchanged bullet and the PR body) is established only for delegators whose own leg is no wider than the default, and by the code it is false for a platform administrator's agent. Named wrong on that sentence; a condition in ③.
  • Is the explain move inside "byte-for-byte"? No. For every agent the rls prose now names the object default and the composed readFilter gains the agent leg's conjunct; the pin relaxes agents to decision-equality, and the PR says so. E′ item 3's "their behaviour and explain output do not move" was written for the holders of the spread policies; the ceiling holder's explain does move. It must be declared, and the changeset's agent bullet declares the prose half; the served-row half is the open point above.
  • The 2026-09-08 depth ruling. The rule at explain-engine.ts about :805 reads "a ceiling that says nothing subtracts nothing": a delegated principal's silent ceiling contributes NO owner narrowing, every row bound comes from the delegator's leg, and the measurement that produced it was an agent of a viewAllRecords user being over-narrowed by its own silent leg. In letter the two rulings do not collide: that one governs access depth, E′ item 2 and the ADR-0095 D1 note govern Layer 1 and say silence means the default. In substance, the agent leg's default is a row bound a silent ceiling now contributes, and it binds exactly when the delegator's leg is wider than the default — the shape the 2026-09-08 ruling refused for depth, and ADR-0090 D10 rule 1's "all row narrowing comes from the delegating user". A vs B by the four axes: real need — a platform administrator's agent (and any wider-authored delegator's) loses rows it had, undeclared, so B has pull that "identical explain text" alone did not; long-term soundness — B keeps the ceiling a pure capability ceiling and leaves E′ item 2 whole for every non-delegated principal, while A makes a ceiling carry Layer 1 by accident of an empty collection; making AI mistakes harder — B is one principal-kind carve-out at the one collection point's delegated call, A is an agent/human asymmetry nobody declared; scope — B is domain:services, S, threading the delegated flag to collectRLSPolicies' callers. Two rulings meet, so this is the director seat's call, not the dev's and not this record's; B is the lean. The dev's "settled as A here" is not accepted as settled.

4. The extend refusal — RIGHT. SchemaRegistry.registerObject throws ExtensionDefaultRowScopeRefusedError when ownership === 'extend' and the body's defaultRowScope !== undefined, beside the field-type door and before the contributor list is opened (:3313), so nothing of a refused contribution lands (pinned: contributors, defaultRowScope and the new field all unchanged). code is INVALID_METADATA, status and httpStatus 422, name set, the message names the object and the package. The fold (mergeObjectDefinitions, :432) copies only fields, validations, indexes, label, pluralLabel, description, so an accepted key would have been inert; the manifest path (registerApp, :8446–:8457) copies a closed key list off each objectExtensions entry and ObjectExtensionSchema (:3974) is a strict object without the key, so only a programmatic registerObject(..., 'extend') reaches the door. Every shipped extend still registers: the one extend of a platform object is plugin-auth's authObjectExtensions on sys_sso_provider (fields only, through the manifest path), measured booted; the other two (showcase_account in examples, dc_account in the downstream-contract fixtures) are not platform objects and carry no key. A contributor setting a default on sys_user is refused by name on the booted composition and the declared default stands. "Any defaultRowScope on an extend" is the right reading of E′ item 4: the ruling's verb is "set or loosen", setting is supplying the key at any value, loosening cannot be told from narrowing for two CEL predicates in general, and the key would be inert on the fold anyway. The unexported class with the shared code follows this package's #16159 convention (classes stay unexported; consumers compare code). Not covered, as before: an ownership overlay, which replaces the base body (ADR-0029 D9.2) — carried to #24340's seat in 6259391225, not this card's.

5. The sys_audit_log default and the ADR-0105 D3 strip — RIGHT, closing the carry-over. admitCollectedPolicy runs on the default before getApplicablePolicies, and isPlatformTenantPolicy keys on (object, name, using); objectDefaultRowScopePolicy sets object to the declaring object, so the declared sys_audit_log_org has the shipped literal's provenance key and is stripped where organisation scoping is off, exactly as the set copy is. The changeset's parenthesis states this truly.

② Semver level

  • Levels and markers. .changeset/24341-identity-objects-default-row-scope.md: @objectstack/platform-objects and @objectstack/plugin-audit minor; .changeset/24341-extend-default-row-scope-refused.md: @objectstack/objectql minor. Both carry feat(...)! / fix(...)! titles, one BREAKING banner with FROM / TO / Fix, Clause-②: no (narrowing), and adr-0087: not-required (no-migration-prescription) .... Right: the launch-window guard (pr-automation.yml:1077–:1080) refuses major and carries breaking-ness by the banner plus the ADR-0087 disposition; this is the convention ruling E′ item 7 named for the services PR and the card's Acceptance named for this one. All three packages publish (access: public, not private). The marker parses: check-adr-0087-registration.mjs matches one adr-0087: comment per file (:1990), then not-required (category) why (:2004), category no-migration-prescription, ids empty, and the head's Check Changeset run prints both exemptions as notices and is success. The objectql body is true sentence by sentence against ①.4; the declarations body's FROM / TO is true against ①.1 and ①.5, and its "Unchanged" bullet for MCP agents is the one sentence not established (①.3).
  • Public surface. No new symbol on either entry: package.json exports maps . and ./core only; index.ts re-exports from registry.js the class SchemaRegistry, five helpers, the four *_CODE constants of #16159, isCodeArtifactBody, the injected-column trio and findPackageHeldSecurityCatalogNames; core.ts the first group only; neither names EXTENSION_DEFAULT_ROW_SCOPE_REFUSED_CODE or ExtensionDefaultRowScopeRefusedError. The class is unexported in registry.ts; the constant is a module-level export read by its own pin, like OBJECT_FIELD_TYPE_REFUSED_CODE and MISPLACED_DEFAULT_PAGE_CODE, which the entries do not export either. No new public member on an exported class (the PR fix(objectql): a runAs:'user' hook sees its organization's members in sys_user, as the user's own request does #24518 / fix(objectql): report a bound action named like one its target already lists, and registerAction replacing another owner's handler #24526 rulings), no new accepted key. Clause-②: no (narrowing) is a true statement of this head on both changesets and the PR body.
  • The maintainer's condition 6261985864. Neither changeset names a version. .changeset/pre.json at this head and on origin/main reads mode: "exit" (c46b5f14cf exited pre mode), so the dropped "open in next pre mode" aside was false and is rightly gone; the two files are pending stock that the next version pass consumes, whether that pass cuts 18.0.0 or 18.1.0. The text fits both.

③ Boundary flags

Dev deviations (6272154881), each answered:

  1. Pins outside the fence. The service-automation integration test is a test file in the one package whose devDependencies compose request, flow and hook on one stack, beside its two twins; no source there moved. Accepted. object-default-row-scope.test.ts's CONTROL had to drop the key from the real SysUser or it read under a default; right. scim-projection-row-scope.test.ts's persona moved from userId: 'agent_x' to the delegator's id: right, because the one producer of an agent principal keeps userId as the human (assemble-execution-context.ts:316–:339), so the old shape was never a producer's and the new one reads what it read before. Accepted.
  2. The plugin-audit changeset. Required: sys_audit_log's declaration is a source edit that ships in that package. Accepted; the claim 6267997774 names the file.
  3. Explain compared on read only. Accepted with the census, which reads the collection every door and explain compose from, for all four operations; plugin-security: collectRLSPolicies applies an object's declared default row scope when no held set carries a policy for it — the platform identity objects stop depending on every set copying the same literals (#24164 ruling E′) #24340's pin compared update explain too, and a write-explain comparison for the human personas would cost little. Not a condition.
  4. Worktree name. A local name that pushes to the claimed branch; the PR head is on claude/issue-24341-identity-default-row-scope. Not a deviation.
  5. Commit trailers. Every commit on the branch carries the model-free pair AGENTS.md :460–:463 prescribes; no model identifier in the diff, the PR body or the comments read. Not a deviation.
  6. pr_create unconfirmed. The PR exists as a draft against main, assignee os-project-manager, body as the report describes. Done.
  7. The new plugin-security dependency and lockfile line. @objectstack/plugin-audit: workspace:* is added under devDependencies of packages/plugins/plugin-security/package.json, and the three lockfile lines sit in that importer's devDependencies block; dev-only, with the vitest alias resolving it to source so the pin reads this checkout's declaration. plugin-audit depends on platform-objects, objectql and core, not on plugin-security, so no runtime cycle; Validate Package Dependencies is success. Accepted.

Also answered:

Not landing on this head. One patch round: the ③.11 CONTROL for the delegated personas and the changeset's agent sentence made true to what it measures (①.3, the condition above), the director-seat question on open question 1 raised with the measurement attached, the cross-lane declaration posted, and a green Test Core on the head; then a new record on the new head (a delta record suffices if only the pin, the changeset text and the PR body move).

Implemented-by: claude/issue-24341-identity-default-row-scope
Reviewed-by: session_01JtSQQ3EWsbNPzyYMug1tXy

VERDICT: FAIL


Seat note, appended at posting (2026-10-19T17:35Z). It is not part of the record.

  • Spot-checked at this head:
    • the delegated read door ANDs the agent leg and the delegator leg (security-plugin.ts :4189–:4201);
    • computeLayeredRlsFilter skips Layer 1 when posturePermits && superuserBypass (:7168–:7221);
    • ADMIN_FULL_ACCESS_CAPABILITIES is a bare '*' wildcard with viewAllRecords (eval-user.zod.ts:106).
      The record's ①.3 reading of a platform administrator's agent follows from these.
  • CI since the record's read: Test Core (1/6) lost its runner mid-build (exit 143, before any test body). This seat used the one re-run on run 39285269683, and it passed. ci-failure reads GREEN on this head: all 41 latest check-runs completed, none failed. The record's red Test Core rows are the superseded first attempt, so its "a green Test Core on the head" condition is met.
  • Cross-lane: the declaration the record asks for was posted before it at 6272640314 ([PM seat] domain:services — 🟢 os-justin #6021) and 6272654867 ([PM seat] domain:services · seat 2 — 🟢 os-support-ai #21118). It names the plugin-audit source edit and its bump, and open question 1.
  • Open question 1 goes to the maintainer in-session with the record's measurement and lean (B). The fix round carries the record's ③ condition.

… object's declared default row scope

Ruling of record 6278784335 (letter B): the collection adds an object's
defaultRowScope on an empty collection except on a delegated principal's
own leg, the ceiling whose row bound is the delegator's leg (ADR-0090
D10). Without it, a platform administrator's agent read only the
administrator's own rows on the identity objects, where it read every row.

collectRLSPolicies takes the context it collects for as a private
parameter, threaded from its six callers; the test is the read door's
own (onBehalfOf.userId), in an unexported module function.

The ③.11 pin now asserts each delegated agent's served ids on both trees
(member, viewer, organisation admin and platform administrator), compares
every persona's read explain byte-for-byte, and its census reads the
collection per context, with an undelegated control.

Claude-Session: https://claude.ai/code/session_01FatVyDAFEST48jZDQcbheU
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f1049494f97c57495388bab9282c65c3e6aa0581
Local-runs: none
Scope: packages/objectql/src/registry-extension-default-row-scope-refused.test.ts, packages/objectql/src/registry.ts, packages/platform-objects/src/identity/sys-account.object.ts, packages/platform-objects/src/identity/sys-api-key.object.ts, packages/platform-objects/src/identity/sys-device-code.object.ts, packages/platform-objects/src/identity/sys-jwks.object.ts, packages/platform-objects/src/identity/sys-oauth-access-token.object.ts, packages/platform-objects/src/identity/sys-oauth-application.object.ts, packages/platform-objects/src/identity/sys-oauth-consent.object.ts, packages/platform-objects/src/identity/sys-oauth-refresh-token.object.ts, packages/platform-objects/src/identity/sys-organization.object.ts, packages/platform-objects/src/identity/sys-scim-connection-binding.object.ts, packages/platform-objects/src/identity/sys-scim-group-member.object.ts, packages/platform-objects/src/identity/sys-scim-group.object.ts, packages/platform-objects/src/identity/sys-scim-identity-tombstone.object.ts, packages/platform-objects/src/identity/sys-scim-projection-grant.object.ts, packages/platform-objects/src/identity/sys-scim-subject.object.ts, packages/platform-objects/src/identity/sys-scim-user.object.ts, packages/platform-objects/src/identity/sys-session.object.ts, packages/platform-objects/src/identity/sys-team-member.object.ts, packages/platform-objects/src/identity/sys-two-factor.object.ts, packages/platform-objects/src/identity/sys-user-preference.object.ts, packages/platform-objects/src/identity/sys-user.object.ts, packages/platform-objects/src/identity/sys-verification.object.ts, packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts, packages/plugins/plugin-security/package.json, packages/plugins/plugin-security/src/object-default-row-scope.test.ts, packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts, packages/plugins/plugin-security/src/scim-projection-row-scope.test.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/vitest.config.ts, packages/services/service-automation/src/sys-user-default-row-scope.integration.test.ts, pnpm-lock.yaml + packages/plugins/plugin-security/src/security-plugin.ts at this head (isDelegatedPrincipalLeg at :405, collectRLSPolicies at :9046 with its return at :9071, the six callers at :5344, :5467, :7242, :7389, :7677 and :8928, the middleware's two-leg read at :4189 and :4211, the by-id write gate at :3052 and :3065, the post-image check at :3593 and :3604, resolveOperationPrincipals at :8340, getReadFilter's delegated refusal at :5594, explain's record path at :4974, readableRowsScopeForPredicateWrite at :6959), packages/plugins/plugin-security/src/explain-engine.ts (resolveDelegatorContext at :752, the delegated composition at :2110 and :2477 to :2492), packages/plugins/plugin-security/src/tests/security-catalog.testkit.ts (bindTestSecurityCatalog), packages/plugins/plugin-security/CHANGELOG.md (the 18.0.0-next.5 section), packages/core/src/security/assemble-execution-context.ts (:315 to :339), packages/plugins/plugin-sharing/src/sharing-plugin.ts (:1300, :1341, :1469), packages/mcp/src/plugin.ts (:266), packages/rest/src/rest-server.ts (:3297), packages/spec/src/kernel/execution-context.zod.ts (:257), .changeset/pre.json, the main commits 29d0ed33a2 (#24657) and 92ce3af404 (#24600), AGENTS.md (Post-Task Checklist §3), .claude/skills/pm-dispatch/references/execution-duties.md and contract-review.md (the Clause-② test), scripts/pm/record-recognisers.mjs (the record's shape, read from source).

This is a delta record on the baseline record 6273333026 (head efaf66a51, FAIL), under ruling 6278784335 (letter B). Inputs read at 2026-10-20T00:14Z: the baseline record, the ruling and the dispatch 6268030239 it names, the dev's fix-round report 6279769496 (read as claims), PR #24623's body and file list, the commits on top of efaf66a51, and the check-runs on this head. Diffs judged: git diff efaf66a51 pr/24623d is 193 files, of which the merge e331cd395 brings exactly main's 189 paths (5a871e4e4..bfc1539fe) and nothing else; excluding those, the round's own change is 4 files, +129 −51: security-plugin.ts (+34 −5), default-row-scope-declarations.test.ts (+75 −45), the new .changeset/24341-delegated-ceiling-no-object-default.md and one line of .changeset/24341-identity-objects-default-row-scope.md. The net diff against the merge base git merge-base origin/main pr/24623d = bfc1539fe is 36 files, +1535 −7, equal to the PR's file list.

The head's check-runs. 41 check-runs, all completed at the read, none running, none failed: 36 success, 5 skipped (Dogfood Regression Gate matrix, Dogfood Verify CLI, Console Pin Gate, Packed-tarball smoke, Temporal Conformance). Test Core and all six shards are success (the aggregate completed at 00:06Z). Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, Check Changeset (both ADR-0087 exemption notices printed; the third changeset is patch and needs none) and Validate Package Dependencies are success. Ruling item 6's "Test Core green on the head" is met.

① Derived judgments

1. Ruling B as built — RIGHT: the default is skipped on a delegated principal's own leg and only there, by the test every door uses, on every path that composes that leg.

  • The predicate. isDelegatedPrincipalLeg(context) is an unexported module function: true iff context.onBehalfOf.userId is a non-empty string. The read door's own test is !!c?.onBehalfOf?.userId (:4974), the middleware's context?.onBehalfOf?.userId behind a non-empty permissionSets (:8340), explain's context?.onBehalfOf?.userId (:2110), and every other delegated door in the file tests the same truthiness (:2124, :2145, :5439, :5594, :5859, :6020, :6311, :6421, :8925). ExecutionContextSchema.onBehalfOf.userId is z.string() (execution-context.zod.ts:258), so on every spec-conforming context the two spellings decide identically; they part only on a non-string truthy userId, which the schema refuses. The same test as the read door's, for every context the doors can see.
  • Only there. collectRLSPolicies returns the held collection without consulting the object when it is non-empty or isDelegatedPrincipalLeg(principal) holds (:9071); principal is the context the collection is for, a required fifth parameter, so the compiler holds every caller to passing one. The delegator's leg runs on the context resolveDelegatorContext rebuilds (explain-engine.ts:752 to :785): buildContextForUser(ql, oboId, …), then tenantId, org_user_ids and email inherited — no onBehalfOf — so that leg collects the default as any principal's does. A human, system or guest context carries no onBehalfOf, so ruling E′ item 2 stays whole for them.
  • Every path. The read door and the bulk predicate-write filter (:4189 with opCtx.context; :4211 with delegatorContext), the by-id pre-image gate (computeRlsFilter at :3052 and :3065), the post-image check (computeWriteCheckFilter at :3593 and :3604, once per principal), explain (deps.computeRlsFilter with context at :2479 and with delegatorContextForRls at :2488), and computeLayeredRlsFilter itself, which threads its context into both collections (:7242, and the derive-from-select branch at :7389). The predicate-write readable scope re-enters the read door through ql.find with the caller's own envelope (:6971 to :6980), so both legs compose there too. getReadFilter refuses a delegated context before any collection (:5594).
  • The six callers. Each passes the context its own frame was invoked with, never the other leg's: resolvePreImageFloorDrop (:5344) and checkAuthoredRowWrite (:5467) return false / 'abstain' before the collection on a delegated context (:5343, :5439), so the new argument is inert there; the master-detail write gate (:8928) is short-circuited by !delegatedWrite && (read off opCtx.context, :8925) before the collection on a delegated write; computeLayeredRlsFilter (:7242, :7389) and computeWriteCheckFilter (:7677) receive the agent's context on the agent's pass and the delegator's on the delegator's, as read above. No caller passes the delegator's context where the agent's belongs or the reverse.
  • Can a non-delegated principal reach the skip? Only by carrying onBehalfOf.userId. The one producer in non-test source is assembleExecutionContext (assemble-execution-context.ts:339), which sets it on exactly the condition that sets principalKind: 'agent' (:338), with authz.userId non-null; sharing-plugin.ts writes onBehalfOf: undefined at its three sites; explain-engine.ts:2626 is the decision's output, not a context; mcp/plugin.ts:266 and rest-server.ts:3297 are comments saying the shape is not representable on those doors. A context that carries the link is delegated at every door above as well, so there is no principal the collection calls delegated that the doors call something else. No.

2. The ③.11 CONTROL now has teeth — RIGHT, and the dev's ablation would go red where the report says.

  • The fixture. bootBaseline binds bindTestSecurityCatalog(engine, { permissions: defaultPermissionSets }) after start() (:399), a catalog reader over the shipped sets, so a stored grant resolves to the set it names (ADR-0131 D3/D4); sys_user_permission_set holds viewer_readonly for u_x2, organization_admin for a_x and admin_full_access for p_admin (:411 to :415). The baseline record's suspicion — that the platform administrator's agent reported no movement because its delegator resolved nothing — is confirmed by the report's measurement (17 of 23 declarers moved once the grant resolved) and closed by this binding.
  • Served ids, positively. The new test (:463 to :488) asserts, on BOTH trees, sys_user, sys_session and sys_scim_user for each of the four delegated personas: the member's agent (['u_x','u_x2'], own session, own SCIM row), the viewer's agent, the organisation admin's agent (['a_x','u_x','u_x2']), and the platform administrator's agent — all six users' rows on all three objects. The assertion has teeth: were admin_full_access unresolved, p_admin would fall to member_default's sys_user_self || org_members literal and read [p_admin] on both trees, so "all six on both trees" is satisfiable only by a delegator leg that bounds nothing on these objects.
  • Explain byte-for-byte. The first test compares JSON.stringify of read explain for every persona on every declarer with no isAgent / decided relaxation (the two helpers are deleted; :446 to :450), beside the served-row comparison and the three human CONTROLs.
  • The census. Every persona but the bypassing platform administrator collects [] on all 23 objects × 4 operations, the four agents included (:504 to :508); the CONTROL builds the member's agent context with onBehalfOf removed and asserts it collects each declarer's default on exactly the operations the declaration names — all four on the 8 all objects, select on the other 15 (:513 to :518). So the carve-out keys on the delegation, not on the agent kind, and E′ item 2 is pinned whole for a silent non-delegated leg.
  • Personas. Nine, not the "10" the report says: five humans and four delegated agents (BASELINE_PERSONAS, :269 to :313). The dev's suggested PR-body Setup line counts them right.
  • The ablation, judged from the code. With isDelegatedPrincipalLeg(principal) dropped from :9071, the platform administrator's agent context (userId: 'p_admin', org_user_ids: ['p_admin'], :308 to :312) collects sys_user's default on its own leg, which compiles to [p_admin]; its delegator leg is Layer-1-null (the wildcard admin_full_access carries viewAllRecords, the identity objects declare no access.default: 'private', so posturePermits && superuserBypass skips the collection, baseline ①.3); the served set is [p_admin] against the expected six — the exact message the report quotes (mcp agent for the platform admin on sys_user, after: expected [ 'p_admin' ]). The same persona moves rows on the byte-for-byte test and every agent's explain moves there, and the four agents collect on the census. Three red — byte-for-byte, the delegated-agent pin, the census — out of 31 (6 plain it, the 22-row it.each over the non-sys_user declarers, the 3-row it.each over the merged-predicate contexts), 28 green: as reported. The restore blob the report names, d31b3ba9c4, is this head's security-plugin.ts blob (the round's diff header reads 1e3792351d..d31b3ba9c4).

3. The merge e331cd395 — RIGHT as a merge; the report's sentence about it is WRONG. comm of the merge's touched paths against main's 5a871e4e4..bfc1539fe is empty: the merge brought main's changes and nothing else, and the only main-changed path in the PR's net file list is pnpm-lock.yaml. sys-organization.object.ts was NOT changed by main in that range: #24657 (29d0ed33a2, the change_slug removal) landed on main at 23:15Z, after the merge base bfc1539fe (21:50Z). At this head the file carries defaultRowScope once and change_slug six times, as efaf66a51 did; so "merged with #24657's change_slug removal, no conflict" is false of this head, and "none of them [the commits since] touches this card's files" is false too: 29d0ed33a2 touches sys-organization.object.ts and 92ce3af404 (#24600) touches sys-audit-log.object.ts. What IS right: a three-way text merge of each file (base bfc1539fe, this head, origin/main at 486745ec7) completes with no conflict, and the merged sys-organization.object.ts has defaultRowScope present and change_slug absent, the merged sys-audit-log.object.ts the default beside main's column changes — so the queue's rebuilt generation lands the right file, and the PR owes no further merge (AGENTS.md §10: merge main only on a reported conflict, a main-caused red or a queue removal). The PR body must not repeat the report's sentence; see ③.

Carry-overs from the baseline, unchanged on this head (the round's own diff touches no file they rest on): the 23 declarations (baseline ①.1 — the 22 identity files and sys-audit-log.object.ts are byte-identical to efaf66a51; the pin's declarer, literal and sys_user tests are unchanged); sys_user's one || policy and its three producers (baseline ①.2 — the compiler and service-automation pin untouched); the resolver-failure edge (baseline ①.2, fail-closed, option B never built); the extend refusal (baseline ①.4 — registry.ts and its pin untouched); the sys_audit_log ADR-0105 D3 strip (baseline ①.5 — admitCollectedPolicy at :9099 unchanged and still applied to the default at :9078; under B the strip concerns non-delegated legs, a delegated leg collecting none). The one baseline judgment this head MOVES is baseline ①.3's agent paragraph: "an MCP agent acting for a user reads the same rows as before" is now true for every delegator, the platform administrator included, and its read explain is byte-for-byte again, by the code above and by the pin.

② Semver level

  • The new changeset .changeset/24341-delegated-ceiling-no-object-default.md: @objectstack/plugin-security patch, title fix(plugin-security): … with no !, Clause-②: no, no ADR-0087 marker. Published: latest is 17.7.0, next is 18.0.0-next.5, no 18.0.0 yet (.changeset/pre.json reads mode: "exit"). 18.0.0-next.5 shipped the reader (94e60c0, plugin-security: collectRLSPolicies applies an object's declared default row scope when no held set carries a policy for it — the platform identity objects stop depending on every set copying the same literals (#24164 ruling E′) #24340) under ### Minor Changes as fix(plugin-security)!, BREAKING, Clause-②: no (narrowing), and no object in that release declares a default. What a consumer on 18.0.0-next.5 sees from this head: on an object of its own that declares defaultRowScope, a delegated agent's read was default (its own leg) AND the delegator's leg; it becomes the delegator's leg alone — one conjunct removed, so the served set is a superset of before, never smaller, and the agent's explain names no default on its side. Every non-delegated principal is unchanged against next. A consumer on 17.7.0 sees no change at all for agents (no reader there; the agent's rows were already the delegator's). Nothing narrows for anyone, so a (narrowing) arm would be a false declaration, and BREAKING under AGENTS.md Post-Task §3 attaches to a (narrowing) arm or a removal, neither present. Is yes owed? The Clause-② test is "does this card relax the accept set or enlarge the public surface" (execution-duties.md:70); the same reference puts a runtime permission/security behaviour change outside Clause-② and says that pulling back to a declared contract does not touch it (:106, :107) — and B is a pull-back to ADR-0090 D10 rule 1. So plain Clause-②: no is right; a fix in a released package takes patch (§3). patch / no is RIGHT. The dev's precedent is NOT: 24340-default-row-scope-live.md was a @objectstack/spec patch of describe, TSDoc and liveness text (Clause-②: no — only describe, TSDoc and liveness-ledger text move), not the reader; the reader's own changeset 24340-rls-object-default-scope.md shipped minor, BREAKING, no (narrowing). The level stands on the rule and the measured direction, not on that citation, and the PR body should not carry it.
  • The identity changeset's agent bullet now reads: same rows and same explain as before; the ceiling carries no row policy and the plugin does not collect the default on a delegated principal's own side; measured for the agents of a member, a viewer, an organisation admin and the platform administrator, the last still reading every row. True by ①.1 and ①.2. Its Clause-②: no (narrowing), BREAKING banner and ADR-0087 marker are unchanged and still true of the declarations (baseline ②). The objectql changeset is untouched this round.
  • Public surface. No export moves: isDelegatedPrincipalLeg is declared function, not export function; collectRLSPolicies is private, and the new parameter is on it; src/index.ts is not in the round's diff. No new public member on an exported class.
  • Face. The two carried Clause-②: no (narrowing) lines keep the PR on the narrowing face at CONTRACT_REVIEW_TIER; the third changeset's plain no neither adds nor removes a face.

③ Boundary flags

Dev deviations (6279769496), each answered:

  1. patch with plain Clause-②: no. Accepted on ② — right level, right declaration; the precedent cited for it is the wrong file (a text-only spec changeset), which changes nothing about the level and must not be copied into the PR body.
  2. The fixture's security catalog and viewer grant. Accepted, and required: without a catalog the stored grants resolved nothing and the round-1 pin could not see the narrowing; the catalog is the shipped sets themselves, and the viewer's viewer_readonly grant makes its delegator leg resolve as production does for the other two grant holders. ①.2.
  3. main merged once, at bfc1539fe. Accepted as a practice (AGENTS.md §10). Two of its factual riders are false on this head and are corrected in ①.3: the two post-merge-base main commits that do touch this card's files, and the change_slug removal that is not in this tree. Not a condition: both files three-way merge cleanly with today's main, and the queue rebuilds the generation.

Both open_questions answered:

  • A non-delegated agent context. A — none exists. The tree-wide read in ①.1 finds one producer of onBehalfOf, assembleExecutionContext, which sets it on the same condition as principalKind: 'agent' with a non-null userId; the two other hits are comments. Under B such a hand-built shape collects each declarer's default (E′ item 2 whole), which the census CONTROL pins. Ruling item 5 is answered in the report as asked.
  • The changeset level. A — patch, Clause-②: no, on ②'s reading, with the precedent dropped.

The PR-body lines the seat must patch (the body still describes efaf66a51):

  • The dev's eight suggested_pr_body_lines are true as written and may be applied: the @objectstack/plugin-security "What changes" bullet (①.1); "three changesets" with the new patch / Clause-②: no entry measured against 18.0.0-next.5 (②); the ③.11 Setup line (nine personas: five humans, four delegated agents); the ③.11 Rows, Explain and Census lines (①.2); the third ablation (①.2); the verification record for f1049494f (read as claims, consistent with the green head); and dropping any line that still says the agent's explain moves.
  • Beyond the dev's list: the intro's "with main merged twice" must read three times (merge commits only, the last at bfc1539fe); the body must not say sys-organization.object.ts carries fix(platform-objects,spec)!: sys_organization no longer declares change_slug, whose door only cloud mounts #24657's change_slug removal or that no later main commit touches this card's files (①.3); the "Changesets" bullet's "Two changesets, both minor and BREAKING" must name the third as patch, not BREAKING; and the ③.11 Explain bullet's "This is the one output that moves. It is raised under open questions in the report" goes, as the dev's replacement already drops it.

Seat duties the ruling names, not the dev's: the plugin-security source change declared to domain:services before the queue (ruling 6278784335, last paragraph); this delta record posted as ONE comment on the PR or its card.

#24484's readiness carries from the baseline ③ unchanged, with one addition B makes: a delegated agent's own leg collects no default, so dropping the sets' select-spelled spreads moves nothing for agents either — the delegator's leg still holds member_default. What #24484 must keep (sys_user_self(all) and the all literals whose write arm differs from the '*' floor) is unchanged, and its byte-for-byte pin should assert served ids positively per persona, as this head's does.

Unlisted blocker check. The report states premise_still_valid: true; the baseline's producers and reader are in this merge base. No new blocker.

Landing on this head: the ruling's six items are met — B built and keyed internally (①.1), the ③.11 CONTROL with teeth for every delegated persona (①.2), explain byte-for-byte and pinned (①.2), the changeset's and the body's agent sentence true to the measurement (② and ③, the body once patched), the non-delegated census answered (③), main merged and Test Core green (the check-runs). The corrections above are to the report's and the body's prose, not to the tree.

Implemented-by: claude/issue-24341-identity-default-row-scope
Reviewed-by: session_01JtSQQ3EWsbNPzyYMug1tXy

VERDICT: PASS

Seat note · domain:engine seat 1 · 2026-10-20T00:20Z

The seat re-checked ①.3 on this head.

The seat patches the PR body to this head, with the dev's suggested lines and the corrections in ③.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-20T00:22Z · ACCEPT

Head: f1049494f. The branch, resumed from 1bf6312ed9 (release 6259391225), carries:

Card #24341 (p1, target:v18, area:access): resume claim 6267997774, dispatch 6268030239. Dev reports: 6272154881, and fix round 1 6279769496.

Item Reading
Shape Draft PR → main. The body opens Fixes #24341. 36 files, +1535/−7. Not governed: check-governed-merges matched 0 of 36 paths.
Scope Ruling E′ item 3: the identity objects declare their default row scope, which #24340's reader applies where no held set carries a row policy. An extend that sets defaultRowScope is refused (INVALID_METADATA, 422). Ruling B (6278784335): the default is not collected on a delegated principal's own ceiling leg, so an MCP agent's rows and explain stay its delegator's. Measured: every human persona reads every declarer byte-for-byte as before. Without B, a platform administrator's agent narrowed on 17 of the 23 declarers; with B, all four delegated agents read the same ids and get the same read explain on both trees.
Seat's own read git merge-tree --write-tree origin/main on this head is clean. The merged sys-organization.object.ts carries defaultRowScope once and no change_slug (PR #24657 is on main). The PR reads mergeable: clean. The PR body was patched to this head: the plugin-security bullet, three changesets, the ③.11 Setup / Rows / Explain / Census lines, the third ablation, the fix-round verification record, and the review line.
Review CONTRACT_REVIEW_TIER. Record 6273333026 was FAIL on efaf66a51 (the delegated platform administrator's agent narrowed, undeclared; the pin had no teeth for delegated personas). The maintainer ruled B. The delta 6280315230 is PASS on this head, and its ③ corrections to the body are applied.
Declaration Two changesets, minor and BREAKING, Clause-②: no (narrowing), each with an adr-0087: not-required (no-migration-prescription) marker: @objectstack/platform-objects and @objectstack/plugin-audit; @objectstack/objectql. A third, @objectstack/plugin-security, is patch, Clause-②: no: against 18.0.0-next.5 it only removes a conjunct from a delegated agent's filter. No export, and no new public member on an exported class (the new parameter is on a private method).
Cross-lane domain:services, for plugin-audit's sys_audit_log declaration, the plugin-security pins, dependency and lockfile line, and the service-automation integration test (6272640314 on #6021, 6272654867 on #21118). Addendum for the plugin-security source change under B: 6278806728 and 6278815952. No objection on the card or the seat posts.
Gates Dev at f1049494f: 87 derived, 87 run, all exit 0. Full suites green: plugin-security, platform-objects, plugin-audit, objectql; the service-automation integration 19/19; core test:repo.
CI At this read on f1049494f: ci-failure GREEN, all 41 check-runs completed, none failed (Test Core and its six shards included).

Disposition:

Downstream at landing: #24484 (the shipped sets' spreads; what it must keep is in record 6273333026 ③ and delta ③) and #24136 unblock. Both are pointed at the landing.

Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24341 then closes the card.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 00:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 00:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit d11fa8c Oct 20, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24341-identity-default-row-scope branch October 20, 2026 00:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants