Add local edition seam and mirror gate
## 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.
Definitions (PRD §6): "Fresh machine" = a clean VM of the named OS with only Docker and git installed. "Each launch OS" = Ubuntu 24.04 x86_64, macOS 15 Apple Silicon, Windows 11 + WSL2 + Docker Desktop (macOS/Windows oracles deferred to the operator's `RELEASE-CHECKLIST.md`, D26). "Local install" = the stack installed by AC-4.
## Scope of this child
**C1. Edition seam + mirror tooling + PR gate.** Owns AC-3 (re-scoped by D25) and AC-17's "no knob" check. Every other child imports what this child creates, so it merges first.
## Approach (from the settled architecture — realize it, do not redesign)
- **Edition constants (Arch §2.1)** — three new literal files, each defaulting to `cloud`; no env var, setting or build arg:
- `apps/api/app/edition.py`: `EDITION: Literal["cloud","local"] = "cloud"`, `IS_LOCAL: bool`, `LOCAL_MAX_ACCOUNTS = 3`, `LOCAL_MAX_AGENTS = 10`, `CLOUD_SIGNUP_URL` (single home of that URL).
- `apps/jaahd/edition.py` (stdlib only): `EDITION`, `IS_LOCAL`, `LOCAL_MAX_ACCOUNTS`, `LOCAL_MAX_AGENTS` (deliberate second copy — the independent node layer AC-17 needs).
- `apps/web/src/lib/edition.ts`: `export const EDITION: 'cloud' | 'local' = 'cloud'`, `IS_LOCAL`, `CLOUD_SIGNUP_URL` (static links only).
- Callers read at call time: `from app import edition` then `edition.IS_LOCAL`. Add a test that forbids `from app.edition import IS_LOCAL` (and the jaahd equivalent) anywhere in the tree.
- Keep all three edition files import-free (stdlib `typing` only): they are rewrite targets in the manifest `include`, so any `app.*` import from them would break the import closure. Cap-enforcing logic lives in separate modules (C4's `app/local_caps.py`).
- **Planner packaging decision:** also add the migrations helper `apps/api/migrations/_edition.py` exposing `def vector() -> bool: return not edition.IS_LOCAL` (a function, read at call time so tests can monkeypatch `edition.IS_LOCAL`) here, so C2's vector guard and C4's local-only trigger migration both import it without depending on each other.
- **Manifest + tools (Arch §2.2)** — `tools/local_edition/manifest.toml` is the single home of what ships: `include` (glob allowlist — unlisted = private), `exclude` (cloud-only globs carved out of included trees: e.g. `apps/api/app/routers_cloud.py`, `apps/api/app/routes/billing*.py`, `apps/api/app/nodelinks.py`, `apps/api/app/support_kb/**`, `apps/api/app/project_memory/**`, `apps/web/src/routesCloud.tsx`, `apps/site-widget/**`, `apps/mobile/**`, `infra/jaah/**`, `infra/host/**`, `packaging/**`), `rewrite` (the three edition files + their one-line substitution), `removed_route_prefixes`, `layout` (`infra/local-edition/*` → public repo root).
- `tools/local_edition/build_tree.py <out>`: copy the manifest's tree, apply rewrites, write `SOURCE_COMMIT`, write `app/edition_removed_routes.py` from `removed_route_prefixes`.
- `tools/local_edition/check_tree.py <out>` fails on any of: (1) Python import closure — AST-walk every `.py`; every `app.*`/jaahd-local import resolves inside the tree; (2) excluded-path absence; (3) gitleaks over the tree; (4) `.context-arch.json` `instance_data` denylist; (5) no Jaah-owned domain in runtime code in URL or hostname form (`https?://…jaah.ai…`, `<sub>.jaah.ai` hosts) except a named allowlist (`CLOUD_SIGNUP_URL`'s module, docs) — bare namespace keys such as jaahd's ACP extension namespace `"jaah.ai"` (`apps/jaahd/agent_vocabulary.py:1362`) are NOT domains and must pass; (6) **no knob**: fail on any env-var (`os.environ`/`getenv`/`import.meta.env`), CLI-flag or settings read whose name matches an edition or cap name (`EDITION`, `IS_LOCAL`, `LOCAL_MAX_*`, account/agent cap names) anywhere in the built tree, docs included.
- Unit tests for both tools (a leaked excluded file, a dangling import, a planted fake secret, a denylist term, a Jaah URL and a planted cap env read each fail `check_tree`; the bare `"jaah.ai"` namespace string passes).
- Until C2 splits the routers, the import closure over `apps/api` will not be green. Seed `include` only with the three edition files, `apps/api/migrations/_edition.py`, and `tools/local_edition/**`; C2 widens it to the shared `apps/api` tree (a glob, so C3/C4 files are picked up without touching `include`), C5 to `apps/jaahd`, C6 to `apps/web`, C7a adds `infra/local-edition/**`; the gate must be green on day one.
- **Gate (Arch §2.2)** — `.github/workflows/local-edition-check.yml` on every PR touching `apps/api`, `apps/web`, `apps/jaahd`, `tools/local_edition` or `infra/local-edition`; C1 ships only `build_tree` + `check_tree`. Later children add: C2 plain-`postgres:18` migrate + API boot; C6 `vite build`; C7a image builds. Never red on unrelated PRs. Pin actions by SHA (as `agent-sdk-mirror.yml`).
- Route the new tooling from the repo's router/docs per the context-architecture policy (one home, reachable).
Full documents: the epic's settled `prd` and `architecture` artifacts (Tasks 10080/10081); read them with `task_read`/`artifact_read` when a section referenced here needs more depth.
Credentials & reach:
- AC-3, AC-17 grep, 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 split routers, models, migrations or web routes (C2/C6), add bootstrap (C3), caps logic (C4), node posture (C5), or any `infra/local-edition/` distribution file or publisher workflow (C7a).
- Do NOT add any env var, setting, build arg or CLI flag that selects the edition or changes a cap (AC-17).
- Cloud behaviour must stay byte-identical: `IS_LOCAL` is False in the monorepo.
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-3 (R3, re-scoped by D25). The mirror contains no cloud-only code, no instance data, and no secrets.** Original: "run a secret scanner (e.g. gitleaks) over the full history of the public repo and observe zero findings… grep the public tree and observe zero hits for each excluded surface in §7… and for every term in the `instance_data` denylist in `.context-arch.json`. States covered: the first publish, and every later sync." Re-scoped oracle (D25): (i) no file matching an excluded path in the manifest; (ii) the import-closure check is green; (iii) zero gitleaks findings; (iv) zero `instance_data` denylist hits — verify: `python3 tools/local_edition/build_tree.py /tmp/le && python3 tools/local_edition/check_tree.py /tmp/le` exits 0 on develop+this PR, AND the tool tests prove each of the four checks (plus the Jaah-domain and no-knob checks) FAILS on a planted violation. (Full-history scan of the published repo: C7b on staging; public deferred — D24.)
- **AC-17 (R9), "no knob" part:** "the caps are not settable by an env var, a settings page, a DB row the UI can edit, or a CLI flag. Grep the public repo docs and observe no knob." — verify: the three edition files hold literal constants only; `check_tree` check 6 is permanent in the per-PR gate (so every later child's tree is re-checked) and its test proves a planted cap env read fails; the from-import-forbidding test passes. (Re-run on the full published tree + docs: C7b.)
- The new workflow runs on this PR and is green; cloud suites unchanged (`./dc check quick` green).
Outcome
Added the edition seam (3 literal cloud files + migrations vector()), tools/local_edition manifest/build_tree/check_tree (6 checks, 37 tests), quick stage and per-PR workflow (green). Quick gate green; PR #6939 queued on merge lane. Memory: none applied.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)