Repository navigation
Commit 877dc03
feat(security): record platform-admin standing on the audit ledger at boot (#19194)
Fixes #18412
Clause-②: no
The walled boot records platform-admin standing on the existing audit
ledger, so «who held administrator standing, and from when» survives the
move off the stored grant row.
## What was lost, and where it went
Platform-admin standing moved from a **stored grant row** to
**config-derived, request-time resolution** (#11663 re-anchor,
ADR-0131). The row carried its own history; config carries none. After
the migration the only trace of a grant or a revocation was a change to
`OS_PLATFORM_OWNER_EMAIL` plus a restart — the product keeps no
environment-variable history and an auditor cannot read one.
`sys_audit_log` recorded the ACTIONS all along; what had no writer at
all was the **basis** of the authority behind them.
The answer was already being computed and thrown away:
`resolvePlatformAdminStanding` builds the per-entry summary at every
walled boot and `bootstrap-platform-admin.ts` logs it at `info`.
## What changed
**`@objectstack/plugin-audit`** — `sys_audit_log.action` declares one
new value, `platform_admin_standing_change`, WRITER-FIRST (the only way
a value is allowed onto that enum, #8147 / #8315). Its writer is named
in `ACTIONS_WITH_WRITERS`, and the four generated translation bundles
gain the option leaf. Its shipped surfaces are the unfiltered `recent`
and `all_events` views, so the value is not an empty widget (审计面宁窄勿谎). ⛔
Deliberately NOT added to the `config_changes` filter — that view
answers «which setting changed», and standing is not a `sys_setting`. ⛔
No new list view, ⛔ no new object, ⛔ no new configuration key.
**`@objectstack/plugin-security`** — `platform-admin-standing-audit.ts`
holds the row shape and the change detector, PURE, so a pin tests the
writer's row rather than a hand-written copy.
`bootstrap-platform-admin.ts` compares the resolved standing against the
last snapshot already on the ledger and writes **one entry per CHANGE of
standing**, plus the **first-boot baseline**. A restarted rig writes
nothing. `old_value` / `new_value` state both sides of the delta, and
`old_value` is null on the baseline row and only there.
**The `single` posture is untouched.** It still promotes the first
registrant and still writes a durable grant row; the durability this
restores is walled-posture-specific.
**Nothing here widens who holds standing or what standing permits.** The
derivation site (`core/src/security/resolve-authz-context.ts`
§6b-config) is not in this diff. This adds a RECORD of authority, never
a grant of it — a maintainer floor that is ⛔ not this lane's to move,
and it was not moved.
### ⭐ The declared exception — `organization_id` is NULL, and ⛔ it is
not a gap to repair
Recorded in **two** places as the ruling requires: a block comment
beside the stamp in `platform-admin-standing-audit.ts`, and on the card.
A third copy is a pin, because nothing else can see this: every
`sys_audit_log` field is `readonly: true` and `validateRecord` skips
readonly fields, so a tenant id stamped here would be accepted by the
entire stack.
The record is deployment-level by construction. ADR-0131 §1.5 「The
rejected middle: a platform organization」 considered inventing an owner
for deployment-level rows and rejected it in its own words — 「it is the
natural repair and the wrong one … exists only to give NULL a new name」.
A tenant id would file a whole-deployment fact behind one tenant's wall;
one row per organization is the fan-out §1.5 names as wrong; and the
first-boot baseline settles it structurally, since it is written before
any `sys_organization` row exists at all. This follows the tree's four
existing deployment-level audit writers (`audit-writers.ts`,
`read-audit.ts`, `auth-event-audit.ts`, `config-change-audit.ts`, each
stamping `tenantId ?? null`), and is the shape ADR-0131 D7 will later
make structural by dropping the column. Maintainer ruling 2026-09-18,
director batch #153 item 2, 「其他同意」.
### Three states at the ledger, because the third is the one a
two-valued read loses
- **not mounted** — skip, silently. `plugin-audit` is OPTIONAL; a host
that never mounted it declined to have a ledger rather than failing to
write one (the #18368 lesson, one file over).
- **readable** — compare, and write only on a difference.
- ⛔ **the read was REFUSED** — write NOTHING and report it. A refusal is
not 「no record exists」: reading it that way files a fresh baseline on
every boot of that rig, which is precisely the per-boot noise the ruling
rejected. An insert that fails on a mounted ledger reports a durability
degradation on `error` and never fails the boot.
## Evidence
**No new engine write call site.** The ledger insert routes through this
file's existing `tryInsert` door rather than a second `ql.insert` beside
it — `check:tenant-audit-census` stays flat at 227 certified write call
sites (a first draft that spelled its own `ql.insert` was refused by
that gate as unplaceable, which is how the reuse was found).
**Gates.** All **68** derived families run, all exit 0, **0
NOT-MEASURED**, 0 UNRUN — derived from the real diff with `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths passed) and reconciled with
`--ran`, exit codes captured before any pipe. Four of them found real
defects in the first draft and were fixed rather than routed around:
`check:doc-authoring` (a tracker id inside a runtime string),
`check:engine-double-contract` (the new double's `update()` now opens
with `assertEngineUpdateDispatch`), `check:where-matcher` (the double's
`where` now REFUSES the combinators it does not implement), and
`check:tenant-audit-census` above. `check:i18n`,
`check:dual-build-cjs-loads` and `check:type-check-debt` first answered
`PREREQUISITE NOT MET` (exit 3, ⛔ not a pass); they were re-run green
after `turbo run build` over the whole workspace, 72/72 tasks.
**Tests.** `@objectstack/plugin-security` 114 files / 2221 tests passed;
`@objectstack/plugin-audit` 24 files / 353 tests passed; `typecheck`
green on both. Repo-wide `pnpm lint` green at `a228d7452` (the final
commit), so no narrowing was claimed.
**Ablation — every new pin was watched fail, with the mutation proved on
disk and the restore proved by blob equality, not by an exit code.** Run
through `scripts/ablation-replace.mjs`; each leg restored with `blob ==
HEAD` and an empty `git diff HEAD`.
| leg | mutation | result |
|---|---|---|
| the NULL-organization exception undone | `row.organization_id = null`
becomes `'org_platform'` | **2 failed / 13 passed** — the exception pin
and the first-boot baseline pin |
| change detection always reports a change | `previousSerialized !==
nextSerialized` becomes `true` | **2 failed / 13 passed** — the
restarted-rig pin and the explicit-null pin |
| a refused read conflated with «no prior row» | the refusal observer
swallows instead of recording | **1 failed / 14 passed** — the refusal
pin only |
| the action value leaves the enum, its writer stays | delete the enum
member | **1 failed / 8 passed** — `the action enum declares exactly the
actions that have a writer` |
The first two legs are deliberately separate from the last two: each
reds a different pin family, which is what makes them four measurements
rather than one restated.
## Acceptance notes
- **No intersection with the H17 on-hold trigger-file index.**
`packages/plugins/plugin-security/src/security-plugin.ts` is ⛔ NOT in
this diff — #7401 and #13542 both declare it as their trigger file and
both stay `pm:on-hold`, untouched and unrelabelled.
- ⛔ **Zero `packages/spec`** and ⛔ zero `content/docs/releases/**`,
verified on the final diff.
- **#15193 is not touched.** It stays open; this card is not an ADR-0131
execution card and carries no `Blocked-by:` line to it.
- `scripts/engine-double-contract.pinned.json` is outside the dispatch's
declared file surface. It is the PINNED ledger, not the shrink-only
exemption baseline: the gate itself printed 「New pinned coverage is GOOD
… Run `--write` and commit」, and the regeneration reported 「0 added or
grown, 0 lost」 in seam terms. Named here because a surface note belongs
in the open, not because anything was adjudicated.
- noted, not filed: the four incumbent `sys_audit_log` writers still
comment that `organization_id` 「only exists in multi-tenant
deployments」, which `spec/src/data/injected-system-columns.ts`
contradicts — the column is injected unless the object declares
`systemFields.tenant: false` or `tenancy.enabled: false`. Behaviour is
unaffected because every writer probes the schema first, so it is a
wrong comment rather than a defect. Successor: the ADR-0131 D7 execution
card for `sys_audit_log`. Carried forward from
`issuecomment-5717560616`; ⛔ not re-measured here and ⛔ not widened into
this PR.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 74554a3 commit 877dc03
11 files changed
Lines changed: 946 additions & 7 deletions
File tree
- .changeset
- packages/plugins
- plugin-audit/src
- objects
- translations
- plugin-security/src
- scripts
| 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 | + | |
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
82 | 87 | | |
83 | 88 | | |
84 | 89 | | |
| |||
Lines changed: 31 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
182 | 202 | | |
183 | | - | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
184 | 214 | | |
185 | 215 | | |
186 | 216 | | |
| |||
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
Lines changed: 166 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 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 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
94 | 238 | | |
95 | 239 | | |
96 | 240 | | |
| |||
226 | 370 | | |
227 | 371 | | |
228 | 372 | | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
229 | 380 | | |
230 | 381 | | |
231 | 382 | | |
232 | 383 | | |
233 | 384 | | |
234 | 385 | | |
235 | 386 | | |
236 | | - | |
| 387 | + | |
| 388 | + | |
237 | 389 | | |
238 | 390 | | |
239 | 391 | | |
| |||
246 | 398 | | |
247 | 399 | | |
248 | 400 | | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
249 | 405 | | |
250 | 406 | | |
251 | 407 | | |
252 | 408 | | |
253 | 409 | | |
| 410 | + | |
254 | 411 | | |
255 | 412 | | |
256 | 413 | | |
| |||
789 | 946 | | |
790 | 947 | | |
791 | 948 | | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
792 | 956 | | |
793 | 957 | | |
794 | 958 | | |
| |||
0 commit comments