Standalone org object transfers

Task 5698 · Done · Coder · created · updated

## Epic requirement (verbatim) > Allow Operator to change ownership of everything. If he changes the ownership of a project - the project and all tasks and everything related moves to another org. > > Operator can see everything. He has a global org filter. Make it a state of the art method of managing - do a deep web research Full settled specs: the PRD (task #5681) and the Architecture (task #5682) of epic #5680 — read them with `task_read` / `task_read_artifact`. This body carries the parts that bind THIS child verbatim. ## Requirements this child serves (PRD R4 + R1.1, verbatim) - R1.1 (objects part) "the standalone org objects (R4): **Credential, Cloud connection, Account (runner auth account), Node group, Cron, Automation, Task trigger, Playbook**." - R4.1 "Credential, cloud connection, account, node group, cron, automation, trigger, playbook: moves with its own derived rows (runs, deliveries, capacity rows, etc.)." - R4.2 "A link from a project/node that stays in org A to the moved object is removed and listed under *Left behind*/*Dropped*. A node group moves WITH its member nodes (R3 applies to each) — assumed, since capacity is the group's nodes." ## Approach (Architecture C-E, verbatim) - Rules on the same engine. Each Stored root plus its Derived children (follow by key): - **credential**: CloudConnection, OrgSharedStore, OAuth flows, NodeVault operation items. - **account**: runners, observations. - **cron**: cron_runs. - **automation**: its When-detail cron/trigger, the way `automation_service` moves them together. - **trigger**: deliveries, notices, dedup. - **playbook**. - **node_group**: capacity lines, group projects (`Detach`), and **member nodes**, each through C-D's node rule in the same transaction (R4.2). - `cloud_connection` is `Derived(Credential)`, so it cannot move alone. "Change org…" on a connection moves its CREDENTIAL, and the manifest names every other connection on it (D7). - Referencers staying in A become `Detach` and are listed under `left_behind`/`dropped` (R4.2). Examples: `project_credentials`, `node_groups.credential_id`, `task_triggers.preflight_credential_id`, `gha_runner_pools.credential_id` (pool `Erase`). - Operator plane as target is a Block EXCEPT for `cron`/`automation` (system rows legitimately live on the operator plane; the existing cron re-file allows it). Per-org name clashes (`automations`/`task_triggers` slug, `playbooks` name) → "rename first" Block (D8). Register the 8 kinds via `transfer.register` (contract from the engine child, unchanged). The rule satisfiability/totality test covers all 8 roots. The node rule comes from the node child — reuse it, never copy it. ## Acceptance criteria (PRD, verbatim) - AC11 "Standalone object move (each of the 8 types): object shows org B with its history; A's projects that used it no longer list it; the manifest named them. Node group moves with its nodes." - AC14 (objects) "Failure is all-or-nothing: a blocked/failed move leaves every row in A and the UI states why." - Quality: a cloud-connection move's manifest names the credential that actually moves and every sibling connection on it, so the operator is never surprised by what moved. Credentials & reach (Architect §8, verbatim rows: oracle | credential/identity | status): - AC4 and unit/integration tests | worktree only (`./dc check full`) | n/a - AC5/AC9/AC10 need org-B capacity and a Multi-Node Host | dev inventory via the operator login | **verified**: Org B owns 2 nodes; Gmind 3, Wealthcharts 3, TimeSaver2 1, Operator 1; `/api/vnode-hosts` lists 3 hosts Out of scope: - Do NOT change the tenant-facing cron org re-file (D13) or `automation_service`'s own move semantics — call them. - Do NOT copy credentials into B or leave a staying-in-A project silently bound to a moved credential. - Do NOT move users/memberships or groups. - Do NOT build the dialog UI. PRD §5 (verbatim, binds every child): - Do NOT add user/membership moves (Q1 not selected) or change `admin_users.org_id` writes. - Do NOT change the account-menu org switcher (`components/account/OrgSwitcherList.tsx`) or `/api/auth/switch` semantics, or the stale-tab guard (`lib/activeOrg.ts`, `stale_tab_guard.py`). - Do NOT add impersonation / "act as org". - Do NOT add plan-limit checks to moves; do NOT touch billing ledgers' org (R2.2) or Stripe objects. - Do NOT weaken tenant read narrowing for non-operators (`authz.py::_bind_read_scope`); the filter only narrows an operator further. - Do NOT allow any path other than the operator move to change `nodes.org_id`. - Do NOT carry a shared dependency or a secret silently into org B; do NOT copy credentials. - Do NOT add email notifications, bulk move, or scheduled moves. - Do NOT move offboarding/deletion logic (`tenancy/offboard.py`, `org_delete.py`) — reuse, don't rewrite. - Nothing ships behind a flag or setting (R6/AC20: "Everything above is ON by default for operators on the dev control plane (dev.jaah.ai) with no flag; no \"ship disabled\".") Done means: - AC11 — `apps/api/test_org_transfer_objects.py`, one test per kind (credential, cloud_connection→its credential, account, node_group with member nodes, cron, automation, trigger, playbook): object + derived history in B; A's referencers detached and listed in the manifest. - AC14 (objects) — for EVERY one of the 8 kinds, a blocked move (name clash in B where the kind has a per-org name; operator-plane target otherwise; and a node group whose member node has a live org-A runner) leaves all rows unchanged (snapshot test). - `GET /api/operator/transfers/kinds` lists all 11 kinds. - `apps/api/test_transfer_rules.py` passes with the 8 roots. - `./dc check full` green.

Outcome

The 8 standalone org-object moves shipped (credential, cloud connection, account, node group with its nodes, schedule, automation, trigger, playbook). Full gate green; PR #4992 merged; Jaah Deploy succeeded. Decision: trigger notices stay in org A.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)