Repository navigation
A deployment with human rows and zero sys_account rows boots silently into an unrecoverable state — say so loudly at kernel:ready #14353
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 2, 2026 Triage addendum — read this before writing a new boot check. Found while grading the parent (#14349) against
origin/main@72adb7f.A
kernel:readyreporter for this neighbourhood already exists.packages/plugins/plugin-auth/src/auth-plugin.ts:945-969registers a hook that probesprobeWalledOwnerAccountState(ql)and callswarnIfWalledOwnerCannotVerify(...); the message builder isresolveWalledOwnerVerificationPathWarninginpackages/plugins/plugin-auth/src/walled-owner-verification-path.ts:309, and it already enumerates named situations — includingowner-absent("Human users already exist but none holds a declared address"), which is adjacent to, but not, the state this card is about.It does not subsume this card, on three counts, each of which is a reason the dead end stays silent today:
- Its predicate is strictly narrower. The probe only runs when all four hold: no email transport, no federated sign-in,
postureEnforcesWall(resolveTenancyPosture()), and a declared platform-owner address. This card's scenario needs none of those — it is about the audience posture (invite_only, the default), which is a different axis from the tenancy posture. A directory-seeded deployment that is unwalled, or declares no owner, or wires an email transport, gets no report at all and still cannot be recovered from inside. - It asks a different question. Every
WalledOwnerAccountStateis about the declared owner's verification standing. This card is about the absence of any login whatsoever — nosys_accountrow exists for anyone.owner-absentis reachable with a perfectly healthy set of accounts. - It is a warning. This card asks for error level, because the consequence is an unrecoverable deployment rather than a degraded path.
So the instruction to whoever takes this: extend this family rather than open a parallel one — same hook site, same message-builder discipline (one named situation per shape, consequence and remedy in the line), and reuse
isHumanUserRowfor the human half. ⛔ Do not add a second independentkernel:readyprober that duplicates the page read; a deployment matching both shapes must not get two overlapping reports.One coupling to #14349, recorded so it is not a surprise. This diagnostic is correct under every limb of that card, which is why it was split out and is queued now. But if the maintainer rules B (count logins), the state this check fires on becomes the state where the bootstrap carve-out opens — so the line's remedy clause would change from "provision an account out of band, or open the audience posture" to something naming that the next visitor to
/sign-up/emailwill be admitted as the bootstrap account. The check, its predicate, and its test all survive; only the sentence moves. Not aBlocked-by:— do not wait on the ruling — but re-read #14349's state before finalising the wording.
Generated by Claude Code
- Its predicate is strictly narrower. The probe only runs when all four hold: no email transport, no federated sign-in,
Claim:
domain:servicesexecution seat, sessionsession_01AUF1NoViznQK32gqpK8wS8, round R21. Branchclaude/issue-14353-no-account-boot-diagnostic.Comments read before dispatch: 1, no ruling. The one comment is the triage addendum
issuecomment-5503696182, which is binding technical guidance (extend the existingkernel:readyreporter family; do not open a parallel prober), not a maintainer ruling. #14349 remains in the decision inbox and is explicitly not aBlocked-by:for this card.Why it is dispatchable now. This card lands in
packages/plugins/plugin-auth/src/auth-plugin.ts, which this lane held serialized behind #14724. PR #14810 merged at 05:33:38Z and the file is released.One correction the taker must carry: the addendum's line numbers were derived at
72adb7fand #14810 has since moved the composition block out of theregisterRoutesgate into its own unconditionalkernel:readyhook. The citedauth-plugin.ts:945-969andwalled-owner-verification-path.ts:309are stale as line numbers. The named symbols are the durable reference — re-derive every position against currentorigin/mainand report the values you measured, never the values quoted here.
Generated by Claude Code
os-dev-report
{ "issue": 14353, "status": "done", "branch": "claude/issue-14353-no-account-boot-diagnostic", "pr": "https://github.com/objectstack-ai/objectstack/pull/14866", "premise_still_valid": true, "summary": "New named boot report `no_sign_in_account_at_boot` at error level: human sys_user rows present and zero sys_account rows now produce one line naming the consequence (nobody can sign in, the carve-out counts humans so it does not open, invite_only refuses self-registration, the deployment keeps looking healthy and cannot be recovered from inside) and the remedy (provision an account out of band, or open the audience posture). Extends the existing kernel:ready walled-owner family rather than opening a parallel one: same hook, and the bounded human-population page is read ONCE and handed to probeWalledOwnerAccountState (which gained an optional param) so no boot pages sys_user twice; WalledOwnerProbeEngine is now an alias of the family's single BootProbeEngine shape. At most one report per boot -- the error suppresses the walled-owner warning when a deployment matches both shapes, and the neighbour is untouched otherwise. No admission semantics, isBootstrapCreation, bootstrap-status or packages/spec touched. THREE QUOTED FACTS MEASURED FALSE: (1) #14349 is NOT still in the decision inbox -- it was ruled option A and closed not_planned on 2026-09-02, and the ruling explicitly licenses this card's wording ('its message text may now say plainly that the door stays shut and the remedy is out-of-band provisioning'), so the addendum's rule-B contingency is moot and the final sentence follows the ruling; (2) walled-owner-verification-path.ts:309 is NOT stale -- resolveWalledOwnerVerificationPathWarning is still at line 309; only auth-plugin.ts moved, 945-969 measured as 1020-1054 (probe call 1048, emitter 1050); (3) breaking the declared-owner precondition alone is an unreachable boot (walled + undeclared refuses startup, #11184), found by a red test, so the independence suite pins the default deployment instead. Level is error per the card; the #13398-class ruling is satisfied not dodged -- BootDiagnosticLogger declares error? and warn? from birth, the warn?-only WalledOwnerVerificationLogger is untouched, and the warn fallback is an explicit branch so a narrower host sink still hears it. Unlike the neighbour, an unanswerable probe is SILENT here: at error level the 'noisy over silent' posture would fire on every engine-less boot.", "tests": "All at final HEAD d1c6ccca1; every exit code captured by redirect-then-read, never across a pipe. (1) pnpm --filter @objectstack/plugin-auth test -- 'Test Files 92 passed (92)', 'Tests 1909 passed (1909)', exit 0. (2) pnpm --filter @objectstack/plugin-auth typecheck -- exit 0, and PROVEN to cover the new files: both boot-sign-in-reachability.ts and .test.ts appear in tsc --listFiles output, so this is not the excluded-tests phantom green. (3) pnpm lint (full repo, eslint . --no-inline-config) -- exit 0 in 67s; NO narrowing to declare. (4) 62 gate families derived by scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (re-derived after the docs edit added 24 to the original 38): 59 GREEN; 3 recorded NOT MEASURED, neither green nor red, each exiting 3 with PREREQUISITE NOT MET in its own verdict text -- check-test-completeness (needs a test-run log), check:dual-build-cjs-loads (needs a full pnpm build), check:type-check-debt (needs the whole workspace closure built; its structural half check:type-check-coverage passed green). ONE gate was genuinely RED and is repaired: check-system-context-census found line rot my hook edit caused, the session-resolution elevation read moving from auth-plugin.ts:1380 to :1405; re-anchored with the gate's own --fix (no census row's meaning changes), re-run green -- '109 elevation read sites in 20 packages across 45 files, all anchored'. check:skill-examples first failed only because @objectstack/client-react was unbuilt (Cannot find module @objectstack/client); after building its closure it passes, 256 prose examples type-check -- not a finding. No test was skipped, disabled or quarantined. ABLATION -- three legs, direction PREDICTED before running. These tests import the subject by RELATIVE path so vitest resolves SOURCE, not dist (confirmed: no vitest alias covers it, not in the unaliased-dist ledger); the mutation proof is therefore literal-text counts on the resolved file, and each leg refused to accept a reading until injected-text count was >=1 AND removed-text count was 0. The script carried trap RESTORE_CMD EXIT INT TERM with absolute paths resolved from git rev-parse --show-toplevel. Leg 1, neuter the account-absence arm, predicted 'negative controls go red', measured 'Tests 5 failed | 54 passed (59)' and all 5 were negative controls. Leg 2, unwire the report from the kernel:ready hook, predicted exactly 6 (1 wiring + 4 independence + 1 precedence), measured 'Tests 6 failed | 53 passed (59)', exactly those 6. Leg 3, stop sharing the page read, predicted exactly 1 (the paged-once test), measured 'Tests 1 failed | 58 passed (59)', exactly that one. RESTORE PROVEN, not assumed: HEAD blob hash equals disk hash for both mutated files (3ed72a2923d8f0fd4590a377e361cf20c0abba2d, 62bf5624ac8b0126974ec4125f98736ffada2e62) and git diff HEAD is empty.", "clause_2": "yes", "clause_2_evidence": "Declared yes; the measurement is mixed and both halves are reported rather than the convenient one. UNCHANGED: package public surface measured 196 exported symbols before and 196 after with an EMPTY diff, and 0 of the 8 new symbols reach dist/index.d.ts (the new module is not re-exported from index.ts); WalledOwnerAccountState is untouched, no union member added; no contract accept/reject behaviour changes. CHANGED: 8 new module-scope exported symbols exist, which meets the dispatch order's literal bar; probeWalledOwnerAccountState gained an optional second parameter and WalledOwnerProbeEngine went from interface to a structurally identical type alias (both in-package only); and -- the substantive one -- the observable output of an EXISTING shipped diagnostic changes, because the walled-owner warning is now suppressed when this error fires. Method: pnpm --filter @objectstack/plugin-auth build before and after, then extract the terminal 'export { ... }' list from dist/index.d.ts and diff. The suppression is the one arguable thing in this PR and a yes routes it to contract review, which is where it belongs.", "mcp_calls": "0 -- every GitHub read and write went through repo-scoped REST (probed first, HTTP 200); no MCP GitHub tool was called", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
os-dev-report
{ "issue": 14353, "status": "done", "branch": "claude/issue-14353-no-account-boot-diagnostic", "pr": "https://github.com/objectstack-ai/objectstack/pull/14866", "premise_still_valid": true, "summary": "Patch round 1. Took check:optional-error-sink's own prescribed fix: `warn` is now REQUIRED on BootDiagnosticLogger and `error` stays optional. Neither forbidden shape was used -- `error` was NOT made required (option C, falsified) and this was NOT satisfied with a required `info`. The round-1 reasoning was one step short rather than wrong: the emitter's explicit fallback branch was already correct, but with both members optional the TYPE still permitted a silent sink, so the gate made the type carry the guarantee the branch assumed. Fallout was three sites, all inside this module's own surface: the emit branch drops its now-dead `?.` on warn; the 'a logger that throws cannot break the boot' double carries a real vi.fn() warn (not a cast) and now also pins that warn stays untouched when a present error throws; the host call site needed nothing because ctx.logger is the spec Logger, whose warn is already required. No test was weakened, skipped or deleted. Clause-2 declaration is UNCHANGED at `yes` -- this round's diff is a two-file type narrowing and does not move the walled-owner suppression that earned it. Test Core (1/6) was left strictly alone: nothing under packages/cli was touched. See gate_derivation_answer for the section-2 answer.", "tests": "All exit codes captured by redirect-then-read, never across a pipe. RED THEN GREEN on the failing gate: at d1c6ccca1 `pnpm check:optional-error-sink` EXIT=1, verdict line 'x 1 sink type(s) declare an optional `error` with no guaranteed fallback channel'; at e10ae5955 the same command EXIT=0, verdict line 'optional-error sink contract: every sink declaring an optional `error` guarantees a `warn` channel (1 baselined, shrink-only)'. The gate's own census moves by exactly one and no further: '30 declare it optional beside a REQUIRED warn, 2 permit silence' -> '31 declare it optional beside a REQUIRED warn, 1 permit silence'; the remaining 1 is the pre-existing baselined entry and no baseline was raised. TESTS at e10ae5955: `pnpm --filter @objectstack/plugin-auth test` EXIT=0, 'Test Files 92 passed (92)', 'Tests 1909 passed (1909)'. TYPECHECK at e10ae5955: `pnpm --filter @objectstack/plugin-auth typecheck` EXIT=0. First attempt was EXIT=2 on examples/basic-usage.ts TS2307 solely because the package's own dist was absent; after `pnpm --filter @objectstack/plugin-auth build` it is clean -- reported because it is a real reading, not hidden. Coverage of the edited files is MEASURED, not assumed: both boot-sign-in-reachability.ts and boot-sign-in-reachability.test.ts appear in `tsc --noEmit -p tsconfig.test.json --listFiles`, that program reports exactly 94 errors matching the frozen test-typecheck-debt.json baseline, and grep for 'error TS' naming either edited file returns NONE. check:test-typecheck green, shrink-only ledger held. DEPENDENCY BUILDS ran under the shared verify lock (scripts/pm/os-verify-lock.sh): dep closure VERDICT command-exit 0 held 237s, package build VERDICT command-exit 0 held 13s, test run VERDICT command-exit 0 held 106s waited 50s. GATE FAMILIES re-derived on the new head: `dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` returns the SAME 62, byte-identical (diff against round 1's list is empty). DECLARED NARROWING on the re-run: the round-1-to-round-2 delta is exactly two files (`git diff --name-only d1c6ccca1 e10ae5955`), so only families whose population can contain TypeScript under packages/ can have changed verdict. Those were re-run on e10ae5955 -- 23 green (comment-mask-adoption, cross-package-test-inputs x2, keyed-text-bounds, plugin-teardown-shape, shard-attestation, system-context-census, tenant-audit-census, undeclared-dep-imports, auth-mount-ledger, dispatcher-error-vocabulary, engine-double-contract, logger-receiver-detach, objectql-double-limit, page-declaration-shape, query-options-erasure, react-page-adapter-contract, role-word, route-envelope, slot-lookup, test-source-alias, where-matcher) plus check:optional-error-sink green, plus the heavy ratchets check:type-check-coverage, check:type-source-resolution and check:published-files green. The doc/changeset/skill/release families were NOT re-run because the files they read are byte-identical to the round-1 tree on which they were green. NOT MEASURED, unchanged from round 1 and by construction (each needs input only a CI run produces): check-test-completeness EXIT=3 'PREREQUISITE NOT MET' (needs a saved turbo run test log), check:type-check-debt EXIT=3 (needs the whole workspace closure built), check:dual-build-cjs-loads EXIT=3 (needs a full pnpm build). Each prints that exit 3 is explicitly NOT a red and NOT a pass; round-1 logs confirm the identical branch, so this is parity, not regression. No ablation was run this round -- the round-1 ablation legs stand and this diff does not move them.", "gate_derivation_answer": "ANSWER: NOT DERIVED -- and reproducibly so, on both heads. It is NOT 'derived and not run' and NOT 'derived and run with a different verdict'. Evidence, all on disk from round 1 plus re-run now: (1) The round-1 derived list (62 commands, scratchpad gate-cmds2.txt) contains no optional-error-sink line, and none of the 62 round-1 gate logs is that gate -- so it was never run locally. (2) Re-running `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` today returns 62 commands, byte-identical to round 1, still with no optional-error-sink. (3) The gate was NOT missing from the tree: it is present at my merge-base 5ff5f9576 as scripts/check-optional-error-sink-contract.mjs, wired in package.json line 109 and invoked at lint.yml line 2390. So this is not a 'landed after my branch point' story. (4) The tool did DISCOVER the family -- it is one of the 202 discovered families -- but placed it in the residue, in the 'Undetermined (source names no path at all -- NOT known irrelevant): 38 famil(ies)' bucket, printed as 'pnpm check:optional-error-sink [lint.yml]' with no 'declared no path population' annotation, unlike several of its neighbours in that bucket which do carry one. --commands prints only the 62 path-and-kind answer and excludes the residue by construction, so a harvest of --commands can never contain it. (5) The CI step is unconditional by design: lint.yml has no workflow paths filter and the step carries an explicit comment -- 'No `paths:` filter, for the standard reason: a filter on packages/** would go dormant on the PR that edits the baseline.' The derivation's own output says the same thing generally: '181 of the 202 are reached by NEITHER path declaration CI obeys ... CI schedules those on EVERY pull request, so no path derivation can narrow them and their verdict above is about relevance, never schedule', and '62 is what THIS CARD owes by path and kind -- NOT a complete account of what CI runs on the PR'. READING: the tool is self-consistent and documents this gap rather than hiding it, so this is a coverage gap in what --commands can promise, not a silent bug in the derivation. But for a diff whose whole subject is a NEW logger-sink interface, the one gate that grades logger-sink interfaces landed in a bucket the runnable harvest excludes -- which is the case worth the seat's attention. As instructed, dispatch-gates.mjs was NOT modified; it was only read and re-run.", "mcp_calls": "0 -- the repo-scoped REST probe returned HTTP 200, so all GitHub reads and writes this round went over REST (curl); zero MCP GitHub calls", "open_questions": [], "out_of_scope_findings": [ "NOT FILED, reported for the seat to file as instructed: scripts/pm/dispatch-gates.mjs --commands does not derive check:optional-error-sink for a diff that adds a new logger-sink interface. The gate is discovered but classified into the 'Undetermined (source names no path at all)' residue bucket, which --commands excludes by construction. Full evidence in gate_derivation_answer. Explicitly told not to fix it (another lane's file), so no edit was made and no issue was filed.", "Already tracked on #14822, untouched: Test Core (1/6) red on d1c6ccca1 in packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts, a package this diff cannot reach. Nothing under packages/cli was edited." ] }
Generated by Claude Code
Frozen pending contract review — the tier is unavailable, recorded here rather than kept in a session's memory
State: work complete, PR #14866 at head
e10ae5955372e4f4b43ee91a7cea73d0b1b6c765, waiting on a Clause-② contract review that cannot be produced right now.- Waiting on: an isolated contract review at the required tier.
- Since: 2026-09-03 ~07:20Z.
- Why: the review subagent was dispatched at that tier and terminated on the provider's own refusal — HTTP 429, "You've reached your Fable limit", model
claude-fable-5-1. This is an entitlement ceiling, not a defect in the change and not a judgement about it. - Who is waiting: the
domain:servicesexecution seat, sessionsession_01AUF1NoViznQK32gqpK8wS8.
Why the seat does not simply review it itself. A Clause-② verdict may only be produced at the required tier — that is a hard gate, and self-certifying around it would make the label mean nothing on every future card. The tier floor binds the card, not just the review activity, so no further dev round is dispatched on #14353 either while this holds.
Unlock — either of these, whichever comes first:
- The tier entitlement resets ⇒ the seat re-dispatches the isolated review, verifies the tier from the transcript, and adopts the verdict verbatim or voids it.
- The maintainer dispatches the review through the director seat / decision-batch route, as was done for perf: authed data-API throughput is pinned to the knex default pool (~10/replica) with no OS_* knob — a 3-replica cluster saturates at ~25 rps while Postgres sits at ~21/200 connections #14176 (PR perf(datasource): size the primary SQL pool from OS_DATABASE_POOL_MAX #14776) on decision batch [WIP] Update action workflow steps for better execution #16 — that route is established and does not require waiting on the entitlement.
Carrier state, unchanged and correct:
needs:contract-reviewis hung on this card and on PR #14866, both written read-modify-write and read back. ⛔ The PR stays a draft and stays unarmed; this gate sits ahead of CI colour, and CI colour is not the thing being waited on.Nothing about the change is in question here. Round 1 and its patch round were verified against the tree (14866#issuecomment-5522052011): the gate that was red is proven red-then-green with captured exit codes, the census moved by exactly one with no baseline raised, tests
1909 passed (1909). What is missing is a verdict, not work.This wait is on the card because a wait that lives only in a session's memory does not exist.
Generated by Claude Code
- added and removedbugSomething isn't workingSomething isn't working
on Sep 3, 2026 - added a commit that references this issue
on Sep 17, 2026 - added a commit that references this issue
on Oct 7, 2026
Filed by the triage seat (session
session_019kDRpB7D2XzVzkaLp57T5D, R+90) as the half of #14349 that needs no ruling. #14349 asks a posture question (should the bootstrap carve-out count humans or logins) and stays in the decision inbox; this card is its option C, which the filer states "moves nothing and composes with either of the others".The measured state (from #14349, on a real
ObjectQLoverbetter-sqlite3)13 seeded
sys_userrows, zerosys_accountrows, default audience posture:api.signUpEmail(...)→SELF_REGISTRATION_CLOSED(isBootstrapCreation()counts 13 humans, so the carve-out does not fire);sys_accountrow at all;invite_onlyposture;⇒ Outside development (where the dev-admin seed is hard-gated off by
NODE_ENV) the deployment cannot be recovered from inside, and its only symptom is a 401 on credentials nobody has.What this card asks for, and what it does not
Asks: at
kernel:ready, when humansys_userrows exist and nosys_accountrow does, report it at error level, naming the consequence ("no one can sign in and self-registration is closed — this deployment cannot be recovered from inside") and the remedy (provision an account out of band, or open the audience posture). Route & surface ownership §3: absence must be loud.⛔ Does not ask for any change to admission semantics, to
isBootstrapCreation's population, or tobootstrap-status— those are #14349's question and are ruled by the maintainer, not here. This diagnostic is correct under every one of that card's three options, which is why it is separable.Grade
pm:queue·priority:p2·domain:services(packages/plugins/plugin-auth) · type Bug. p2: the state is unrecoverable and today entirely silent; the fix is one boot-time check.Size/model suggestion: S,
opus; the pin is a boot fixture with human rows and no accounts asserting the error line, plus the negative control (an account exists ⇒ silent).Refs: #14349 (the posture question) · #14157 (the development-lane half, which fixes this for
objectstack dev) · #14348 (the promotion-target half).