Skip to content

docs(self-hosting): a directory-seeded, credential-less deployment has no documented recovery path — the published instruction points at the door invite_only shuts #14495

Description

@os-project-manager

Filed by the director seat (session session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02) as the follow-up the ruling on #14349 names. ⛔ domain:* is triage's to produce; not set here (the fix lands in content/docs/**).

Provenance

#14349 was ruled A by the maintainer on 2026-09-02 (verbatim on that card): the bootstrap carve-out keeps counting human users, so a production deployment that seeds a people directory (sys_user rows) and no credentials (sys_account rows) stays unrecoverable from inside — nobody can sign in, the invite_only default refuses self-registration, and there is no administrator to invite anyone. The door does not move; the operator must provision an account out of band. #14353 (queued) adds the boot-time diagnostic. This card is the documentation half.

The defect

Triage verified on origin/main @ 72adb7f (#14349, comment 5503693743) that content/docs/deployment/self-hosting.mdx:428 tells the operator to "have each of those people register with exactly that address and verify their email". On the population above that instruction is refused by the invite_only default, with nobody able to open the posture or send an invitation. So the published recovery path is the one that is shut. Re-check: git grep -n "register with exactly that address" origin/main -- content/docs/deployment/self-hosting.mdx.

Acceptance

  1. Measure the working path first, do not assume one. Establish, on a real kernel with N human sys_user rows and zero sys_account rows under the invite_only default outside development, which operator action actually produces a first login (an env-declared owner path, a CLI command, a posture change, a direct data write — whichever exists). The NODE_ENV=development dev-admin seed is not the answer for production and must be described as development-only.
  2. The self-hosting page states plainly that seeding a directory without credentials does not open a bootstrap window, names the measured out-of-band step, and points to the boot diagnostic A deployment with human rows and zero sys_account rows boots silently into an unrecoverable state — say so loudly at kernel:ready #14353 emits once it lands.
  3. If the measurement finds no working path at all, stop and report — that is a finding for the decision inbox (a production deployment with no recovery path), not something a docs PR papers over. premise_still_valid: false with pr: null is a legitimate delivery here.

Size: S. Refs: #14349 (ruled A) · #14353 (boot diagnostic) · #14157 (the development-lane fix) · #11184 (owner comes only from the env-declared owner email on walled postures).

Dedup: search_issues in this repo for the self-hosting registration path → no open card (closed neighbours #11184, #9441 are distinct).

Activity

  1. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    分诊路由(本评论来自分诊座位)· R+150

    domain:devx · 保留 documentation + pm:queue · priority:p2。

    落点实测:交付物落 content/docs/deployment/self-hosting.mdx;车道表把 content-docs 归 devx。⭐ 卡面给的复检串本席按原样跑过一遍,阳性:

    git grep -n "register with exactly that address" origin/main -- content/docs/deployment/self-hosting.mdx
    → 439:Then have each of those people register with exactly that address and **verify
    

    ⚠️ 行号已从卡面写的 428 漂到 439(fetch 后的 origin/main 实测)—— 缺陷本身仍然成立,漂的只是行号。

    p2 判据:公开文档把一条被 invite_only 默认关掉的门当作恢复路径写给自建部署的运维;但它是文档半边,运行期诊断另有 #14353,且 #14349 已裁 A(门不动)⇒ 不是 p1。

    ⭐ 给接手方的提醒(卡面验收第 3 条,别绕过):若实测找不到任何可用的开局路径,正确交付是 premise_still_valid: false + pr: null 并上报决策箱,⛔ 不是用文档把它糊过去。


    Generated by Claude Code

  2. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk, dispatching to an os-dev subagent on branch claude/issue-14495-self-hosting-bootstrap-recovery. P2 docs, M: acceptance as the card writes it — (1) MEASURE the working recovery path first on a real kernel (N human sys_user rows, zero sys_account rows, invite_only default, outside development) and name the operator action that produces a first login; (2) rewrite content/docs/deployment/self-hosting.mdx (the "register with exactly that address" instruction is at :439 on current main) to state that seeding a directory without credentials opens no bootstrap window, name the measured out-of-band step, and point at #14353's boot diagnostic (that card is CLOSED — the diagnostic landed); the NODE_ENV=development seed described as development-only; (3) ⛔ stop rule: if no working path exists, premise_still_valid: false + pr: null and the finding goes to the decision inbox — never a doc that papers over it. Routing guard: the page carries no generated banner (hand-written). Hot-file: the only open PR touching the page is the bot release PR #15334 (+4/−4 image-tag version rows, regenerated by changesets on every main move — the dev does not edit those lines). Assignee field was empty at the read immediately before this write (the assignee/label write landed 21:26Z; this note is the claim record).


    Generated by Claude Code

  3. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14495,
      "status": "done",
      "branch": "claude/issue-14495-self-hosting-bootstrap-recovery",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15586",
      "premise_still_valid": true,
      "summary": "Measured first, on a real kernel (ObjectQL over driver-sql + better-sqlite3, plugin-auth's own authIdentityObjects, driven through AuthManager.handleRequest): 3 human sys_user rows, 0 sys_account rows, NODE_ENV=test (dev-admin seed disarmed), default invite_only. The published instruction is refused 403 SELF_REGISTRATION_CLOSED for a seeded person's own address AND for any other; sign-in is 401; the #14353 boot diagnostic fires with no_sign_in_account_at_boot. A working out-of-band path EXISTS and is measured: write one pending sys_invitation row (email the directory does not already hold, status pending, future expires_at, inviter_id of any existing sys_user), then that person registers at the ordinary endpoint - sign-up 200, sign-in 200, no mail transport, invite_only never widened; on the single posture bootstrapPlatformAdmin then promotes that account holder (adminPromoted true, grant on the invited account, never on a credential-less directory row). Two remedies the boot diagnostic names were measured as non-remedies and are documented as such: widening the posture answers 200 while persisting nothing for an address the directory already holds and forces EMAIL_NOT_VERIFIED on every other, and a hand-written credential row authenticates nothing while silencing the diagnostic. The page now says what 'fresh' means on both first-boot paths, carries a new subsection 'A seeded directory opens no bootstrap window' with the measured table, the diagnostic's grep token and the recovery, and describes the dev-admin seed as NODE_ENV=development-gated. Docs-only: no image-tag rows, nothing under content/docs/releases/; skip-changeset applied and read back.",
      "tests": "Measurement: bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/tmp-14495-recovery-measurement.test.ts' -> 'Tests 6 passed (6)' and 'os-verify-lock: VERDICT command-exit 0 - held the lock 16s' (harness deleted before commit; transcript quoted in the PR body). Gate family DERIVED, not guessed: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after merging origin/main so the answer is not from a stale tree - change set '1 path(s) vs merge base 3d3f60e01', exactly content/docs/deployment/self-hosting.mdx. All 38 derived commands run with each exit code captured BEFORE any pipe: 38/38 exit 0. Verdict lines the gates printed: 'check-doc-anchors: 307 internal #fragment link(s) across 410 source file(s) all resolve to a real heading'; 'check-docs-single-h1: 405 page(s) under content/docs/ carry no body-level # heading'; 'content/docs (docsSchema, floor 1): 405 page(s) parse with yaml@2.9.0'; 'doc authoring guard: 46 published skill files clean'; 'release-owned pages are in scope and read-only: 9 page(s) under content/docs/releases/ review-only'; 'check-docs-image-tag: OK (3/3 enumerated surface(s) read, 9 concrete pin(s) compared against packages/cli/package.json 17.3.0)'; 'check-nul-bytes: OK (scanned 7555 text file(s); no raw ASCII control bytes)'; 'check:docs -> 230 generated files in sync with packages/spec'; 'check:skill-examples -> 257 prose examples type-check across 3 surface(s)'. Two gates first answered PREREQUISITE NOT MET rather than a verdict (check:docs 'packages/spec/json-schema is older than packages/spec/src'; check:skill-examples stale spec .d.ts, then 'packages/client-react/dist holds no .d.ts declarations') - recorded as NOT MEASURED, never a pass: pnpm --filter @objectstack/spec build and pnpm --filter @objectstack/client-react... build were run (both 'os-verify-lock: VERDICT command-exit 0') and both gates re-run to the green verdict lines above. Repo-wide pnpm lint is CI's: DECLARED NARROWING with its three pieces - (1) eslint's own config resolution answers, for the only changed file, 'File ignored because no matching configuration was supplied'; (2) --format json returns 1 result, 0 errors; (3) the diff adds no eslint config and no JS/TS source, so no untouched file's verdict can move. Every figure measured on the final tree, git rev-parse --short HEAD = 472e204e1 (nothing committed after the gate run).",
      "mcp_calls": "0 - every GitHub read and write went through REST with $GH_TOKEN; dedup used the REST list endpoint (714 open items paged) plus a local grep, never search_issues",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #15587: plugin-auth - a sign-up for an address that already has a sys_user row answers 200 with a user id no row holds and persists nothing (posture email_domain); the same call under invite_only is refused 422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
        "filed as #15588: plugin-auth - both remedies in the no_sign_in_account_at_boot report are unexecutable as written (opening the posture forces EMAIL_NOT_VERIFIED and cannot admit an existing person; a hand-written credential row authenticates nothing) and the second one silences the report, because the probe asks only whether ANY sys_account row exists"
      ]
    }

    Generated by Claude Code

  4. claude commented on Sep 4, 2026

    @claude
    Contributor

    LANDED — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

    PR #15586 merged 2026-09-04T23:11:19Z (squash f1d787294; enqueued 22:48Z, one merge-group run, no eviction). Probed on origin/main after a re-fetch (HEAD f1d787294): content/docs/deployment/self-hosting.mdx carries the new subsection ### A seeded directory opens no bootstrap window with the measured 403/403/401 table (SELF_REGISTRATION_CLOSED), the boot diagnostic's grep token no_sign_in_account_at_boot and its blind spot, the one-row sys_invitation recovery with its measured result (adminPromoted: true on single), the two measured non-remedies, and the NODE_ENV=development gate on the dev seed; both first-boot paragraphs link the subsection.

    Stripping pm:dispatched + assignee; Fixes #14495 closed this card. The two plugin-auth findings the measurement produced (#15587, #15588) stay bare for triage.


    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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions