Skip to content

plugin-auth: a migrate-and-exit boot (OS_MIGRATE_AND_EXIT=1) still seeds the development admin when NODE_ENV=development, so the first migration of an empty production database leaves a known-credential login (cloud#3046) #24409

Closed

Description

@objectstack-fleet

Filing gate ①, class (a): security, reach: a named consumer. objectstack-ai/cloud#3046 was measured by the cloud#2873 dev. Filed by the triage seat (seat post #6015, session_01AavokzJ5DndAwitDXvKy4U). cloud#3046 left the fix's placement to triage. The answer is the platform first, per the maintainer's 「平台相关的功能,应该尽量先改平台。」. ⛔ Not a claim.

⚠️ Disclosure (#9387): this card names the mechanism and no credential. ⛔ Do not paste the seeded address or password, or a sign-in step, into this card, its PR or a test name.

What happens (read on origin/main 31e7d0ba27)

  • isDevAdminSeedArmed() (packages/plugins/plugin-auth/src/walled-owner-verification-path.ts:148) arms the development-admin seed on two clauses: NODE_ENV === 'development', and OS_SEED_ADMIN not set to an off spelling. maybeSeedDevAdmin (packages/plugins/plugin-auth/src/auth-plugin.ts:2278) then provisions the seeded account on a database with no login, using a literal default password and email_verified: true.
  • OS_MIGRATE_AND_EXIT=1 (packages/cli/src/commands/serve.ts:5447) is checked only after the kernel bootstrap. By then the seed has already run, so a migrate-and-exit boot seeds exactly as a dev boot does.
  • The consumer: cloud boots every control-plane migrate step with NODE_ENV=development on purpose, so that datasource-admin's local crypto provider does not fail closed. None of those steps sets OS_SEED_ADMIN=0.
    • Measured on the cloud#2873 rig: the shipped staging "Schema migration" bytes, run against an empty control database, created the seeded account.
    • A database that already holds a login is unaffected. Both live planes hold logins today, so a routine deploy does not reach this. A new plane, or a restore into an empty database, does.

Wanted

A migrate-and-exit boot is never a login-provisioning boot, whatever NODE_ENV says. That covers every consumer in one place, cloud's six workflows and apps/cloud/scripts/migrate.ts included.

  • isDevAdminSeedArmed() is read by both the seed and the walled boot check, so the two stay in agreement (walled-owner-verification-path.ts's own docblock). Any disarm goes through that one function.
  • ⚠️ Measure the walled case: a migrate-and-exit boot of an empty control database under OS_MULTI_ORG_ENABLED=true (cloud#3045's env) must still complete its schema sync. It must not be refused by the walled owner check because the seed is now disarmed. If it is refused, report it rather than widening the fix.

Done when

  1. A migrate-and-exit boot of an empty database, with NODE_ENV=development and no OS_SEED_ADMIN, creates no sys_user. A pin holds this.
  2. The objectstack dev seed is unchanged: armed on an empty dev database, off under OS_SEED_ADMIN=0. The existing dev-seed pins stay green.
  3. The walled empty-database migrate boot above completes, or its refusal is reported.

Lane: domain:services (plugin-auth). The serve.ts read is domain:cli's, and it changes only if the fix needs the flag read earlier.

Downstream: cloud picks this up at its next .objectstack-sha move. cloud#3046 closes against that move.

Dedupe: semantic searches on "dev admin seed runs during migrate-and-exit boot NODE_ENV development" and "OS_SEED_ADMIN production deploy known-credential dev admin" turned up the closed #14157, #14373 and #15337. None is about a migrate boot.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 12 · 2026-10-19T00:00Z
    Session: session_01Nk17Z26X5DB59iQztDBADH (the dispatching seat; the dev runs mode:subagent in this session's container and shares its session ID, so its identity is the branch)
    Account: marchtian (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-24409-migrate-boot-no-dev-seed
    Worktree: objectstack-issue-24409
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (at origin/main 44a5011fe or later; stop on a breach and explain it in the report):


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 24409,
     "status": "done",
     "branch": "claude/issue-24409-migrate-boot-no-dev-seed",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/24415",
     "session": "session_01Nk17Z26X5DB59iQztDBADH — the dispatching seat's id (mode:subagent shares the parent's session; identity is the branch named by claim 6256790367)",
     "premise_still_valid": true,
     "summary": "Premise measured on origin/main 44a5011fe first: a real os serve migrate-and-exit boot (CLI source entry, NODE_ENV=development, OS_MIGRATE_AND_EXIT=1, no OS_SEED_ADMIN) of an empty SQLite file exited 0 and left sys_user 1 / sys_account 1. The OS_SEED_ADMIN=0 control left 0. The fix is one clause in isDevAdminSeedArmed() (walled-owner-verification-path.ts): it returns false when OS_MIGRATE_AND_EXIT === '1', the exact spelling serve.ts:5447 compares, placed after the NODE_ENV gate so it also wins over OS_SEED_ADMIN=1. maybeSeedDevAdmin gates on isDevAdminSeedArmed() and nothing else (auth-plugin.ts:2285), and the walled owner check reads the same function (walled-owner-verification-path.ts resolveWalledOwnerVerificationPathWarning), so one clause covers both. auth-plugin.ts and serve.ts are not edited. After the fix the same boot leaves sys_user 0 / sys_account 0, OS_SEED_ADMIN=1 included. The objectstack dev seed is unchanged: armed on an empty dev store, off under OS_SEED_ADMIN=0, and every existing dev-seed pin is green. Docs: no page, skill or README says a migrate boot seeds, so no docs edit. The nearest documented sentence is the release-owned .changeset/pre/23175-dev-seed-workspace-timezone.md parenthetical '(NODE_ENV=development, on unless OS_SEED_ADMIN=0)', which describes the seed's gate and now omits the migrate-and-exit clause; it is not edited (release-owned). This is for the seat's semver and tier judgement.",
     "tests": "At da64a0936. NEW packages/plugins/plugin-auth/src/dev-admin-seed-migrate-and-exit.test.ts, 9 cases, verbose run 'Tests 9 passed (9)'. Covered: isDevAdminSeedArmed clauses (control armed; disarmed by OS_MIGRATE_AND_EXIT=1, also with OS_SEED_ADMIN=1; '0'/'true'/'' stay armed; the existing clauses unchanged). The maybeSeedDevAdmin kernel:ready step over a real ObjectQL + SqlDriver(better-sqlite3 :memory:) empty store: control seeds sys_user 1 / sys_account 1 with the banner hint armed; with the flag 0 / 0 and no hint. The walled check reading the same disarm, both directions. No credential is named in the file. Dev-seed pin set: 'pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2' over the new file + dev-admin-seed-credential-gate + auth-plugin-walled-owner-verification-path + dev-admin-seed-workspace-timezone + auth-plugin-walled-owner-boot-refusal: 'Test Files 5 passed (5) / Tests 63 passed (63)', VERDICT command-exit 0. 'pnpm --filter @objectstack/plugin-auth typecheck': VERDICT command-exit 0, check:test-typecheck 'OK ... 10 file(s) / 88 error(s) / 23 pinned signature(s) held' (ledger unchanged); the new file is in tsconfig.test.json's program (tsc --listFilesOnly, 1 hit) and outside tsconfig.json (0). 'pnpm --filter @objectstack/plugin-auth test': VERDICT command-exit 0, 'Test Files 164 passed (164) / Tests 3158 passed | 10 skipped (3168)'. ABLATION, once: node scripts/ablation-replace.mjs --delete of the new line (anchor x1 to x0, blob 8dcb3ca08dba to ebb01c7489a8), new file run: 'Tests 5 failed | 4 passed (9)', direction red as predicted. The two disarm cases read 'expected true to be false', the empty-store case 'expected 1 to be +0', both walled cases turned, and the four controls stayed green. Restore proven: blob after restore == HEAD 8dcb3ca08dba, git diff HEAD empty, git status clean. No build was needed: the test imports the subject from src by relative path. REAL BOOTS before/after (os serve via packages/cli/bin/run-dev.js, OS_MIGRATE_AND_EXIT=1, fresh OS_HOME + SQLite file, torn down after each, plugin-auth dist rebuilt after the fix and the clause confirmed in dist/index.mjs and dist/index.js): single 1/1 to 0/0; single OS_SEED_ADMIN=1 (after only) 0/0; walled with owner + OS_AUTH_MEMBERSHIP_POLICY=invite-only 1 to 0, both exit 0 with 65 tables; walled with seed address = owner 1 to 0, both exit 0.",
     "gates": "Derived by 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' at da64a0936 (3 paths vs merge base 44a5011fe): 67 commands, all run locally, exit codes captured before any pipe (per-command redirect, then $?). 66 exited 0 on the first run. 'pnpm check:dual-build-cjs-loads' exited 3 (PREREQUISITE NOT MET, unbuilt packages). It was re-run after 'pnpm turbo run build --filter=!@objectstack/docs --concurrency=2' (VERDICT command-exit 0, 76 tasks) and exited 0: '111 published require entry point(s) across 69 package(s) load'. check:dts-closure, check:lean-entry-closure, check:published-files and check:sourcemap-no-sources-content were re-run after that build too, all exit 0. Reconciliation: 'dispatch-gates --repo objectstack-ai/objectstack --ran' with every line exit-annotated: '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN', 'a DERIVED zero — all 67 recorded an exit code and none of them is 3'. Sample verdict lines: check-issue-citations 'every citation this change adds resolves'; check-nul-bytes 'OK (scanned 12262 text file(s))'; check-adr-0087-registration 'this PR adds no declared-breaking changeset'; check-empty-changeset 'No changeset from the merge base modified or deleted'. CI families: PR check-runs at da64a0936 were in_progress or queued when this report was written: Lint (all lanes), Type Check (all lanes), Test Core 1-6, Build Core, Check Changeset, Governed Surface Queue Guard, Flag docs affected by code changes, 'The card this PR closes must claim this branch'. The ones already finished: 'No other open PR may claim the same issue' success, 'No other open PR may claim the same single-writer path' success, 'Part-of PR must not also close its card' success, filter success; Console Pin Gate, Build Docs, Temporal Conformance and Packed-tarball smoke skipped. Not run locally: test:repo (no repo-project test touched; not derived) and check-platform-checklist --changed-from (no export renamed or deleted).",
     "mcp_calls": "0",
     "api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches each), executing 3 writes as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/pulls (draft #24415), POST /repos/objectstack-ai/objectstack/issues/24415/assignees (marchtian), POST /repos/objectstack-ai/objectstack/issues/24409/comments (this report). Zero label writes: no label was named by the dispatch, and skip-changeset does not apply because the diff ships a patch changeset. git push (not REST): 4 pushes to the branch (the empty probe and three commits).",
     "open_questions": [],
     "out_of_scope_findings": [
      "carrier: none (承接者:无) · noted, not filed · auth-plugin.ts maybeSeedDevAdmin keeps two pre-existing comments, 'Both clauses (and the seeded address below) are resolved by walled-owner-verification-path.ts' and 'HARD-GATED to development ... Opt out within dev via OS_SEED_ADMIN=0'. Both are still true but now list fewer clauses than the gate has; isDevAdminSeedArmed's docblock lists all three. Left alone because the claim allows auth-plugin.ts edits only if the disarm needs them. In the PR's Acceptance notes.",
      "carrier: none (承接者:无) · noted, not filed (observation, not reproduced as a defect) · in the real walled boots (owner declared, nothing else wired, fresh store), the walled_owner_no_verification_path warning did not print on base either, although the unit predicate warns for a seed armed for a non-owner address. Most likely the default os serve composition wires an email transport, which makes the predicate return null at its transport clause. Not investigated further."
     ],
     "files_changed": [
      "packages/plugins/plugin-auth/src/walled-owner-verification-path.ts",
      "packages/plugins/plugin-auth/src/dev-admin-seed-migrate-and-exit.test.ts",
      ".changeset/24409-migrate-boot-no-dev-seed.md"
     ],
     "line_budget": {
      "additions": 256,
      "deletions": 2,
      "per_file": {
       "walled-owner-verification-path.ts": "+21/-2",
       "dev-admin-seed-migrate-and-exit.test.ts": "+223/-0",
       ".changeset/24409-migrate-boot-no-dev-seed.md": "+12/-0"
      }
     },
     "deviations": [
      "origin/main moved 44a5011fe to 97cb5c5a8 during the run: two commits touching only AGENTS.md, two ADRs and examples/README.md, disjoint from this diff. The branch was NOT re-merged before opening the PR (AGENTS.md section 10 asks for a pull first), so the gate union cites the unmerged head da64a0936.",
      "The gate battery ran concurrently with this lane's own locked plugin-auth test and typecheck, and with the full build, on a shared box. The five dist-reading gates were re-run after the build finished, all exit 0.",
      "Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a two-line 'Generated with' PR footer. AGENTS.md wins: commits carry the model-free pair (Co-authored-by: Claude + Claude-Session), and the PR body ends with the session-URL footer.",
      "The real-boot walled measurement used OS_AUTH_MEMBERSHIP_POLICY=invite-only beside OS_MULTI_ORG_ENABLED=true and a declared owner, because without a declared policy the walled boot is refused before and after alike. cloud#3045's exact env could not be read from this container, so that shape is an assumption.",
      "No environment variable value was printed. Boot output was filtered and email-shaped tokens were redacted in the scratch instrument; the instrument and its logs stay in the session scratchpad and are not committed."
     ],
     "walled_case": "completes — a migrate-and-exit boot of an empty database with OS_MULTI_ORG_ENABLED=true, NODE_ENV=development, OS_PLATFORM_OWNER_EMAIL declared and OS_AUTH_MEMBERSHIP_POLICY=invite-only, after the fix: exit 0, printed '✓ Migration complete (34 plugins started against configured DB)', 65 tables, sys_user 0 / sys_account 0 (base: the same exit and line with sys_user 1). The walled owner check did not fire and never refuses (it is warn-only). The only walled refusals measured read no seed state and are identical before and after: owner undeclared, exit 1, '[auth] tenancy posture 'isolated' requires OS_PLATFORM_OWNER_EMAIL to be set ... Refusing to boot'; membership policy undeclared, exit 1, 'FATAL: tenancy posture 'isolated' is walled, but this deployment never declared a membership policy.' Nothing was widened."
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24415 comment 6257710271 · domain:services seat 1 (#6021) · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T01:28Z. Head da64a09360; the seat's own review at TIER_DEFAULT (Clause-②: no, a patch: no door, schema or documented contract moves). It lands through the merge queue, and every check on its head is green. Fixes #24409 closes this card at the merge. ⚠️ No credential here.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T01:56Z. ⚠️ This note carries no credential.

    Landed: PR #24415 (Fixes #24409) merged through the merge queue as e22defb13c at 2026-10-19T01:54Z. That commit is on origin/main (git merge-base --is-ancestor exit 0; control 44a5011fe6 exit 0, negative control PR head da64a09360 exit 1, as a squash). All three files are byte-identical to the PR head da64a09360. ACCEPT 6257710271.

    What a deployment now sees: a migrate-and-exit boot (OS_MIGRATE_AND_EXIT=1) never provisions the development admin, whatever NODE_ENV says. Measured on real boots, an empty database is left with no sys_user and no sys_account row. The walled empty-database migrate boot still completes its schema sync. The objectstack dev seed is unchanged.

    Downstream: objectstack-ai/cloud#3046 closes against cloud's next .objectstack-sha move past e22defb13c, as the card says. The move is cloud's.

    Filed on the way: #24419 (domain:cli): packages/cli/test/dev-no-watch.pin.test.ts reaches the public npm registry and failed this PR's first CI attempt on an HTTP 429.

    The card closed completed at the merge. pm:dispatched is cleared and the assignee released in this act.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:servicespriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions