Repository navigation
Commit d11fa8c
feat(platform-objects)!: the identity objects declare their default row scope, and the registry refuses an extend that sets one (#24623)
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).
- `sys_user` is the one merge, per ruling A′ and rider 6255156624. It
declares one `select` policy: `id == current_user.id || id in
current_user.org_user_ids`.
- The platform-global objects declare none (item 5).
- The shipped sets keep their spreads. #24484 removes them after this
card.
- **`@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. #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:
- `object-default-row-scope.test.ts` (#24340's pin). Its "undeclared"
shape spread the real `SysUser`, 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`. The `mcp_agent_data_write`
persona carried `userId: 'agent_x'`, and no producer emits that shape.
The one producer of an agent principal is core's
`assembleExecutionContext`, which keeps `userId` as the delegating
human. With `agent_x`, the agent's own leg applied each SCIM default as
`agent_x` and read nothing. With the producer's shape it reads what it
read before. The persona now carries the delegator's `userId`.
**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
- **Same-semantic pins.** I grepped
`ExtensionDefaultRowScopeRefusedError`, the refusal message, and
`defaultRowScope` beside `'extend'`. They are pinned in
`objectql/src/registry-extension-default-row-scope-refused.test.ts` and
the new integration test only.
- **Pins on `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-gate` and `platform-ownership-policies` (plugin-security);
- `sys-user-self-service-route` (plugin-auth);
- the two #24495 and #24518 twins (service-automation);
- `identity-admin-fields-org-peer.dogfood` and
`zero-set-masking.dogfood`, run locally: 13 / 13.
- **Pins on served identity rows in no-baseline compositions** pass:
`resolve-execution-context.oauth-organization` (runtime) and
`serve-verify-security-parity.contract` (cli).
- **What moved** is the two pins in 1 above.
## 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](https://claude.ai/code/session_01FatVyDAFEST48jZDQcbheU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d117382 commit d11fa8c
36 files changed
Lines changed: 1535 additions & 7 deletions
File tree
- .changeset
- packages
- objectql/src
- platform-objects/src/identity
- plugins
- plugin-audit/src/objects
- plugin-security
- src
- objects
- services/service-automation/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1465 | 1465 | | |
1466 | 1466 | | |
1467 | 1467 | | |
| 1468 | + | |
| 1469 | + | |
| 1470 | + | |
| 1471 | + | |
| 1472 | + | |
| 1473 | + | |
| 1474 | + | |
| 1475 | + | |
| 1476 | + | |
| 1477 | + | |
| 1478 | + | |
| 1479 | + | |
| 1480 | + | |
| 1481 | + | |
| 1482 | + | |
| 1483 | + | |
| 1484 | + | |
| 1485 | + | |
| 1486 | + | |
| 1487 | + | |
| 1488 | + | |
| 1489 | + | |
| 1490 | + | |
| 1491 | + | |
| 1492 | + | |
| 1493 | + | |
| 1494 | + | |
| 1495 | + | |
| 1496 | + | |
| 1497 | + | |
| 1498 | + | |
| 1499 | + | |
| 1500 | + | |
| 1501 | + | |
| 1502 | + | |
| 1503 | + | |
| 1504 | + | |
| 1505 | + | |
| 1506 | + | |
| 1507 | + | |
| 1508 | + | |
| 1509 | + | |
| 1510 | + | |
| 1511 | + | |
| 1512 | + | |
| 1513 | + | |
| 1514 | + | |
| 1515 | + | |
| 1516 | + | |
| 1517 | + | |
| 1518 | + | |
| 1519 | + | |
| 1520 | + | |
| 1521 | + | |
| 1522 | + | |
| 1523 | + | |
| 1524 | + | |
| 1525 | + | |
| 1526 | + | |
1468 | 1527 | | |
1469 | 1528 | | |
1470 | 1529 | | |
| |||
3246 | 3305 | | |
3247 | 3306 | | |
3248 | 3307 | | |
| 3308 | + | |
| 3309 | + | |
| 3310 | + | |
| 3311 | + | |
| 3312 | + | |
| 3313 | + | |
| 3314 | + | |
| 3315 | + | |
| 3316 | + | |
3249 | 3317 | | |
3250 | 3318 | | |
3251 | 3319 | | |
| |||
0 commit comments