Repository navigation
feat(platform-objects)!: the identity objects declare their default row scope, and the registry refuses an extend that sets one - #24623
Conversation
…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>
…e mode Claude-Session: https://claude.ai/code/session_01FatVyDAFEST48jZDQcbheU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 35 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: Net diff judged: 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: ① Derived judgments1. The 23 declarations — RIGHT on the set, RIGHT on 22 operations, and 2.
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.
4. The 5. The ② Semver level
③ Boundary flagsDev deviations (6272154881), each answered:
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 Implemented-by: VERDICT: FAIL Seat note, appended at posting (2026-10-19T17:35Z). It is not part of the record.
|
… 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>
Claude-Session: https://claude.ai/code/session_01FatVyDAFEST48jZDQcbheU Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: This is a delta record on the baseline record 6273333026 (head 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). ① Derived judgments1. 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.
2. The ③.11 CONTROL now has teeth — RIGHT, and the dev's ablation would go red where the report says.
3. The merge 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 ② Semver level
③ Boundary flagsDev deviations (6279769496), each answered:
Both
The PR-body lines the seat must patch (the body still describes
Seat duties the ruling names, not the dev's: the #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' Unlisted blocker check. The report states 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 (③), Implemented-by: VERDICT: PASS Seat note ·
|
席位核对 ·
|
| 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:
open_questions: all answered:- the resolver-failure edge: A, fail-closed;
- the agent explain move: resolved by ruling B; it no longer moves;
- the non-delegated agent census: A, no producer emits one, and the census CONTROL pins what one would collect;
- the
plugin-securitychangeset level: A,patch/Clause-②: no.
deviations: all accepted (records 6273333026 ③ and 6280315230 ③). The fix-round report's sentence that the merge carried PR fix(platform-objects,spec)!: sys_organization no longer declares change_slug, whose door only cloud mounts #24657'schange_slugremoval is false of this head, and the delta records it; the merged tree is right, and the body no longer says it.out_of_scope_findings: theagent_xpersona inprivate-credential-row-scope.test.tsmeets no filing gate; plugin-security: the shipped permission sets stop copying the identity objects' row-scope literals once those objects declare them as their default row scope (#24340 follow-up, ruling E′ item 3) #24484's pin pass in the same package is its natural carrier.
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
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), withmainmerged three times (merge commits only, no rebase; the last atbfc1539fe), under claim 6267997774. Fix round 1 builds ruling B (6278784335).What changes
@objectstack/platform-objectsand@objectstack/plugin-audit. The 23 identity objects ruling E′ item 3 names now declaredefaultRowScope. Each value is the policy the shipped permission sets carry for that object, moved verbatim.sys_audit_log's declaration lives inplugin-audit, which owns the ledger (ADR-0029 K2).sys_useris the one merge, per ruling A′ and rider 6255156624. It declares oneselectpolicy:id == current_user.id || id in current_user.org_user_ids.@objectstack/objectqlregistry. Anextendcontribution that carriesdefaultRowScopeis refused atregisterObject, whatever its value and whatever its target. The answer isINVALID_METADATA, status 422, and the error is namedExtensionDefaultRowScopeRefusedError. A contribution without the key registers as before.@objectstack/plugin-security. Under ruling 6278784335 (letter B),collectRLSPoliciesdoes 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.collectRLSPoliciesapplies 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 onmain, so the old "not read yet" was false.minorand BREAKING under the launch-window convention, each withClause-②: no (narrowing)and anadr-0087: not-required (no-migration-prescription)marker, whichcheck-adr-0087-registrationaccepts. The third,24341-delegated-ceiling-no-object-default.md, coversplugin-security,patch,Clause-②: no: measured against18.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.mdcoversplatform-objectsandplugin-audit.24341-extend-default-row-scope-refused.mdcoversobjectql.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:object-default-row-scope.test.ts(plugin-security:collectRLSPoliciesapplies 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). Its "undeclared" shape spread the realSysUser, which now carries the key, so its CONTROL read under a default. The undeclared shape now drops the key.scim-projection-row-scope.test.ts. Themcp_agent_data_writepersona carrieduserId: 'agent_x', and no producer emits that shape. The one producer of an agent principal is core'sassembleExecutionContext, which keepsuserIdas the delegating human. Withagent_x, the agent's own leg applied each SCIM default asagent_xand read nothing. With the producer's shape it reads what it read before. The persona now carries the delegator'suserId.2.
sys_user's one||policy on every user-context producer (service-automation/src/sys-user-default-row-scope.integration.test.ts).usr_meholds one authored'*'read set with no row policy, so the declared default is what applies. Explain namessys_user_self_or_org_membersas the object default.usr_meis in organisations A and B and acts in A.usr_peeris in A.usr_otheris in B only.usr_loneris in none.org_user_ids. Each must read exactly the rows the two shipped literals admit together on that same context.resolveUserAuthzGrants, asresolveAuthzContextcopies it)usr_me, usr_peerusr_merunAs: 'user'flow run (#24495)usr_me, usr_peerusr_merunAs: 'user'hook fired from a hand-built{ isSystem: true, userId }(#24518)usr_me, usr_peerusr_meIn every row the captured
org_user_idsis 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_userwith noorg_user_ids. The producers ofisSystem: falsecontexts are core'sassembleExecutionContext, the sharing plugin's{ ...authz }, explain'sbuildContextForUser,resolveRunDataContextandwithRunAs('user')with its seeding. Each one carriesorg_user_idsfrom 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).viewer_readonly, the organisation adminorganization_admin, the platform administratoradmin_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.sys_user,sys_sessionandsys_scim_useron both.readexplain is byte-for-byte as before for every persona, the four delegated agents included.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,selecton the other 15).readonly, 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
extendrule on the real tree.objectExtensionsentry and everyregisterObject(..., 'extend')caller inpackages/andexamples/, the oneextendof a platform object is@objectstack/plugin-auth'ssys_sso_providerextension. The other two extendshowcase_accountanddc_account.extendcontributor the registry holds: exactlycom.objectstack.plugin-auth → sys_sso_provider, folded, withoidc_client_secretpresent.defaultRowScopeonsys_userin 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.
objectqlentry. The builtdist/index.jsanddist/core.jsexpose neitherEXTENSION_DEFAULT_ROW_SCOPE_REFUSED_CODEnorExtensionDefaultRowScopeRefusedError, withSchemaRegistrypresent as the positive control. Both.d.tsentries name neither.registry.ts, read by its own pin only, and no entry re-exports it.Ablations (each committed first, then restored with
git checkout HEAD, blob and porcelain checked)sys_user's declaration with the peer arm removed.usingbecameid == current_user.id && id != null. I rebuiltplatform-objects, andablation-dist-preflightfound 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.The ③.11 pin's teeth. I deleted the
sys_scim_user_selfliteral fromscimProjectionRowScope, 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.The B guard removed (
isDelegatedPrincipalLegdropped 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: blobd31b3ba9c4== 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
ExtensionDefaultRowScopeRefusedError, the refusal message, anddefaultRowScopebeside'extend'. They are pinned inobjectql/src/registry-extension-default-row-scope-refused.test.tsand the new integration test only.sys_user's two literals all pass unchanged. They hold only while the sets keep their spreads, which they do:member-default-explicit-allow,identity-admin-field-group,default-permission-sets,rbac-objects,security-plugin,authz-matrix-gateandplatform-ownership-policies(plugin-security);sys-user-self-service-route(plugin-auth);identity-admin-fields-org-peer.dogfoodandzero-set-masking.dogfood, run locally: 13 / 13.resolve-execution-context.oauth-organization(runtime) andserve-verify-security-parity.contract(cli).Verification record
Fix round 1, head
f1049494f(after mergingmainatbfc1539fe):plugin-security189 files / 3,785 tests (+45 skipped);platform-objects78 / 1,192;plugin-audit57 / 930;objectql449 / 8,453.service-automationintegration (the new pin and both twins): 19 / 19, against a rebuiltplugin-securitydist.test:repo6 / 82.plugin-security,platform-objects,objectql,plugin-auditandservice-automation.--ranreports a derived zero.mainhas moved sincebfc1539fe;git merge-treeagainst it is clean.Round 0, head
efaf66a51(after the second merge ofmainat5a871e4e4):pnpm turbo run build --concurrency=2 --filter='!@objectstack/docs': 76 / 76.objectql,platform-objects,plugin-audit,plugin-securityandservice-automation.service-automation'stsc --listFilesincludes the new integration test.plugin-securityandobjectqlexclude tests fromtsc, and theircheck:test-typecheckpassed inside the same script.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.node scripts/pm/dispatch-gates.mjs --commandswith no paths derives 87 families, and all 87 ran onefaf66a51, each exit 0, captured before any pipe.--ranreconciles: 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-loadsandcheck:i18n. After the full build both exit 0.check:query-options-erasure, test surface 236 to 237: oneas anyon afindoptions bag in the new test.check:slot-lookup: onegetServicelookup typedany.Acceptance notes
CONTRACT_REVIEW_TIERrecord 6273333026 was FAIL onefaf66a51(a delegated platform administrator's agent narrowed, undeclared). The maintainer ruled B (6278784335). Delta record 6280315230 is PASS onf1049494f.@objectstack/plugin-auditis bumped in the declarations changeset, becausesys_audit_log's declaration ships there.service-automation, which is the package that composes request, flow and hook on one stack, like its two twins;private-credential-row-scope.test.tsalso carries an agent persona withuserId: 'agent_x', a shape no producer emits. It passes, because no row is readable there under either reading, so it was left as is.mainbrought in Changesets pre-mode exit (c46b5f14c). The changesets' "pre mode" aside was dropped to keep them true.Generated by Claude Code