Local node posture, caps, installer
## 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. Node placement (D18): native by default on Linux (user-level, no sudo, D23); macOS/Windows run the node as a Linux container in the compose stack.
## Scope of this child
**C5. Node local posture + node caps + product image + native installer (Arch §2.6).** Owns AC-9, AC-10 (Linux: claude, deepseek), AC-16 (node side), AC-17 (node layer), AC-22 (CLI telemetry off). Owns the `capacity` verdict on BOTH sides (node refusal and the CP lease-return path).
## Approach (from the settled architecture — realize it, do not redesign)
- **Local posture (`IS_LOCAL`).** No vnode/nft path (`vnodehelper/nft.py`), no NodeLink mount (`nodelink_mount.py`), no MicroVM, no GHA pool, no self-upgrade from the CP apt route. Single-uid works already (`session_user.py` ≈`:148-153`). The dev sandbox fences in `infra/local/` (stub claude, blackholed vendor, collapsed uid) are NOT reused.
- **Node-layer caps (independent of the CP):**
- Concurrency: `fleet_dispatch.launch_runner` (≈`:1306`) and the console-session launch refuse when the node already has `LOCAL_MAX_AGENTS` live runner/console windows; worktree creation refuses an 11th pool worktree. New hard refusals (`launch_refusal` in `fleet_launch.py` is observation-only — do not repurpose it).
- Accounts: a ledger of DISTINCT account refs the node was asked to use — OAuth config dirs AND `apikey:` refs (`apps/jaahd/account_auth.py`); first `LOCAL_MAX_ACCOUNTS` admitted; refuse to mint, log in, probe or launch with a 4th; the CP's forget command drops the ref and removes the dir (frees the place, S4).
- **Capacity verdict, both sides.** Refused launch replies `{"ok": false, "refused": "capacity", "live": int, "cap": int}`; the CP returns the lease and the task to `ready` WITHOUT spending an attempt (no task ever `system_failed` on the cap).
- **Provider check.** Verify each provider's `max_accounts_per_node` (`agent_providers.py` ≈`:277`; 0 for claude/codex) does not cap below 3 accounts on the one node.
- **No CLI telemetry.** Single home `infra/local-edition/node/cli_env`: each provider CLI's documented telemetry/error-reporting off-switch, looked up per CLI from its docs (never from memory — use `document-specialist`). A CLI with no off-switch is recorded (CLI + host) for C7a's `RELEASE-CHECKLIST.md` — list it in this PR body.
- **Product node image** `infra/local-edition/Dockerfile.node`: Python, jaahd, git, tmux, Node, every provider CLI via `infra/local-edition/node/install_clis.sh` (single home, also used natively); amd64-only (antigravity has no linux arm64); volume `jaah-node-home` for pool + account dirs; no vendor blackhole; loads `cli_env`.
- **Native Linux (D18, D23)** `jaah-local-node install` from `infra/local-edition/node/`: probes first (`systemctl --user` answers; `tmux` + `git` present; runtime download succeeds) — any failure → enable the compose `node` profile instead and say so in one line. Else: `uv` standalone → managed Python 3.14 and a Node LTS tarball under `~/.local/share/jaah-local/` (no apt, no sudo); install jaahd from the mirrored source; `install_clis.sh`; write a `systemd --user` unit; try `loginctl enable-linger "$USER"` (if refused, one line: runs while logged in). **The installer writes** `~/.config/jaah-local/node.env` (0600) from `docker compose exec api python -m app.local_node env --native` (C3), and the unit reads it. `--upgrade <tag>` re-provisions jaahd + CLIs at the API's tag.
- Add the jaahd/node files to the manifest `include` with cloud-only node modules in `exclude`, so `check_tree`'s closure stays green.
Full documents: the epic's settled `prd` and `architecture` artifacts (Tasks 10080/10081).
Credentials & reach:
- AC-9/18/19: coder task, PR on `jaah-ai/jaah-local-e2e-fixture` — `me/GitHub (ruzhukov)` — **verified** (`credential_get` ok). (The fixture repo is created by C7a; for this child's AC-9 oracle use any GitHub test repo reachable with this credential if the fixture does not exist yet.)
- AC-10 claude — `me/jaah/system/anthropic_api_key` — **verified** (`credential_get` ok)
- AC-10 deepseek — `org/deepseek` — **verified** (`credential_get` ok)
- AC-10 codex, antigravity, pi — none on the roster — **deferred** to the operator's release checklist (D27)
- AC-4/5/10/11 on macOS 15 and Windows 11 — none on the roster — **deferred** to the operator's release checklist (D26)
- 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 touch the CP account/agent caps in `apps/api` (C4) beyond the capacity-verdict lease-return path this child owns; no bootstrap (C3); no web (C6); no compose.yml/install.sh/publisher (C7a).
- Do NOT remove or weaken the cloud node's hardening (nftables, bubblewrap, two-uid split, MicroVM) — only skip it when `IS_LOCAL`.
- Do NOT add a new alembic revision.
- Do NOT reuse the `infra/local/` dev-sandbox node; do NOT make the node caps configurable.
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-9 (R13). The node can run natively.** "on fresh Linux, follow the documented native-node path. Observe the node shown as online in the console… run a coder task on a GitHub repo. Observe a fresh clone created under the node's working area and a PR opened." — verify: on an Ubuntu 24.04 host/container without sudo, `jaah-local-node install` succeeds, the `systemd --user` unit runs, the node heartbeats against a CP (C3's bootstrap may not have landed: a dev `./dc` CP with a hand-written `node.env` is acceptable here; the local-mode CP run is C7b's); a coder task clones under the node's working area and opens a PR. Also verify the probe fallback path: with `systemctl --user` unavailable, the installer switches to the compose node profile and prints one line.
- **AC-10 (R13, S6), Linux claude + deepseek:** "add one account of each provider… and run a one-line task on each. Observe a completed session for every provider." — verify: in the product node image, a one-line task completes on claude and on deepseek. (codex, antigravity, pi and macOS/Windows: deferred — D26/D27, `RELEASE-CHECKLIST.md`.)
- **AC-16 (R8), node side:** with the CP cap disabled in a test, the node refuses the 11th concurrent launch with the `capacity` verdict, the CP returns the task to `ready` with no attempt spent, and it starts when a slot frees; no task fails because of the cap.
- **AC-17 (R9), node layer:** the node refuses a 4th distinct account ref (OAuth or API key) for mint/login/probe/launch, and forgetting an account frees the place — tests. Grep `apps/jaahd` for env/flag reads of cap values → none.
- **AC-22 (R19), CLI part:** `cli_env` sets every off-switch each provider CLI documents (each cited to the CLI's docs in a comment); both the image and the native unit load it; CLIs without one are named in the PR body for the release checklist.
- `docker build -f infra/local-edition/Dockerfile.node` succeeds (amd64); `./dc check quick` green; cloud node behaviour unchanged when `IS_LOCAL` is False.
Outcome
Node posture, node-side caps (capacity verdict + CP requeue, account ledger, worktree cap), cli_env, product image and native installer; quick gate green, PR #6945 queued. AC-9 coder task + PR on a native node deferred to draft #10145; cloud-only modules to #10146. Memory: proposed 1 gotcha.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)