Bootstrap local org, node and admin
## Original requirement (verbatim, epic body)
> Build new deployment type for Jaah: local. Do a web research and copy the model of OpenClaw, N8N and other similar products:
> - Open source in a public repo (for not private), mirrored some of the components from main repo
> - A package of node + CP that user runs on his machine, no multiple machines, single user, no advanced functionality, no networking, no nodes
> - Should be a very easy start - one line install and/or build from repo and/or docker compose
> - Limited number of accounts
>
> Analyze and propose options
Settled upstream (do not reopen): PRD (Task 10080) decisions D1–D16 + assumptions S1–S9; Architecture (Task 10081) decisions D17–D29 — the **1+3 hybrid**: one codebase with a build-time edition (three literal constants rewritten only by the mirror build), an allowlist manifest + tree builder + per-PR mirror-build gate, caps enforced in the CP (route + DB trigger) and the node (independent refusals), public repo `jaah-ai/jaah-local` (fair-code, read-only mirror, images on GHCR), published only on `master` promotion and fail-closed until the licence is approved (D24). Binding numbers: 1 user, ≤3 AI provider accounts in total (any state/provider, S4), ≤10 concurrent agent sessions (over-cap work queues, never fails, S5), no pgvector, no telemetry, loopback only, Docker required.
## Scope of this child
**C3. Local bootstrap (Arch §2.4, plus §2.6 "AC-16 must be reachable").** Owns AC-4 step 3 (sign in as the single user), AC-12 (first-run), AC-14 (exactly one node). Security surface (secrets + first-user auth): run the autonomous review lane.
## Approach (from the settled architecture — realize it, do not redesign)
- **Secrets.** On first boot with `IS_LOCAL`, if the session secret or `CREDENTIAL_ENCRYPTION_KEY` is absent, generate it into a 0600 file on the `jaah-secrets` volume and read it from there. Locate the real read points first (the arch flags `config.py` did not show them by those names). Nothing printed, nothing written to the repo (S7).
- **`app/local_bootstrap.py`** on boot ensures: the single org; the single node row (`os` = `ubuntu` in every case — both the container node and the native node run Ubuntu/Linux at launch; token minted via `app/node_credentials.py`); a pool of exactly `LOCAL_MAX_AGENTS` (10) worktrees for that node; in local, account `max_runners` null and the fleet's `max_runners_per_account` ≥ 10 so per-account caps never bind below 10; `node.env` (`NODE_ID`, `NODE_TOKEN`, `JAAH_CP_BASE_URL`) at `/var/lib/jaah-local/node-identity/node.env` on the shared volume `jaah-node-identity` (CP URL `http://api:8000` in container — loopback http is already accepted, ADR-0107).
- **`python -m app.local_node env`** prints the same three lines for the container node; **`python -m app.local_node env --native`** prints them with `JAAH_CP_BASE_URL=http://127.0.0.1:<published API port>` — the port is one named constant in `app/local_node.py` (default 8000 unless a clash requires otherwise; C7a's compose publishes exactly this port on 127.0.0.1, and C5's installer writes the output to `~/.config/jaah-local/node.env`, 0600).
- **First run (D20), pinned contract (C6 consumes it):**
- `GET /api/local/setup` → `200 {"needs_setup": bool}` (unauthenticated, local only).
- `POST /api/local/setup` `{"email": str, "password": str}` (password ≥ 12 chars) → `201` + session cookie, creates the one admin via the existing seed path (`seed.py:404-416` shape); `409` if any user exists.
- `python -m app.local_admin reset-password` prompts for a new password; no email path.
- **Attachment store**: when S3 is unset, fall back to a local volume (no MinIO in local).
- Register these routes/modules on the shared (mirrored) side. Do NOT edit the manifest `include` — C2's `apps/api` glob picks the files up.
Full documents: the epic's settled `prd` and `architecture` artifacts (Tasks 10080/10081).
Credentials & reach:
- AC-3, AC-17 grep, AC-8 migrations, C1–C6 unit and gate checks — nothing beyond the worktree (plus GitHub Actions for `local-edition-check.yml`) — **verified**: the workflow runs in this repo's existing CI
Out of scope:
- Do NOT implement caps or `/api/local/limits` (C4), the router split (C2), node-side changes or the native installer (C5), the first-run screen UI (C6), or compose/installer files (C7a).
- Do NOT add a new alembic revision (C4 owns this wave's only one).
- Do NOT add an env var or setting that alters caps; do NOT log or print generated secrets or write them anywhere but the 0600 volume file.
- Cloud boot path unchanged when `IS_LOCAL` is False.
PRD §7 (verbatim) — the local edition, and the work to build it, MUST NOT:
- Change the behaviour, UI or data model of the cloud deployments (dev, `dev.jaah.ai`; prod, `app.jaah.ai`). Any edition switch must default to today's behaviour there.
- Ship, in the local edition or in the public repo: Stripe or billing; SES or system mail; Slack or Telegram; headscale or tailnet; cloud node launch (`apps/api/app/cloud_node_launch.py`); node groups (`apps/api/app/node_group_reconcile.py`); Multi-Node Hosts (`apps/api/app/vnodes.py`); NodeLink (`apps/api/app/nodelinks.py`); MicroVM (`docs/policies/microvm-nodes.md`); Jarvis (`apps/site-widget/`); the support knowledge base (`apps/api/app/support_kb/`); project memory (`apps/api/app/project_memory/`); SSO, SCIM or multi-org tenancy; the mobile app (`apps/mobile/`); GitHub Actions self-hosted runners (`infra/host/gha-runners/`); `infra/jaah/` (Terraform) and `infra/host/`.
- Require pgvector, Redis, S3/MinIO, or any cloud account.
- Phone home: no telemetry, no version check, no licence or activation server.
- Accept community PRs into the public repo, or require a CLA.
- Offer data export from local to Jaah Cloud, or attaching the local node to a cloud org.
- Rewrite the control plane for SQLite, or install the control plane natively without Docker.
- Expose any port beyond loopback, or support more than one machine.
- Make the caps (3 accounts, 10 concurrent sessions) configurable by any setting, env var, flag or UI.
Done means:
- **AC-4 (R10), step 3 — "Sign in as the single user."** — verify (local-mode API tests): fresh DB → `GET /api/local/setup` = `{"needs_setup": true}`; `POST` with a ≥12-char password → 201 + working session cookie; then `needs_setup` false; normal password login works; `reset-password` CLI changes it and the old password stops working. A <12-char password is refused with readable copy.
- **AC-12 (R4). Exactly one user.** "there is no sign-up page, no invite UI, and no Users management page… States: before first sign-in (the one user is created by the installer or a first-run screen), and after." — verify: second `POST /api/local/setup` → 409; no other user-create path succeeds in local (routes covered by C2).
- **AC-14 (R6), part:** "exactly one node exists, the local one, created by the install." — verify: boot twice against the same DB → exactly one org, one node row, 10 pool worktrees, idempotent; `node.env` written 0600 with the three keys; `local_node env` output matches.
- Secrets: first boot with none set generates both into a 0600 file; second boot reuses them (existing sessions + encrypted credentials still readable) — test.
- Attachment upload works with S3 unset (local volume) — test.
- With `IS_LOCAL` False (cloud), `/api/local/setup` returns 404 and no bootstrap runs — test (cloud behaviour unchanged, PRD §7).
- `local_node env --native` prints a `127.0.0.1` CP URL with the named port — test.
- Cloud tests unchanged; `./dc check quick` green; autonomous security review lane run and recorded.
Outcome
Local edition bootstrap: generated 0600 secrets, one node + node.env, 10-slot pool, /api/local/setup first user, reset/env CLIs, S3-less attachment volume. Quick gate + area tests green; security lane cleared; PR #6942 merged 09108f5c, Jaah Deploy succeeded. Memory: proposed 1 gotcha.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)