Skip to content

Commit 877dc03

Browse files
huangyiireneclaude
andauthored
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
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/plugin-security": minor
3+
"@objectstack/plugin-audit": minor
4+
---
5+
6+
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 (#18412).
7+
8+
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.
9+
10+
The answer was already being computed and thrown away: `resolvePlatformAdminStanding` builds the per-entry summary at every walled boot and the bootstrap logs it at `info`.
11+
12+
- **`@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). Its rows appear on the shipped, unfiltered `recent` and `all_events` views; ⛔ no new list view, ⛔ no new object, ⛔ no new configuration key.
13+
- **`@objectstack/plugin-security`** — the walled bootstrap 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. Each row carries, per declared entry, the declared spelling, whether an account exists, whether it is verified, and which user id holds standing; `old_value` and `new_value` state both sides of the delta, and `old_value` is null on the baseline row and only there.
14+
- **The `single` posture is untouched.** It still promotes the first registrant and still writes a durable grant row, so the durability this restores is walled-posture-specific.
15+
- ⭐ **`organization_id` is NULL on this row, deliberately and by maintainer ruling** (2026-09-18, director batch #153 item 2). The record is deployment-level by construction: ADR-0131 §1.5 rejects inventing a platform organization 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, and the first-boot baseline is written before any `sys_organization` row exists at all. This follows the tree's four existing deployment-level audit writers, and is the shape ADR-0131 D7 will later make structural by dropping the column. The exception is recorded beside the write, on the card, and in a pin — ⛔ it is not a gap waiting to be repaired.
16+
- **Nothing here widens who holds standing or what standing permits.** The derivation site is untouched; this adds a RECORD of authority, never a grant of it.
17+
- **Best-effort, and never fatal to boot.** A deployment that never mounted the optional `@objectstack/plugin-audit` skips silently — an unmounted ledger is a composition choice, not a fault. A ledger read that is REFUSED writes nothing and says so: «cannot tell» is not «first boot», and reading it that way would file a fresh baseline on every restart. A mounted ledger whose insert fails reports a durability degradation on the `error` channel.

‎packages/plugins/plugin-audit/src/objects/sys-audit-log-retired-actions.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ const ACTIONS_WITH_WRITERS: ReadonlyArray<readonly [action: string, writer: stri
7979
['logout', 'plugin-audit/src/auth-event-audit.ts — createAuthEventAuditSink (#8144)'],
8080
['config_change', 'service-settings/src/config-change-audit.ts — CONFIG_CHANGE_ACTION (#8145)'],
8181
['import', 'plugin-auth/src/admin-import-users.ts — run-level row, record_id null'],
82+
[
83+
'platform_admin_standing_change',
84+
'plugin-security/src/bootstrap-platform-admin.ts — recordPlatformAdminStandingChange, '
85+
+ 'row shape in plugin-security/src/platform-admin-standing-audit.ts (#18412)',
86+
],
8287
];
8388

8489
/** Option values declared by the `action` select field. */

‎packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,38 @@ export const SysAuditLog = ObjectSchema.create({
179179
// record-detail views on per-object opt-in, batched off the request path —
180180
// so a deployment that opts nothing in never writes one, and the value is
181181
// narrow rather than absent (审计面宁窄勿谎).
182+
// [#18412, maintainer ruling 2026-09-18 — director batch #153 item 2]
183+
// `platform_admin_standing_change` joins the enum WRITER-FIRST, the only
184+
// way a value is allowed onto this surface. Its writer is
185+
// `plugin-security/src/bootstrap-platform-admin.ts`
186+
// (`recordPlatformAdminStandingChange`, row shape in
187+
// `platform-admin-standing-audit.ts`), which records WHO holds
188+
// platform-administrator standing and from when — the property lost when
189+
// standing moved from a stored grant row to config-derived, request-time
190+
// resolution (#11663, ADR-0131). One entry per CHANGE of standing plus a
191+
// first-boot baseline, so a frequently restarted rig writes nothing.
192+
//
193+
// Its shipped surfaces are the unfiltered `recent` and `all_events` views
194+
// above: the rows are visible on both, so this value is not the empty
195+
// widget the 2026-08-12 ruling named (审计面宁窄勿谎). ⛔ It is deliberately
196+
// NOT added to the `config_changes` filter — that view's label and columns
197+
// answer 「which setting changed」, and standing is not a `sys_setting`.
198+
//
199+
// ⛔ Dotless `snake_case`, per Prime Directive #3 and every incumbent value
200+
// here; the ruling's illustrative `platform_admin_standing.changed` carried
201+
// a dot and was written 「e.g.」.
182202
action: Field.select(
183-
['create', 'read', 'update', 'delete', 'login', 'logout', 'config_change', 'import'],
203+
[
204+
'create',
205+
'read',
206+
'update',
207+
'delete',
208+
'login',
209+
'logout',
210+
'config_change',
211+
'import',
212+
'platform_admin_standing_change',
213+
],
184214
{
185215
label: 'Action',
186216
required: true,

‎packages/plugins/plugin-audit/src/translations/en.objects.generated.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
3434
login: "login",
3535
logout: "logout",
3636
config_change: "config_change",
37-
import: "import"
37+
import: "import",
38+
platform_admin_standing_change: "platform_admin_standing_change"
3839
}
3940
},
4041
user_id: {

‎packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
3434
login: "Inicio de sesión",
3535
logout: "Cierre de sesión",
3636
config_change: "Cambio de configuración",
37-
import: "Importar"
37+
import: "Importar",
38+
platform_admin_standing_change: "Cambio de acceso de administrador de plataforma"
3839
}
3940
},
4041
user_id: {

‎packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
3434
login: "ログイン",
3535
logout: "ログアウト",
3636
config_change: "構成変更",
37-
import: "インポート"
37+
import: "インポート",
38+
platform_admin_standing_change: "プラットフォーム管理者権限の変更"
3839
}
3940
},
4041
user_id: {

‎packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
3434
login: "登录",
3535
logout: "登出",
3636
config_change: "配置变更",
37-
import: "导入"
37+
import: "导入",
38+
platform_admin_standing_change: "平台管理员权限变更"
3839
}
3940
},
4041
user_id: {

‎packages/plugins/plugin-security/src/bootstrap-platform-admin.ts‎

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,151 @@ import {
9090
reportSeedWriteRefusals,
9191
type SeedWriteRefusals,
9292
} from './per-organization-catalog.js';
93-
import { resolvePlatformAdminStanding } from './platform-admin-service.js';
93+
import {
94+
resolvePlatformAdminStanding,
95+
type PlatformAdminStandingEntry,
96+
} from './platform-admin-service.js';
97+
import {
98+
buildPlatformAdminStandingRow,
99+
platformAdminStandingChanged,
100+
platformAdminStandingSnapshot,
101+
PLATFORM_ADMIN_STANDING_ACTION,
102+
PLATFORM_ADMIN_STANDING_LEDGER,
103+
readRecordedStandingSnapshot,
104+
serializePlatformAdminStandingSnapshot,
105+
} from './platform-admin-standing-audit.js';
106+
107+
/**
108+
* The order the standing-audit read states TO THE DRIVER.
109+
*
110+
* `created_at` and not `id`: `sys_audit_log` ids carry no ordering, while
111+
* `created_at` is the field every shipped list view on that object already
112+
* sorts by, and it is the column the row's own meaning rests on. The read is
113+
* capped at one row, and a capped read without an order returns whichever row
114+
* that driver produced first — see {@link tryFind}.
115+
*/
116+
const STANDING_AUDIT_SCAN_ORDER: { field: string; order: 'asc' | 'desc' }[] = [
117+
{ field: 'created_at', order: 'desc' },
118+
];
119+
120+
/**
121+
* [#18412] Record a CHANGE of platform-admin standing on the existing audit
122+
* ledger, and write nothing when nothing changed.
123+
*
124+
* ## Three states, and the third is the one a two-valued read would lose
125+
*
126+
* - the ledger is not mounted → SKIP, silently. `sys_audit_log` belongs to the
127+
* OPTIONAL `@objectstack/plugin-audit`; a host that never mounted it
128+
* (`serve --preset minimal`, an EE host that composes no audit) has not
129+
* FAILED to write a row, it declined to have a ledger. Attempting the insert
130+
* would throw and be reported as a degradation on a deployment behaving
131+
* exactly as composed — the #18368 lesson, one file over.
132+
* - the ledger is mounted and the last row is readable → compare, and write
133+
* only on a difference.
134+
* - ⛔ the read was REFUSED → write NOTHING and say so. A refusal is not
135+
* 「no record exists」: reading it that way writes a fresh baseline on every
136+
* boot, which is precisely the per-boot noise the ruling rejected. The
137+
* record stays silent for this boot and the next successful read catches up,
138+
* because the comparison is against the ledger rather than against a memo.
139+
*
140+
* Best-effort throughout: an audit write must never be what stops a deployment
141+
* booting, and this runs after the bootstrap has already done its work.
142+
*/
143+
async function recordPlatformAdminStandingChange(
144+
ql: any,
145+
standing: readonly PlatformAdminStandingEntry[],
146+
logger?: BootstrapOptions['logger'] & { debug?: (message: string) => void },
147+
): Promise<void> {
148+
// ⛔ Ask before writing, and only an engine that ANSWERS licenses the skip.
149+
// `getSchema` is an ObjectQL member, not an `IDataEngine` one, so an engine
150+
// that does not carry it has told us NOTHING about the ledger — that case
151+
// leaves the write attempted rather than skipped.
152+
const getSchema = ql?.getSchema;
153+
let ledgerSchema: any;
154+
if (typeof getSchema === 'function') {
155+
try {
156+
ledgerSchema = ql.getSchema(PLATFORM_ADMIN_STANDING_LEDGER);
157+
} catch {
158+
ledgerSchema = undefined;
159+
}
160+
if (ledgerSchema == null) return;
161+
}
162+
163+
const declared: string[] = Array.isArray(ledgerSchema?.fields)
164+
? ledgerSchema.fields.map((f: any) => f?.name).filter(Boolean)
165+
: ledgerSchema?.fields && typeof ledgerSchema.fields === 'object'
166+
? Object.keys(ledgerSchema.fields)
167+
: [];
168+
const declaresField = (field: string): boolean => declared.includes(field);
169+
170+
let refused: unknown;
171+
const previousRows = await tryFind(
172+
ql,
173+
PLATFORM_ADMIN_STANDING_LEDGER,
174+
{ action: PLATFORM_ADMIN_STANDING_ACTION },
175+
1,
176+
STANDING_AUDIT_SCAN_ORDER,
177+
undefined,
178+
(e) => {
179+
refused = e;
180+
},
181+
);
182+
if (refused !== undefined) {
183+
const message =
184+
'[security] platform-admin standing was NOT recorded on this boot: the audit ledger ' +
185+
`(${PLATFORM_ADMIN_STANDING_LEDGER}) refused the read of the last recorded snapshot, so ` +
186+
'this boot cannot tell whether standing changed. ⛔ Nothing was written — writing a ' +
187+
'baseline here would file a fresh row on every boot. The next boot whose read succeeds ' +
188+
'records the current standing. Cause: ' +
189+
String((refused as any)?.message ?? refused);
190+
if (logger?.error) logger.error(message);
191+
else logger?.warn?.(message);
192+
return;
193+
}
194+
195+
const snapshot = platformAdminStandingSnapshot(standing);
196+
const serialized = serializePlatformAdminStandingSnapshot(snapshot);
197+
const previousSerialized = readRecordedStandingSnapshot(previousRows[0]);
198+
if (!platformAdminStandingChanged(previousSerialized, serialized)) {
199+
// ⛔ The card id stays in this comment and out of the STRING: a runtime
200+
// line reaches operators, who have no tracker to resolve `#NNNN` against
201+
// (#18412; `check:doc-authoring`).
202+
logger?.debug?.(
203+
'[security] platform-admin standing is unchanged since the last recorded entry — no ' +
204+
'audit row written. One entry per CHANGE of standing is the recorded shape.',
205+
);
206+
return;
207+
}
208+
209+
const row = buildPlatformAdminStandingRow({
210+
snapshot,
211+
previousSerialized,
212+
declaresOrganizationId: declaresField('organization_id'),
213+
declaresActor: declaresField('actor'),
214+
});
215+
// ⛔ Through this file's ONE write door, not a second `ql.insert` beside it.
216+
// `tryInsert` already threads the system execution context every write here
217+
// needs, and a second door with identical semantics would be a second place
218+
// for that context to be forgotten — it is also a second row on the
219+
// tenant-audit write-call-site census for one write.
220+
let insertRefusal: unknown;
221+
await tryInsert(ql, PLATFORM_ADMIN_STANDING_LEDGER, row, undefined, (e) => {
222+
insertRefusal = e;
223+
});
224+
if (insertRefusal !== undefined) {
225+
// The ledger IS mounted (or could not be asked) and the insert still
226+
// failed, which is AGENTS.md's durability degradation to the letter: the
227+
// deployment's administrators just changed and the record that was
228+
// supposed to outlive the process is missing. Loud, and never fatal.
229+
const message =
230+
'[security] platform-admin standing CHANGED and the audit row was NOT written — the ' +
231+
'durable record of who administers this deployment is missing for this change, and ' +
232+
'nothing retries it. Boot itself is unaffected. Cause: ' +
233+
String((insertRefusal as any)?.message ?? insertRefusal);
234+
if (logger?.error) logger.error(message);
235+
else logger?.warn?.(message);
236+
}
237+
}
94238

95239
interface BootstrapOptions {
96240
/** Logger from PluginContext. */
@@ -226,14 +370,22 @@ async function tryFind(
226370
limit = 100,
227371
orderBy?: { field: string; order: 'asc' | 'desc' }[],
228372
offset?: number,
373+
// ⛔ "Refused" and "empty" are not the same answer, and on most call sites in
374+
// this file the difference is harmless because `[]` is the conservative
375+
// reading. On the standing-audit read below it is NOT: `[]` would read as
376+
// "no record has ever been written", which writes a duplicate baseline on
377+
// every boot of a rig whose driver refused the query. A caller that cannot
378+
// afford that conflation passes this and is told.
379+
onRefusal?: (error: unknown) => void,
229380
): Promise<any[]> {
230381
try {
231382
const query: Record<string, any> = { where, limit };
232383
if (orderBy) query.orderBy = orderBy;
233384
if (offset !== undefined) query.offset = offset;
234385
const rows = await ql.find(object, query, { context: SYSTEM_CTX });
235386
return Array.isArray(rows) ? rows : [];
236-
} catch {
387+
} catch (e) {
388+
onRefusal?.(e);
237389
return [];
238390
}
239391
}
@@ -246,11 +398,16 @@ async function tryFind(
246398
// boots. See `reportSeedWriteRefusals` in `per-organization-catalog.ts`.
247399
async function tryInsert(
248400
ql: any, object: string, data: any, refusals?: SeedWriteRefusals,
401+
// Symmetric with {@link tryFind}'s observer, and for the same reason: a
402+
// caller outside the SEED pass needs the refusal itself, not a `null` that
403+
// the seed reporter will later summarize on a channel that is not its own.
404+
onRefusal?: (error: unknown) => void,
249405
): Promise<any | null> {
250406
try {
251407
return await ql.insert(object, data, { context: SYSTEM_CTX });
252408
} catch (e) {
253409
refusals?.record(object, e);
410+
onRefusal?.(e);
254411
return null;
255412
}
256413
}
@@ -789,6 +946,13 @@ export async function bootstrapPlatformAdmin(
789946
`at request time. ${summary}`,
790947
{ standing: standing.map((s) => ({ ...s })) },
791948
);
949+
// [#18412] …and the SAME answer, recorded durably. The log line above is
950+
// the operator's first sight of it and nothing else: it is not queryable,
951+
// it does not survive the process, and an auditor asking 「who held
952+
// administrator standing three months ago, and since when」 cannot read it.
953+
// This is the write that makes that question answerable — one entry per
954+
// CHANGE of standing, plus the first-boot baseline.
955+
await recordPlatformAdminStandingChange(ql, standing, logger);
792956
return {
793957
seeded: seededCount,
794958
adminPromoted: false,

0 commit comments

Comments
 (0)