Skip to content

Commit 450e73b

Browse files
claude[bot]claudeos-project-manager
authored
fix(plugin-auth): report the zero-account boot dead end at kernel:ready (#14866)
* feat(plugin-auth): report the zero-account boot dead end at kernel:ready A deployment with human `sys_user` rows and zero `sys_account` rows cannot be recovered from inside: nobody can sign in, the bootstrap carve-out counts humans and so does not open, `invite_only` refuses self-registration, and no administrator exists to invite anyone. Today it boots silently. Reports it at `kernel:ready`, at `error` level, naming both the consequence and the remedy. Extends the existing walled-owner reporter family rather than opening a parallel one: same hook, and the bounded human-population page is read ONCE and shared with `probeWalledOwnerAccountState`, which now accepts the already-known answer. At most one report per boot — the error subsumes the walled-owner warning when a deployment matches both shapes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8 * test(plugin-auth): pin the zero-account boot report, its controls and its independence Pins the dead-end shape, all four silent controls, the error level with its `warn` fallback, the boot wiring, independence from each of the walled-owner probe's four preconditions, the one-report-per-boot precedence, and that `sys_user` is paged exactly once per boot. Measured while writing this: breaking the declared-owner precondition alone is an unreachable boot — a walled posture with no declared owner refuses startup in `init()` (#11184) — so the reachable no-owner shape is the default deployment, which is what the suite pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8 * chore(changeset): patch changeset for the zero-account boot report Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8 * docs(permissions): re-anchor the system-context census row past the new boot check `check:check-system-context-census` caught the line rot my kernel:ready edit caused: the session-resolution elevation read moved from auth-plugin.ts:1380 to :1405. Re-anchored by the gate's own --fix; no census row's meaning changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8 * fix(plugin-auth): make the boot diagnostic sink's `warn` channel required `check:optional-error-sink` (gate #9754) failed on this branch: with both `error?` and `warn?` optional, `BootDiagnosticLogger` was a type every value of which may print nothing, so the contract permitted silence even though the emitter's explicit fallback branch was careful. Take the gate's own prescribed fix — `warn` becomes required, `error` stays optional. The two shapes the gate forbids are NOT taken: `error` is not made required (falsified, hosts inject reduced sinks), and this is not satisfied with a required `info` (a lost sign-in path reported at `info` is the reassuring half-truth AGENTS.md "Degradation log levels" removes). Fallout, all of it inside this module's own surface: - the emit branch drops its now-dead `?.` on `warn`; the type guarantees the channel, and the surrounding try/catch still holds for a throwing sink. - the "a logger that throws cannot break the boot" double no longer satisfied the type. It carries a real `vi.fn()` warn rather than a cast — a cast would re-open exactly the hole the gate closes — and now also pins that `warn` stays untouched when `error` is present and throws. The host call site is unaffected: `ctx.logger` is the spec `Logger`, whose `warn` is already required. Gate census moves by exactly one, in the intended direction: sinks declaring an optional `error` beside a REQUIRED `warn` 30 -> 31, sinks permitting silence 2 -> 1 (the remainder is the pre-existing baselined one, shrink-only). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8 --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Leehom <pm@objectstack.ai>
1 parent 95fdf62 commit 450e73b

6 files changed

Lines changed: 844 additions & 16 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
Report the zero-account boot dead end at `kernel:ready` (#14353)
6+
7+
A deployment holding human `sys_user` rows and zero `sys_account` rows cannot
8+
be recovered from inside, and until now it booted silently. Nobody can sign in;
9+
the first-account bootstrap carve-out counts humans, and humans exist, so it
10+
does not open; the default `invite_only` audience posture refuses
11+
self-registration; and no administrator exists who could send an invitation.
12+
The only symptom was a 401 on credentials nobody holds.
13+
14+
That state is now reported at `kernel:ready` at `error` level, under the name
15+
`no_sign_in_account_at_boot`, naming both the consequence (the deployment will
16+
keep looking healthy and cannot be recovered from inside) and the remedy
17+
(provision an account out of band, or open the audience posture).
18+
19+
⛔ No admission semantics change. Whether the carve-out should count humans or
20+
logins was ruled on 2026-09-02 (option A — the door does not move); this only
21+
reports.
22+
23+
The check extends the existing `kernel:ready` walled-owner reporter rather than
24+
opening a parallel one: it shares that hook, and the bounded human-population
25+
page is read ONCE per boot and handed to `probeWalledOwnerAccountState`, so no
26+
deployment pages `sys_user` twice. At most one report is emitted per boot — a
27+
deployment matching both shapes gets this error, and the walled-owner warning
28+
is suppressed rather than stacked on top of it.

content/docs/permissions/system-context.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ that silently does not happen.
9797
| 8 | `explain()` may target a principal other than the caller | plugin-security | Get: no `manage_users` / delegated-admin check | `security-plugin.ts:3857` |
9898
| 9 | Anonymous-deny treats the caller as authenticated | core | Get: passes the 401 seam with no `userId` | `anonymous-deny.ts:154` |
9999
| 10 | Permission-set projection middleware skipped | plugin-security | Lose: projection of permission-set-derived columns | `permission-set-projection.ts:1015` |
100-
| 11 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `auth-plugin.ts:1380` |
100+
| 11 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `auth-plugin.ts:1405` |
101101
| 12 | Per-request performance timings disclosed | observability | Get: timing headers a normal caller cannot pull | `perf-timing.ts:474` |
102102
| 13 | Permission-set **overlay discard** skips the tenant-admin assertion | plugin-security | Get: an overlay can be discarded with no authenticated tenant administrator | `permission-set-overlay-discard.ts:142` |
103103
| 14 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `stdio-data-bridge.ts:246` |

packages/plugins/plugin-auth/src/auth-plugin.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ import {
7878
warnIfWalledOwnerCannotVerify,
7979
type WalledOwnerAccountState,
8080
} from './walled-owner-verification-path.js';
81+
import {
82+
probeSignInReachability,
83+
reportIfNoSignInAccountExists,
84+
} from './boot-sign-in-reachability.js';
8185
import { judgePlatformAdmin, isPlatformAdminUser, type PlatformAdminActor } from './platform-admin-gate.js';
8286
import {
8387
runAdminBanUser,
@@ -1036,21 +1040,42 @@ export class AuthPlugin implements Plugin {
10361040
// hook below (registration order), so the probe reads the pre-seed
10371041
// store — the predicate's dev-seed clauses are written for exactly
10381042
// that reading.
1043+
let ql: IDataEngine | undefined;
1044+
try { ql = ctx.getService<IDataEngine>('objectql'); } catch { ql = undefined; }
1045+
1046+
// [#14353] "Can ANYONE sign in?" — asked UNCONDITIONALLY, because it is
1047+
// independent of all four preconditions below: a deployment that is
1048+
// unwalled, or declares no owner, or wires an email transport is just as
1049+
// unrecoverable when it has human rows and no `sys_account` row. This is
1050+
// the family's ONE store read: the human-population page is paged here
1051+
// and the answer handed to the walled-owner probe, so no boot pages
1052+
// `sys_user` twice. Cost on a fresh store is a single bounded page.
1053+
const reachability = await probeSignInReachability(ql);
1054+
const deadEnd = reportIfNoSignInAccountExists(reachability, ctx.logger);
1055+
10391056
let ownerAccountState: WalledOwnerAccountState = 'unknown';
10401057
if (
10411058
!hasEmailTransport &&
10421059
!hasFederatedSignIn &&
10431060
postureEnforcesWall(resolveTenancyPosture()) &&
10441061
resolvePlatformOwnerEmail()
10451062
) {
1046-
let ql: IDataEngine | undefined;
1047-
try { ql = ctx.getService<IDataEngine>('objectql'); } catch { ql = undefined; }
1048-
ownerAccountState = await probeWalledOwnerAccountState(ql);
1063+
ownerAccountState = await probeWalledOwnerAccountState(ql, {
1064+
humanUsers: reachability.humanUsers,
1065+
});
1066+
}
1067+
// [#14353] ONE report per boot. A deployment can match both shapes at
1068+
// once (no accounts AND a declared owner that cannot verify); the
1069+
// no-sign-in error strictly subsumes the walled-owner warning there —
1070+
// an owner who cannot reach platform-admin standing is moot when nobody
1071+
// can sign in at all — so the warning is suppressed rather than stacked
1072+
// on top of it. When the error did not fire, the warning is untouched.
1073+
if (!deadEnd) {
1074+
warnIfWalledOwnerCannotVerify(
1075+
{ hasEmailTransport, hasFederatedSignIn, ownerAccountState },
1076+
ctx.logger,
1077+
);
10491078
}
1050-
warnIfWalledOwnerCannotVerify(
1051-
{ hasEmailTransport, hasFederatedSignIn, ownerAccountState },
1052-
ctx.logger,
1053-
);
10541079
});
10551080

10561081
// Dev-only: provision a known, loginable platform admin on an empty DB.

0 commit comments

Comments
 (0)