Flip operator plane, redesign fixtures
Epic: Make gmind a tenant — narrow the operator plane (follow-up to Task 5190). This child is C1 in the architecture (Task 5200 §2.1, §2.2). It MERGES LAST of the code children. It depends on C2 (OAuth2 rule), C3 (gmind boot step), C4 (knob credentials) and C5 (retag action + ACL), all already merged, because the flip is not safe without them.
## Original requirement (verbatim, task body)
"Goal: `apps/api/app/tenancy/ids.py::OPERATOR_PLANE_ORG_IDS` = org zero only, so gmind (`DEFAULT_ORG_ID`, slug `gmind`) is an ordinary tenant and `operator` is the one operator org."
"1. Product change is small: `OPERATOR_PLANE_ORG_IDS` → `{ORG_ZERO_ID}`; `app/seed.py` bootstrap admin + superadmin fixture logins seated in org zero (they keep an org-zero seat, like revision 0369 gave every default superadmin); comments naming \"the default org\" as operator."
"2. Test fallout is large: flipping it fails 1,718 API tests (~200 files — credentials, access_explain, virtual_nodes, notify_targets, crons, billing authz, commands…). Root cause: fixtures use one org as both \"the operator's\" and \"the tenant under test\" (`apps/api/tests/conftest.py` `_SEAT_SHAPES[\"operator\"]`, `FIXTURE_ORG_ID`, the seeded admin). Needs a fixture redesign, not per-test patches."
"gmind token usage becomes billable under Jaah mode (`token_billing/metering.py::_billable`)."
## Settled requirements (PRD, verbatim)
**R1. Flag narrowed.** `OPERATOR_PLANE_ORG_IDS == frozenset({ORG_ZERO_ID})`. `is_tenant_org(DEFAULT_ORG_ID)` is True. `is_operator_plane(DEFAULT_ORG_ID, True)` is False. `is_operator_plane(ORG_ZERO_ID, True)` is True.
**R2. Seed seats.** On a fresh database, the bootstrap admin and the e2e superadmin fixture login are superadmins holding a seat in org zero. If they also hold a gmind seat, that seat is optional and is not what makes them operators. On an existing database, a re-run gives a pre-existing bootstrap admin an org-zero seat and never removes a gmind seat.
**R3. No stale prose.** Every comment and doc that names "the default org", "Default" or gmind as operator-plane is corrected in the same change. This includes `tenancy/ids.py:62-65`, `seed.py:70`, `vnodes.py:395`, the `tenant_defaults.py`/`provider_auth_policy.py`/`cloud_node_launch.py:260` headers, and `docs/policies/shared-storage.md` if it says so.
**R4. Test fixtures redesigned, not patched.** The API test fixtures model two distinct orgs: an operator org (org zero) and a tenant under test. "The operator's" and "the tenant's" are never the same org. Tests that assert operator behaviour act as an org-zero superadmin. Tests that assert tenant behaviour act in a tenant. No test is deleted, skipped or xfailed to get green, unless it asserts the retired two-org operator plane (it is then rewritten to assert the one-org one).
**R5. Billing: gmind is billed like any tenant.** (Decision Q1.) There is no billing exception for gmind. In Jaah mode its token usage is ingested and metered: `_billable(DEFAULT_ORG_ID)` is True. Its mode is not switched by this work.
**R13. gmind is otherwise an ordinary tenant.** (Decision Q8, accepted.) Node alarms go to gmind members, not to the operator runbook. gmind owners/admins get tenant-admin rights. gmind appears in tenant billing rollups. gmind's exception fields (`routes/orgs.py:694-699`) become operator-editable. gmind superadmins act as operators only while their active org is `operator`.
## How (architecture §2.1–2.2)
- `tenancy/ids.py:66`: `OPERATOR_PLANE_ORG_IDS = frozenset({ORG_ZERO_ID})` and rewrite the comment at 62-65. `is_tenant_org`'s fail-closed None and `is_operator_plane`'s rule are UNCHANGED.
- `seed.py` bootstrap admin (~:369-392):
- Fresh DB: create the org-zero membership FIRST, so `identities.first_membership` is the operator seat.
- Existing DB: if there is no org-zero membership, add one shaped like revision 0369 step 2: an identity-named `AdminUser`, `is_superadmin=True`, the Everyone group, bound `org_owner`. Use one shared helper, NOT `identities.add_membership`, which refuses superadmin.
- Never remove the gmind seat.
- Seat the e2e superadmin login (~:410-415) the same way, and fix the `seed.py:70` prose.
- `seed_dev_profile.py`: move the operator-facing dev rows to `ORG_ZERO_ID`: `_DEV_PROJECTS` (~:258), the dev node (~:290), dev epics/children (~:323, 409, 427, 447). The org-a probe (`_probe_orgs`, ~:165) stays in gmind.
- R3 prose: `tenancy/ids.py`, `seed.py:70`, the `vnodes.py:395` docstring, the `tenant_defaults.py`/`provider_auth_policy.py` headers, the `cloud_node_launch.py:260` and `_share_is_the_nodes` docstrings. "Operator, Default" becomes "org zero (Operator)". (shared-storage.md and the headscale docs belong to C3/C5; fix any leftover.)
- Fixtures, in conftest + seed, NOT per test:
- `FIXTURE_ORG_ID = ORG_ZERO_ID` (`conftest.py` ~:843).
- `_adopt_org` (~:1279-1289) stamps `FIXTURE_ORG_ID`, not a re-spelled `DEFAULT_ORG_ID`.
- In `_SEAT_SHAPES` (~:1916) and `tenant_seats` (~:1932), the non-tenant org is `ORG_ZERO_ID`, and the tenant default stays org B.
- `logged_in_client` (~:1624) selects the admin's org-zero membership explicitly.
- Triage the ~68 files naming `DEFAULT_ORG_ID`:
- "the fixture org" becomes `FIXTURE_ORG_ID`;
- genuine gmind meaning (0369/0370 migration tests, the org-a probe) keeps it;
- a test that asserted gmind IS operator-plane is rewritten to assert it is a tenant.
- New tests:
- R1: the four ids facts.
- R2: fresh-seed and re-seed seat tests.
- R5: `_billable(DEFAULT_ORG_ID) is True` and `_billable(ORG_ZERO_ID) is False`.
- R13: `node_liveness` recipients for a gmind node; `Principal.is_org_admin` for a gmind admin.
- R13: a gmind superadmin active in gmind gets 403 from `require_operator`, and 200 after switching to org zero.
## PRE-MERGE CHECKS (all three BLOCKING; read-only against dev `dev.jaah.ai`, see `docs/architecture.md` § Environments)
1. **C4 excluded knobs.** C4's migration logged no `operator-knob credential … stays in gmind` line on dev. If it did, raise AskUserQuestion naming each credential and knob (never a secret) and resolve it before merging.
2. **C3 boot step.** Dev's boot log shows `gmind_tenancy.converge` steps 1, 2, 4 and 5 as `done` and step 3 as `skipped`. If not, fix that first.
3. **C5 ACL.** Read the live dev headscale policy back and confirm the `tag:org-gmind` → `main-private:2049`/`dev-private:2049` rule and the `tagOwners` entry are in place. If they are missing (C5's operator approval was declined or never applied), raise AskUserQuestion. Do NOT merge the flip until it is in place, because a new tenant-tagged gmind node would wedge on its hard EFS mount (Task 4926).
Cite all three results in the PR body.
## Security gate
This touches authz (operator verdict, seats), so the merge sits behind the security-surface human gate (charter § The approval stop).
## Quality bar (architecture §5)
The naive route stamps `DEFAULT_ORG_ID` across hundreds of tests or adds `is_operator=True` monkeypatches. That gets green with no real two-org model and is a FAIL. Move the fixture org once in conftest and seed, and keep tests that mean gmind meaning gmind.
Out of scope: Do NOT widen `is_tenant_org`'s fail-closed None or change `is_operator_plane`'s flag-and-org rule. Do NOT add a billing exemption for gmind. Do NOT change gmind's `jaah_ai` mode, quotas, provider-auth override, cron override or colocation/shared-store flags (0370). Do NOT move/re-own/delete/re-key any credential (C3/C4 own the data steps). Do NOT edit revisions 0369/0370. Do NOT skip/xfail/delete tests to reach green, and do NOT per-test-patch the failures in place of the fixture redesign. Do NOT touch tailnet/headscale or EFS (C3/C5). Do NOT touch `apps/web` beyond what an operator-page rendering requires (none expected). Do NOT promote to master.
Done means:
- R1 flag. Oracle: a unit test asserts the four facts. `grep -rn "OPERATOR_PLANE_ORG_IDS\|is_tenant_org\|is_operator_plane" apps/api/app` shows no reader re-spelling a two-org set or special-casing `DEFAULT_ORG_ID` as operator.
- R2 seats. Oracle: a fresh-seed test shows the admin's org-zero superadmin seat exists and is `first_membership`; login, then switch to `operator`, passes `require_operator`. A re-seed test shows an admin seated only in gmind gains an org-zero seat and keeps the gmind seat. The e2e superadmin login gets the same treatment.
- R3 prose. Oracle: `grep -rni "default org\|Operator, Default" apps/api/app docs` returns no line calling gmind or the default org operator-plane. `python3 tools/check_context.py` is green.
- R4 real redesign. Oracles:
- `conftest.py` sets `FIXTURE_ORG_ID = ORG_ZERO_ID`, and `_adopt_org` stamps `FIXTURE_ORG_ID`;
- `git diff develop -- apps/api/tests | grep -c '^+.*\(skip\|xfail\)'` → 0;
- `git diff develop -- apps/api/tests | grep -c '^+.*\(is_operator\|is_operator_plane\|OPERATOR_PLANE_ORG_IDS\).*monkeypatch\|monkeypatch.*\(is_operator\|OPERATOR_PLANE\)'` → 0;
- the count of `DEFAULT_ORG_ID` occurrences under `apps/api/tests` does not go up versus develop;
- pytest's collected count does not drop (cite before/after numbers).
- R5 billing. Oracle: unit tests show `_billable(DEFAULT_ORG_ID) is True` and `_billable(ORG_ZERO_ID) is False`.
- R13 tenant. Oracle: unit tests show `node_liveness` recipients for a gmind node are gmind members, `is_org_admin` is true for a gmind admin, and a gmind superadmin active in gmind gets 403 from `require_operator`, then 200 after switching to org zero.
- Tenancy oracles. Oracle: `python3 tools/tenancy_oracles.py` and the tenancy stages it drives pass locally (they run in CI only on master PRs, so run them here).
- Pre-merge checks 1–3 above are cited in the PR body.
- Gate. Oracle: `./dc check full` green; cite the summary.
Required checks: test-stack
Outcome
Operator plane is now org zero only; gmind is a tenant. Seeds give superadmins an org-zero seat; fixtures redesigned (FIXTURE_ORG_ID=org zero). Full gate green, dev checks 1–3 pass, security gate approved. PR #4624 merged. Deferred: system actor still in gmind.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Awaiting response
- Awaiting response
- Awaiting response
- Runner resumed
- Awaiting response
- Awaiting response
- Runner resumed
- Runner ended (done)