Cut cloud-only surfaces from API
## 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
**C2. API cut (Arch §2.3).** Owns AC-8, AC-14 (routes), AC-21 (routes), AC-12 (routes), AC-22 (no Jaah domain in API code).
## Approach (from the settled architecture — realize it, do not redesign)
- **Routers.** Move every cloud-only `include_router` out of `apps/api/app/main.py` (≈178 inline registrations around `:456-619`) into a new `apps/api/app/routers_cloud.py` with `include(app)`; guard with `if not edition.IS_LOCAL: from app import routers_cloud; routers_cloud.include(app)`. Same for cloud startup workers (e.g. the Slack socket consumer near `main.py:231`) and cloud imports at startup (`main.py:74-196`).
- Cloud-only surfaces: billing/hostings, agent_email/org_email/email delivery log, slack_connect, headscale_allowlist, nodelinks, vnode_hosts, agent_microvms, cloud_nodes, node_groups, node_orders, support chat and knowledge base, project_memory, users and invites, signup, SSO/SCIM, login_oauth, Telegram, SIEM export, data residency.
- **The whole Help assistant (support chat + its tickets) is removed in local (D28)**, not only its KB.
- **Models.** Models importing `pgvector` (`models/support.py`, `project_memory/models.py`) and other cloud-only models leave the shared model registry and move under the cloud import path. Shared models with only cloud *columns* (e.g. `orgs.stripe_*`) stay (D25).
- **Migrations — one chain, a vector guard.** Use C1's `migrations/_edition.py` (`vector()`). Wrap in `if vector():` the baseline's extension creation (`202610130700_baseline.py:1771`) and the two vector tables + indexes (`:4018`, `:5490`, `:4025`, `:5511`), and the same in the 12 later revisions that touch `support_kb_*` or `project_memories`. Edit existing revisions only — add no new revision id.
- **Org routes.** Local still needs reads of its one org. List in the PR body exactly which org prefixes are removed in local (tenant switching, org create/transfer, plans/billing, SSO, SIEM, email, shared stores, retention/residency — e.g. `org_transfer`, `org_billing`, `org_plan`, `org_sso`, `org_siem`, `org_email`, `org_shared_store`, `billing_orgs`) and which stay (the single org's own read/settings needed by shared screens).
- **Removed routes.** When `IS_LOCAL`, one catch-all for `edition_removed_routes` prefixes returns `404 {"detail": "Not in Jaah Local. Available on Jaah Cloud.", "upgrade_url": CLOUD_SIGNUP_URL}`. Populate `manifest.toml`'s `removed_route_prefixes`.
- **Parity test.** Pin that the cloud route table (method+path set) and startup worker set are identical before and after the split.
- **Manifest + gate.** Widen C1's `include` to the shared `apps/api` tree (with `routers_cloud.py` and the cloud modules in `exclude`) so the import closure is green; add to `local-edition-check.yml`: `alembic upgrade head` against plain `postgres:18`, then API boot with `IS_LOCAL` rewritten + `GET /api/health` 200.
- Any outbound call to a Jaah domain at runtime must sit behind `routers_cloud` or be removed (`check_tree` check 5).
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 build local bootstrap/first-run/secrets (C3), caps or `/api/local/limits` (C4), node changes (C5), web changes (C6), or distribution files (C7a).
- Do NOT add a new migration revision (C4 owns this wave's one new revision); do NOT fork a separate local migration chain.
- Cloud route table, workers and schema must be unchanged (parity test).
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-8 (R12). Postgres has no pgvector.** "on the local install, run `docker compose exec <db service> psql -U <user> -c "select extname from pg_extension"` and observe no `vector` row… the DB image is a plain Postgres image… all migrations run green." — verify here: the gate's plain-`postgres:18` step runs `alembic upgrade head` green with the local rewrite and `select extname from pg_extension` shows no `vector`; and on the cloud (vector) path the chain still produces the vector tables (existing migration tests green). End-to-end on the local install: C7b.
- **AC-12 (R4), routes:** "`POST` to the user-create and invite endpoints returns 403 or 404 with a message that names Jaah Cloud." — verify: API test with `edition.IS_LOCAL` monkeypatched True.
- **AC-14 (R6), routes:** "the node-create, node-group and NodeLink API routes return 404." — verify: API test (local) gets 404 + the Jaah Cloud payload.
- **AC-21 (R18), routes:** "the matching API routes return 404" for billing/plans/Stripe, orgs/tenant switching, users/invites, SSO/SCIM, nodes/node groups/Multi-Node Hosts/NodeLink/MicroVM, Jarvis (site widget, Support → Website chats), Help assistant (D28) + KB, project memory, Slack/Telegram, headscale/tailnet, Audit SIEM export, data residency — verify: one parametrised local-mode test over every removed prefix.
- **AC-22 (R19), API part:** no Jaah-owned domain is called from API runtime code — verify: `check_tree` check 5 green on the built tree.
- Cloud parity test green; `./dc check quick` green; `local-edition-check.yml` green including the new migrate + boot step.
Outcome
The cloud-only API is cut behind the edition: routers_cloud.py, a Cloud 404 on 54 removed prefixes, a vector() migration guard, plain-pg18 migrate + boot in CI, and parity tests. Full gate passed, PR #6943 merged. Deferred: full apps/api closure (446 imports). Memory: none applied; proposed 1 recipe
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)