Repository navigation
Commit 7ebb543
Fixes #21260
Clause-②: yes (widening)
This executes ruling B on #21175 (comment `5942331027`, maintainer
「同意264」). The compliance ledger gets an audit capability,
`view_all_audit_log`. A caller who holds it is exempt from the ledger's
parent-record read gate. Field-level narrowing (PR #21171) still applies
to that caller. Platform administrators hold it by default. Every other
position holds it only by explicit grant. #21175 is the parent card.
This PR does not close it (it is already closed).
**Status.** Draft. `Clause-②: yes`, so an isolated contract-review-tier
review is owed before enqueue. The PM runs it.
## Cross-lane surfaces, named before the change list
- **`packages/spec/src/security/capabilities.ts`** (`domain:spec`). One
new `PLATFORM_CAPABILITIES` entry. This is cross-lane, as the claim
declares.
- **`packages/spec/src/identity/eval-user.zod.ts`** (`domain:spec`).
This file holds the default grant to platform administrators.
- The claim expected the admin grant path to live in `plugin-security`.
Measured: it lives here. `ADMIN_FULL_ACCESS_CAPABILITIES` is the one
list that two consumers read:
- plugin-security's `admin_full_access` set spreads it;
- core's configured-owner envelope (`resolve-authz-context.ts`) reads
it.
- Adding the capability anywhere else would fork that list. It is
reported here as the claim requires: "If that path lives outside
`plugin-security`, the build reports it before touching it."
-
**`packages/plugins/plugin-security/src/objects/default-permission-sets.test.ts`**.
Test only, no source change in `plugin-security`.
- Its pin on the admin set's exact capability list goes red on any
capability change, by design. The literal now names the new capability,
and its docblock says why.
- A new pin says that no other shipped set carries the capability.
## What changed
1. **The declaration.** `PLATFORM_CAPABILITIES` gains
`view_all_audit_log`:
- label "View All Audit Log", `scope: 'org'` (see "Scope" below);
- its description names exactly what it lifts and what still applies.
- `PLATFORM_CAPABILITY_NAMES` derives from the list, so the seeder
(`bootstrapSystemCapabilities`), the authoring lint and the anchor floor
pick it up without a second edit.
2. **The default holder.**
`ADMIN_FULL_ACCESS_CAPABILITIES.systemPermissions` lists it. Platform
administrators hold it both ways:
- through the `admin_full_access` grant;
- through the configured-owner envelope.
- There is no role-name test anywhere.
3. **The exemption.** `parent-record-read-gate.ts` is the shared
mechanism. It gains one declared field,
`ParentRecordGate.exemptCapability`, and one check at the top of
`computeParentRecordFilter`, before anything is scanned or ANDed in.
- The check reads the caller's resolved `systemPermissions`: the
capability set that the request's authorization resolver
(`resolveAuthzContext`, `assembleExecutionContext`) stamps on the
execution context.
- Every other runtime capability check outside plugin-security reads
that same set, for example the `sys_record_share` read scope and the
object-schema mask exemption. No new resolver is added.
- Only the ledger gate declares the field (`LEDGER_AUDIT_CAPABILITY` in
`audit-log-read-visibility.ts`). The activity stream's gate declares
none and says so.
4. **The anchor floor (`high-privilege.ts`).** No edit. That file's
platform floor is `PLATFORM_CAPABILITY_NAMES` itself, so declaring the
capability puts it on the floor.
- Reading: it unlocks ledger rows that the parent-record gate otherwise
withholds. So the anchor-laundering argument applies, and a set carrying
it must never bind to `everyone` or `guest`, even when an app declares a
capability of the same name.
- A pin in `high-privilege.test.ts` measures this. It refuses for
`everyone` and for `guest`, with the name declared.
5. **Docs.** `content/docs/permissions/record-view-auditing.mdx`, the
ledger reader sentence, which PR #21194 wrote. It now names the
capability, who holds it by default, the field-level narrowing, and the
organization bound. Old and new text are in the report.
6. **Pins made false by this ruling.**
- `audit-log-parent-read-gate.dogfood.test.ts` said the admin is not
served a deleted record's rows. The admin is now a default holder: that
pin now asserts the reader is served none of them and the admin is
served both.
- The comment in `audit-log-field-values.dogfood.test.ts` is corrected
the same way.
7. **Changeset.** `@objectstack/spec` minor and
`@objectstack/plugin-audit` minor, carrying `Clause-②: yes (widening)`.
## Measured before the change (the gate as landed)
Real boot through the public data doors: `bootStack`, the real
SecurityPlugin, the SQL driver, REST, auth, and AuditPlugin's CRUD
mirror and auth-event sink.
- **At base `d2bc644f2`, with `dist` built from that tree.** A member
holding the ledger grant was served:
- none of the deleted record's rows;
- `404` on the sign-out row;
- `404` on a private record's row;
- a total of 2 against 2,053 at rest on a broad read past the 2,000-row
bound.
- The platform administrator was served none of the deleted record's
rows. Its resolved capability set held the seven existing capabilities.
- **With only the new exemption line removed, so the gate is exactly as
#21194 landed it.** Platform administrator and ledger-grant member,
both:
- deleted-record list 0;
- `delete` row `404`, sign-out row `404`, ended session's sign-in row
`404`;
- broad read total 2 against 2,054 at rest.
- This is A, as the dispatch expected.
## Scope: `org`, measured rather than assumed
- The dispatch default was `platform` unless the build measured that the
ledger is org-scoped. It is.
- The registry provisions `organization_id` on `sys_audit_log`.
- The CRUD mirror stamps each row with the record's organization.
- Measured on a real `isolated` boot, with two organizations each
created by its owner and the first owner granted the capability (the
readings below are row counts):
| Caller | Rows about the deleted record in its own org | Rows about the
other org's deleted record |
|---|---|---|
| Holder (org A owner) | 2 | 0 |
| Non-holder (org B owner) | 0 | 0 |
| Platform administrator | 2 | 2 |
- The capability lifts only the parent-record gate. The tenant wall
still bounds a holder to its own organization's rows. The platform
administrator reaches every organization through its own wall bypass,
not through this capability.
- This is now a pin, in the second block of the new dogfood file.
## Pins
| Pin | Where |
|---|---|
| A holder is served the deleted-record rows (list and by id), the
sign-out row, the ended session's sign-in row, a record it cannot open,
and a broad read past the bound whole. | dogfood |
| Field narrowing still applies to the holder: a withheld field's value
is absent from the deleted record's served snapshots, a plain field's
value is present, and the value is present at rest. | dogfood |
| A non-holder (ledger grant only) gets exactly A: none of these rows,
and a broad-read total below the bound. Its own record's rows are the
control. | dogfood |
| A platform administrator holds the capability by default, read through
`resolveAuthzContext`, and is served the deleted-record, sign-out and
broad-read rows. | dogfood |
| The activity stream's gate is not exempted for a holder or for the
admin. | dogfood; unit `activity-read-visibility.test.ts`; integration
`audit-log-read-visibility.integration.test.ts` (real SQLite driver) |
| The holder is bounded by its organization. | dogfood, second block |
| On every read operation the holder is not narrowed and is not
pre-scanned. Its broad read never meets the bound and logs no warning. A
caller with other capabilities, a non-list or none gets the gate
unchanged. The constant is pinned against `PLATFORM_CAPABILITIES` and
`ADMIN_FULL_ACCESS_CAPABILITIES`. | unit
`audit-log-read-visibility.test.ts` |
| The holder is served every row, deleted and unjudgeable rows included,
and the count matches. | integration |
| The capability is on the anchor floor. | `high-privilege.test.ts` |
| The admin grant carries it, declared `org`. |
`platform-admin-capabilities.test.ts` |
| Only `admin_full_access` among the shipped sets carries it. |
`default-permission-sets.test.ts` |
The pre-scan bound is pinned with a real fixture past it: 2,050 inserted
rows plus the real ones. The constant is not shrunk.
## Ablations
Each leg: mutate through `scripts/ablation-replace.mjs` (anchor must
hit), run the src-aliased plugin-audit suites, rebuild
`@objectstack/plugin-audit`, prove the mutation reached `dist/` with
`scripts/ablation-dist-preflight.mjs`, run the dogfood file, restore
with `git checkout HEAD -- ABSOLUTE_PATH` (blob hash equals HEAD and
`git diff HEAD` is empty), rebuild, and prove the restore in `dist/`. A
trap restores on EXIT, INT and TERM. Every leg turned red, as predicted.
| Leg | Unit and integration | Dogfood |
|---|---|---|
| Exemption removed | 3 red: 2 unit holder pins, 1 integration holder
pin | 8 red: every holder and admin pin, including the org-bound holder
and admin pins. Non-holder and activity pins stayed green. |
| Exemption applied to the activity gate | 2 red: unit activity pin,
integration activity pin | 1 red: the activity pin |
| Capability held by everyone | 19 red: the non-holder pin, plus every
pre-existing ledger narrowing case | 2 red: both non-holder pins |
The first run of the activity leg was a no-op. The replacement contained
its own anchor, and the tool refused it with "anchor count moved 1 to
1". Its readings are void. It was re-run with an anchor the replacement
does not contain, and the readings above are from that run.
## Gates and tests, at `d0a1903b7`
- **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 110 families. All 110
exited 0.
- Two first answered `PREREQUISITE NOT MET` (exit 3) because sibling
packages had no `dist/`: `check:skill-examples` and
`check:dual-build-cjs-loads`. Both were re-run after building those
packages and exited 0.
- `--ran` reconciliation: "110 derived famil(ies) accounted for — 110
run, 0 NOT-MEASURED (a DERIVED zero — all 110 recorded an exit code and
none of them is 3)".
- **Spec artifacts.** `pnpm --filter @objectstack/spec check:generated`:
"All 15 generated artifacts are up to date". Nothing was regenerated,
because no artifact moves for one new list entry.
- **Tests.**
- `@objectstack/spec`: 645 files, 18,316 passed.
- `@objectstack/plugin-security`: 157 files, 3,407 passed.
- `@objectstack/plugin-audit`: 35 files, 542 passed.
- `@objectstack/core`
`resolve-authz-context.platform-admin-config.test.ts`: 31 passed.
- Dogfood, 4 ledger files at this head: 37 passed.
- **Typecheck.** `typecheck` passed for spec, plugin-audit,
plugin-security and dogfood, including each package's test-layer
typecheck.
- **Lint.** ESLint on the 14 touched TS files with `--no-inline-config
--format json`: 14 files linted, 0 errors, 0 warnings.
- The repo's `eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`). So this diff cannot change the verdict on any
untouched file.
- The full `pnpm lint` is left to CI.
## Acceptance notes
- **Agent principals (ADR-0090 D10).** The exemption reads
`systemPermissions` as the shared resolver stamped them. An OAuth agent
principal carries the delegating user's capabilities only when the user
consented to `actions:execute`, and none otherwise
(`assemble-execution-context.ts`). The `sys_record_share` read scope and
the object-schema mask exemption read the same set and follow the same
rule. This is a reading, recorded for the contract reviewer. It is not a
defect filed by this PR.
- **What a holder is served.** A holder is served rows about records of
objects it holds no object-level read on. Field-level security still
narrows the snapshots. This is the breadth "exempt from the
parent-record gate" means, and the capability's description and the docs
page both say so.
- **`PLATFORM_ADMIN_ONLY_CAPABILITIES` in plugin-security is
deliberately untouched.** The new capability is not a platform-admin
marker: it is org-scoped, and an explicit grant must not confer platform
standing.
- **Base.** `origin/main` has moved since `d2bc644f2`. The incoming
commits touch none of this PR's files, so they were not merged here. CI
and the queue validate the merge ref.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4e53056 commit 7ebb543
16 files changed
Lines changed: 692 additions & 18 deletions
File tree
- .changeset
- content/docs/permissions
- packages
- plugins
- plugin-audit/src
- plugin-security/src/objects
- qa/dogfood/test
- spec/src
- identity
- security
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
209 | 209 | | |
210 | 210 | | |
211 | 211 | | |
212 | | - | |
213 | | - | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
214 | 220 | | |
215 | 221 | | |
216 | 222 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
198 | 199 | | |
199 | 200 | | |
200 | 201 | | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
201 | 212 | | |
202 | 213 | | |
203 | 214 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
65 | | - | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
66 | 70 | | |
67 | 71 | | |
68 | 72 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| 51 | + | |
| 52 | + | |
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
| |||
260 | 262 | | |
261 | 263 | | |
262 | 264 | | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
263 | 281 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
12 | 18 | | |
13 | 19 | | |
14 | 20 | | |
| |||
212 | 218 | | |
213 | 219 | | |
214 | 220 | | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
36 | | - | |
37 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
61 | 74 | | |
62 | 75 | | |
63 | 76 | | |
| |||
99 | 112 | | |
100 | 113 | | |
101 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
102 | 123 | | |
103 | 124 | | |
104 | 125 | | |
105 | 126 | | |
106 | 127 | | |
| 128 | + | |
107 | 129 | | |
108 | 130 | | |
109 | 131 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
44 | 54 | | |
45 | 55 | | |
46 | 56 | | |
| |||
92 | 102 | | |
93 | 103 | | |
94 | 104 | | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
95 | 121 | | |
96 | 122 | | |
97 | 123 | | |
| |||
133 | 159 | | |
134 | 160 | | |
135 | 161 | | |
136 | | - | |
137 | | - | |
138 | | - | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
139 | 165 | | |
140 | 166 | | |
141 | 167 | | |
142 | 168 | | |
143 | 169 | | |
144 | 170 | | |
145 | 171 | | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
146 | 176 | | |
147 | 177 | | |
148 | 178 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
361 | 361 | | |
362 | 362 | | |
363 | 363 | | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
364 | 369 | | |
365 | 370 | | |
366 | 371 | | |
| |||
386 | 391 | | |
387 | 392 | | |
388 | 393 | | |
| 394 | + | |
| 395 | + | |
389 | 396 | | |
390 | 397 | | |
391 | 398 | | |
392 | 399 | | |
393 | 400 | | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
394 | 408 | | |
395 | 409 | | |
396 | 410 | | |
| |||
0 commit comments