Strip SSO, SCIM and multi-org tenancy
Epic 10745 "Close the identified gaps in Jaah CE". Original request (verbatim): "Close the identified gaps in Jaah CE". Settled inputs: PRD (Task 10746) and Architecture (Task 10747). Requirement and AC ids below are the PRD's; section refs (§) are the Architecture's. Read them with task_read on 10746/10747 if you need more than is quoted here.
## Requirement (verbatim)
R1 — Enterprise code does not ship in the CE tree (PRD, verbatim). The SSO, SCIM, token-billing and multi-org tenancy feature modules are absent from the built public tree, not just their routes. Today `apps/api/app/org_sso.py`, `sso_policy.py`, `scim.py`, `token_billing/`, `tenancy/` and `tenant_gmail/` all ship inert.
Operator's words: "Decide whether enterprise code ships in the mirror." → "Strip enterprise code" / "Exclude from tree".
Assumption A1 (PRD, verbatim): "Multi-org tenancy" means the modules the R2 map assigns to that feature; if a `tenancy/` module is needed by single-org CE core, it stays and the map records it as core. Because CE is single-user but still has one org.
## Scope and approach
Child C2a of Architecture §7 (Architecture §3 tenancy split, §6).
**Map rows** in `packages/editions/features.toml`, each `editions = ["cloud"]`. Re-render the cloud modules after editing.
- `sso`: api `org_sso.py`, `sso_policy.py`, `routes/org_sso.py`, `routes/sso_signin.py`; web `components/security/SsoSection.tsx`, `lib/orgSso.ts`, `pages/orgs/SsoTab.tsx`.
- `scim`: api `scim.py`, `routes/scim.py`; web `components/security/ScimSection.tsx`, `lib/scim.ts`.
- `multi_org_tenancy` and `org_transfer`: the multi-org-only `tenancy/` modules.
**Tenancy split** (PRD A1, confirmed core).
- These stay core: `ids.py`, `seam.py`, `write_seam.py`, `manifest.py`, `keys.py`, `labels.py`, `node_org.py`, `exclusion_log.py`, `inherit_trigger.py`.
- Candidates for exclusion: `transfer*.py`, `rehome.py`, `offboard.py`, `contraction.py`, `copy.py`, `create_surface.py`, `org_lifecycle.py`.
- Confirm each candidate by its core importers. Keep any that a CE code path needs, and record why in the map row comment.
**Guard every core reach** (§2.3 idioms):
- `auth.py:478,485` (`org_sso.enforced`);
- `routes/auth.py:43-44` (top-level `org_sso`, `sso_policy` → lazy import inside the guard) and `routes/auth.py:781`;
- `login_funnel.py:368`;
- `routes/orgs.py:56,1197` (`sso_policy`);
- `main.py:22`;
- `routes/users.py:46` (scim, already cloud-mounted via `users_invites`);
- the importers of the excluded tenancy modules.
CE behaviour when off: SSO is never enforced and password login works as today; there is a single org (org zero + the bootstrap org). The `activity_kinds.py:250-254` strings stay.
**Also:**
- Remove `scim.py` from the manifest's `[check] domain_allowlist`.
- Guard the importers of the web SSO/SCIM files above with `isEnabled('sso')` / `isEnabled('scim')`.
- Delete your `PENDING_SWEEP` entries (tagged C2a).
**Seam** (Architecture §2, pinned by C1; build against it).
- The edition map is `packages/editions/features.toml`. A row has a slug, `label`, `editions`, and optional `paths`, `route_prefixes` and `console_routes`.
- The rendered modules are `apps/api/app/edition_features.py`, `apps/jaahd/edition_features.py` and `apps/web/src/lib/editionFeatures.ts`, exposing `enabled(name)` / `isEnabled(name)`.
- After any map edit, regenerate with `python3 tools/editions/render.py --edition cloud --out .`; a freshness test diffs them.
**Guard idioms** (§2.3, the ONLY forms check_tree honours).
- Python: `from app import edition_features` (jaahd: `import edition_features`), then either `if edition_features.enabled("<slug>"):` with the feature module imported lazily inside, or the early exit `if not edition_features.enabled("<slug>"): return …` before the import.
- Never `from app.edition_features import enabled`.
- Web: `if (!isEnabled('<slug>')) return null` / `isEnabled('<slug>') && <Row/>`.
**Where `edition.IS_CE` stays.** Only for the edition-wide traits listed in §2.3: caps, loopback auth, single-user bootstrap/first run, local storage, schema guards, router wiring in `main.py`, prompt-composition edition copy, signing-key handling, jaahd host-layout sites, and CE node subagent placement. For a site in doubt: if a future edition could plausibly include the behaviour without the rest of the single-user package, it is a feature.
**Tests and the allowlist.**
- Tests that today monkeypatch `IS_CE` alone switch to the `edition_fixture(edition)` helper, which sets `EDITION`, `IS_CE` and `ENABLED` together.
- As you rewrite a site, delete its entry from the `PENDING_SWEEP` section of `tools/tests/test_editions.py` (C1 tagged each entry with its owning child).
- If a site you own is really edition-wide, move it to `EDITION_WIDE` with a one-line reason.
Credentials & reach: (Architecture §10 rows, verbatim)
- Build, check (incl. gitleaks), tooling + app test suites | worktree only; gitleaks at `~/.local/bin/gitleaks` on node h2 | verified (binary present on h2)
- Jaah Deploy watch per child PR | `jaah-forge` read verbs (`pr checks`, `ci log`) | verified (same forge identity and read probe as above)
Out of scope: (PRD §3, verbatim — do NOT touch)
- MUST NOT make `jaah-ai/jaah-ce` public, change repo visibility, or run a `ce-mirror` publish.
- MUST NOT edit LICENSE text or any legal page (legal review is a human item).
- MUST NOT edit `docs/pricing.md` or the marketing site (deferred to the flip; not in this repo).
- MUST NOT add a second secret scanner (trufflehog etc.).
- MUST NOT add an env var, setting, build arg or runtime flag that selects an edition, enables a feature or moves a cap.
- MUST NOT change cloud behaviour or cloud plan entitlements/pricing; the R2 guard rewrite is behaviour-preserving for `cloud`.
- MUST NOT ship `.claude/skills/` (translate, marketing-copy) — named a deferred follow-up below.
- MUST NOT build an on-prem or enterprise edition — only make the map able to describe one.
Architecture boundaries:
- D4, D5: no monorepo file moves.
- D1: keep schema, strip logic. The enterprise ORM models (the `models/orgs.py` `OrgSsoConnection`/`OrgSsoDomain`/`SsoSpentRequest`/`ScimToken`) and the squashed baseline migration stay. No edition-forked schema, no new migration.
Done means:
- AC1.1 → verify: `python3 tools/community_edition/build_tree.py $T && find $T -path '*org_sso*' -o -path '*scim*' -o -path '*sso_policy*'` prints nothing; and every path the feature map (R2) assigns to `sso`, `scim`, `multi_org_tenancy` (and `org_transfer`) is absent from `$T`.
- AC1.2 → verify: `python3 tools/community_edition/check_tree.py $T` exits 0 (import closure intact: no shipped module imports a removed one outside an edition/feature guard).
- CE-mode api tests (`edition_fixture("ce")`):
- password login works and SSO enforcement is skipped;
- org listing and creation paths answer for the single org without importing an excluded module.
- Cloud-mode SSO, SCIM and org transfer tests pass unchanged.
- Every entry added to `EDITION_WIDE` falls in a §2.3 category and is listed with its reason in the PR body. Moving a per-feature site there to dodge the rewrite fails review.
- Cloud behaviour preserved (PRD §3):
- the changed-area api/jaahd/web test suites pass in cloud mode with their assertions unchanged;
- the CE-mode tests for each rewritten site run under `edition_fixture("ce")` and assert the off behaviour;
- a guard with the wrong slug or polarity fails at least one test.
- Quick gate `./dc check quick` green.
- `python3 tools/community_edition/build_tree.py $T && python3 tools/community_edition/check_tree.py $T` exits 0.
- The PR's Jaah Deploy on GitHub succeeds. If not, fix it and re-merge; leave a cancelled or superseded deploy alone.
Outcome
Activity
- Draft
- Ready
- Runner started