Skip to content

fix(plugin-auth): a migrate-and-exit boot (OS_MIGRATE_AND_EXIT=1) never provisions the dev admin, whatever NODE_ENV says - #24415

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24409-migrate-boot-no-dev-seed
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-24409-migrate-boot-no-dev-seed

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24409

Clause-②: no

What changed

isDevAdminSeedArmed() (packages/plugins/plugin-auth/src/walled-owner-verification-path.ts) now returns false when OS_MIGRATE_AND_EXIT is exactly '1'. That is the same spelling objectstack serve compares before it exits. The clause sits after the NODE_ENV === 'development' check and before the OS_SEED_ADMIN off spellings, so it also wins over an explicit OS_SEED_ADMIN=1.

The seed (AuthPlugin.maybeSeedDevAdmin) and the walled owner check (resolveWalledOwnerVerificationPathWarning) both read this one function, so the disarm reaches both. auth-plugin.ts and packages/cli/src/commands/serve.ts are not edited: the seed already gates on isDevAdminSeedArmed() and nothing else, and the fix does not need the flag read any earlier in serve.

Why

serve checks OS_MIGRATE_AND_EXIT only after the kernel bootstrap. The dev-admin seed runs on kernel:ready, inside that bootstrap, so a migrate-and-exit boot with NODE_ENV=development seeded exactly as a dev boot does. Run against an empty database, it left a known-credential login behind. Per the card's Wanted: "A migrate-and-exit boot is never a login-provisioning boot, whatever NODE_ENV says."

Evidence: real os serve migrate-and-exit boots of an empty SQLite file

Each run went through the CLI source entry (packages/cli/bin/run-dev.js, which pins NODE_ENV=development), with OS_MIGRATE_AND_EXIT=1, no OS_SEED_ADMIN, a fresh OS_HOME and a new database file. The fixture and database were deleted after each run. Rows were counted from the file the boot left behind.

shape before (44a5011fe) after (this branch)
single posture exit 0, Migration complete, sys_user 1, sys_account 1 exit 0, Migration complete, sys_user 0, sys_account 0
single posture, OS_SEED_ADMIN=0 (control) exit 0, sys_user 0 not re-run
single posture, OS_SEED_ADMIN=1 not run exit 0, sys_user 0, sys_account 0
OS_MULTI_ORG_ENABLED=true, owner declared, membership policy declared (invite-only) exit 0, Migration complete (34 plugins), 65 tables, sys_user 1 exit 0, Migration complete (34 plugins), 65 tables, sys_user 0
the same, with the seed address set to the declared owner exit 0, 65 tables, sys_user 1 exit 0, 65 tables, sys_user 0
OS_MULTI_ORG_ENABLED=true, owner undeclared exit 1, refused in AuthPlugin.init(): the walled posture requires OS_PLATFORM_OWNER_EMAIL same refusal, unchanged
OS_MULTI_ORG_ENABLED=true, owner declared, no membership policy exit 1, FATAL: the walled posture never declared a membership policy same refusal, unchanged

The walled case (the card's Done-when 3): it completes. A walled migrate-and-exit boot of an empty database still finishes its schema sync with the seed disarmed. The walled owner check is never a refusal (it only warns), and it did not warn in either run. The two walled refusals in the table read only OS_PLATFORM_OWNER_EMAIL and OS_AUTH_MEMBERSHIP_POLICY, never the seed. They refuse the same way before and after this change, so the fix does not widen the walled case.

Tests

New pin: packages/plugins/plugin-auth/src/dev-admin-seed-migrate-and-exit.test.ts, 9 cases. No credential is named in it. The pins count rows and never read the seeded address or password.

  • isDevAdminSeedArmed(): armed under NODE_ENV=development (control). Disarmed by OS_MIGRATE_AND_EXIT=1, also when OS_SEED_ADMIN=1. Any other value ('0', 'true', '') stays armed, because serve keeps serving on those. The existing clauses are unchanged.
  • The seed's kernel:ready step (maybeSeedDevAdmin) over a real ObjectQL and SQLite engine, on an empty store. Control: without the flag it provisions one sys_user and one sys_account (armed and seeding). With the flag, zero of each, and no banner hint.
  • The walled owner check reads the same disarm, both ways. A fresh walled store whose seed address is not the owner warns on the dev boot and stays silent on the migration run. When the seed address is the owner and the store is unknowable, the dev boot stays silent and the migration run warns, because the seed no longer verifies anyone.

Run at da64a0936:

pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/dev-admin-seed-migrate-and-exit.test.ts src/dev-admin-seed-credential-gate.test.ts src/auth-plugin-walled-owner-verification-path.test.ts src/dev-admin-seed-workspace-timezone.test.ts src/auth-plugin-walled-owner-boot-refusal.test.ts
 Test Files  5 passed (5)
      Tests  63 passed (63)
pnpm --filter @objectstack/plugin-auth typecheck   exit 0 (the new file is in tsconfig.test.json's program: --listFilesOnly, 1 hit)
pnpm --filter @objectstack/plugin-auth test        exit 0 · Test Files 164 passed (164) · Tests 3158 passed | 10 skipped (3168)

The existing dev-seed pins (dev-admin-seed-credential-gate, dev-admin-seed-workspace-timezone, auth-plugin-walled-owner-verification-path) stay green. That covers Done-when 2: the objectstack dev seed is unchanged.

Ablation, run once. node scripts/ablation-replace.mjs deleted the new if (env()[MIGRATE_AND_EXIT_ENV] === '1') return false; line (anchor 1 to 0, blob 8dcb3ca08dba to ebb01c7489a8) and ran the new file. Result: 5 failed, 4 passed. The two disarm cases read expected true to be false, the empty-store case read expected 1 to be +0, and both walled cases turned. The four controls stayed green. Restore was proven: the blob matched HEAD (8dcb3ca08dba) and git diff HEAD was empty. No build was needed, because the test imports the subject from src/ by relative path.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at da64a0936: 67 commands, all run locally against that tree. 66 exited 0 on the first run. pnpm check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: some workspace packages had no dist/). After pnpm turbo run build --filter='!@objectstack/docs' it was re-run and exited 0 ("111 published require entry point(s) across 69 package(s) load"). The other dist-reading gates (check:dts-closure, check:lean-entry-closure, check:published-files, check:sourcemap-no-sources-content) were re-run after that build too, and all exited 0.

Reconciliation with --ran (each line carrying its exit code): "67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN", "a DERIVED zero — all 67 recorded an exit code and none of them is 3". Families that only CI runs are read from this PR's check-runs, not re-run here. test:repo was not run locally: no repo-project test changed, and the derivation does not name it.

origin/main moved to 97cb5c5a8 while this ran. The two new commits touch only AGENTS.md, two ADRs and examples/README.md, none of which this diff touches, so the branch was not re-merged before opening.

Acceptance notes

  • auth-plugin.ts keeps two comments on maybeSeedDevAdmin that predate the new clause. One says "Both clauses (and the seeded address below) are resolved by walled-owner-verification-path.ts". The other says "HARD-GATED to development… Opt out within dev via OS_SEED_ADMIN=0". Both are still true, but they no longer list every clause, and the docblock of isDevAdminSeedArmed() now does. They were left alone because the claim allows edits to auth-plugin.ts only if the disarm needs them. Carrier: none.
  • No docs page or skill says that a migrate boot seeds a login. content/docs/deployment/environment-variables.mdx ("When 1, run pending migrations and exit") is still true, and every --seed-admin / OS_SEED_ADMIN sentence in content/docs, skills/ and AGENTS.md describes objectstack dev, which is unchanged. So no docs edit was made.
  • Downstream: cloud picks this up at its next .objectstack-sha move.

Generated by Claude Code

isDevAdminSeedArmed() now returns false when OS_MIGRATE_AND_EXIT is '1',
the spelling objectstack serve compares. The seed and the walled owner
check both read this one function, so they stay in agreement.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nk17Z26X5DB59iQztDBADH
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx (via OS_MIGRATE_AND_EXIT (literal, a string literal in MIGRATE_AND_EXIT_ENV))
What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 97cb5c5a821cb1e07b0e0026db9923bdbc860863 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6ff8e42d1805488baf493f34e4f32a54da95d5d2 — the merge of head da64a0936064031c71fafc248a29901032a2ecbe into base 97cb5c5a821cb1e07b0e0026db9923bdbc860863, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6ff8e42d1805488baf493f34e4f32a54da95d5d2 && git checkout 6ff8e42d1805488baf493f34e4f32a54da95d5d2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 97cb5c5a821cb1e07b0e0026db9923bdbc860863 da64a0936064031c71fafc248a29901032a2ecbe && git checkout -B drift-repro 97cb5c5a821cb1e07b0e0026db9923bdbc860863 && git merge --no-ff da64a0936064031c71fafc248a29901032a2ecbe

node scripts/docs-audit/affected-docs.mjs --json 97cb5c5a821cb1e07b0e0026db9923bdbc860863

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 97cb5c5a821cb1e07b0e0026db9923bdbc860863 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI note · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T01:08Z · head da64a09360

Test Core (2/6) failed, and the failure is not this PR's. Read from the job's log (job 117837189332):

  • What failed: two tests in packages/cli/test/dev-no-watch.pin.test.ts: "a PACKAGE that selects no workspace project fails > and the failure is pnpm refusing the PACKAGE …" and "--no-watch where os dev boots no environment is refused, not ignored > the fixture starts its own dev script when asked — the control". The shard's summary is Test Files 1 failed | 155 passed, Tests 2 failed | 2129 passed.
  • Why: both tests spawn pnpm through corepack. In both, corepack's request for https://registry.npmjs.org/pnpm/latest was answered HTTP 429 (rate limited), so the child os dev exited 1 before it printed what the tests read.
  • Why it is not this PR's: this PR changes plugin-auth only (isDevAdminSeedArmed() and one new test). Neither failing test reads the dev seed or OS_MIGRATE_AND_EXIT. No fix exists yet for a test that reaches the public registry, so none is ported.
  • Next: the failed jobs are re-run once, when this workflow run finishes. A second failure counts as real.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T01:27Z · head da64a09360. This is the seat's own review at TIER_DEFAULT. --tier read no path-derived mandate, and the diff touches no published schema or governed text. It lands through the merge queue once every check on this head is green. ⚠️ No credential here, under the card's disclosure clause.

  • Shape:
    • A draft PR to main. Its first line is Fixes #24409, and that is the only closing keyword in the body.
    • Clause-②: no is at a line start in the body and in the changeset (@objectstack/plugin-auth patch).
    • Card and PR assignee: marchtian.
    • Not governed: check-governed-merges reads 0 of 3 paths, and 258 changed lines ≤ 3000.
  • The fix, read by the seat: one clause in isDevAdminSeedArmed() (walled-owner-verification-path.ts). It answers "not armed" when OS_MIGRATE_AND_EXIT is exactly '1', the spelling serve.ts:5447 exits on, and it sits after the NODE_ENV gate.
    • maybeSeedDevAdmin gates on this function and nothing else.
    • The walled owner check reads the same function, so the seed and the check stay in agreement, as the card's Wanted requires.
    • auth-plugin.ts and serve.ts are not edited.
  • The card's Done when, each met:
    1. Measured on real migrate-and-exit boots (os serve, NODE_ENV=development, an empty SQLite store). On 44a5011fe the boot left one sys_user and one sys_account row. After the fix it leaves none, OS_SEED_ADMIN=1 included. A new pin over a real ObjectQL and SQL driver holds this.
    2. The objectstack dev seed is unchanged. It is armed on an empty dev store and off under OS_SEED_ADMIN=0. All five dev-seed pin files (63 cases) pass.
    3. The walled case completes. OS_MULTI_ORG_ENABLED=true with a declared owner and membership policy: exit 0, "Migration complete", 65 tables, no login. The walled owner check is warn-only and refuses nothing. The only walled refusals measured read no seed state and are identical before and after.
  • Ablation: dropping the clause turned 5 of the 9 new cases red, with the 4 controls green. The restore is blob-equal to HEAD.
  • Semver: patch with a bare Clause-②: no stands. This is the seat's reading, named because security: a session the ADR-0069 auth gate holds can still be served by the storage route family (detail withheld under the disclosure ruling) #24366's review turned on this question.
  • Gates on this head: 34 checks succeeded and 6 were skipped by design (Build Docs, Console Pin Gate, Dogfood Regression Gate, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance: the roster set). There were no failures on run attempt 2, and the required contexts are green. Test Core (2/6) failed on attempt 1 in packages/cli/test/dev-no-watch.pin.test.ts, on an HTTP 429 from the npm registry while corepack fetched pnpm, which is not this diff (CI note 6257506863). Its one re-run passed, and the flake is filed as cli test: dev-no-watch.pin.test.ts reaches the public npm registry (corepack resolves pnpm@latest for its tmpdir fixtures) and fails CI on an HTTP 429 #24419. Locally the dev derived 67 families, ran all 67, and all exited 0 (--ran: 0 NOT-MEASURED, 0 UNRUN). One of them, check:dual-build-cjs-loads, first exited 3 on an unbuilt prerequisite and passed after the build.
  • Accepted deviations:
    • The branch was not merged with later main. The two commits since touch only AGENTS.md, two ADRs and examples/README.md, and the merge queue tests the merged tree.
    • The walled measurement's exact env is an assumption (OS_AUTH_MEMBERSHIP_POLICY=invite-only beside a declared owner). cloud#3045's env is not readable from this container, and without a declared policy the walled boot is refused before and after alike.
  • Out-of-scope, Acceptance notes:
    • Two comments in auth-plugin.ts's maybeSeedDevAdmin list fewer clauses than the gate now has. They are still true, and isDevAdminSeedArmed's docblock lists all three.
    • On the real walled boots the walled_owner_no_verification_path warning did not print on base either, likely because the default composition wires an email transport. This is an observation, not reproduced as a defect.
  • Downstream: cloud picks this up at its next .objectstack-sha move, and cloud#3046 closes against it (the card's Downstream line).

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants