Live-runner carry-over at node arm

Task 5693 · 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. ## Requirement this child serves (PRD R2.3, verbatim) "Live tasks (active / awaiting_response / paused) do NOT block the move (Q5: allow live). A running runner keeps running to completion on its current old-org node; its task already belongs to org B; its settle, artifacts, telemetry and transcript land in org B. Every dispatch AFTER the move targets org-B capacity only." Acceptance it serves (PRD AC5, verbatim — this child owns the RUNNER half; the end-to-end browser run is verified once the move exists, by the "Change-org dialog" child): "A live task during the move: its runner finishes; settle, report and transcript appear on the task in org B; the next dispatch of any task of that project goes only to an org-B node." Why (Architecture recon): "`_check_token` binds the node's org before task resolution, so another org's task 404s; the git token sits behind `_require_derived_node`." ## Approach (Architecture C-C, verbatim) - Table `org_transfer_carryovers`. Verdict `Shared`: it is operator machinery, read before any org is bound. Columns: `id`, `transfer_id`, `task_id`, `node_id`; `worktree_id` NULL, `environment_id` NULL (exactly one is set); `launch_session_id`, `from_org_id`, `to_org_id`, `created_at`, `ended_at`. - **Grant.** Written by the engine for each task of the moved project that has a live session: a static slot (`slot_state.live_hold_for_task`): that slot's row is NOT erased now; a dynamic environment (`task_environments` in a holding state). The environment row follows its task into B (Derived), its `env_intents` stay deleted, and its teardown is deferred like the slot's. - **Rebind, inside `routes/agent.py::_check_token`, BEFORE `seam.bind`.** This covers both `_check_token` and `_check_token_for_task` callers, about 69 routes, task id in header or body. Lookup: `carryover.scope_for(conn, node_id=caller.node_id, task_id=<X-Agent-Task-Id>) -> UUID | None`. It is a raw `text()` read on the Shared table, so it does not stamp the session; add it to `ALLOWED_RAW_SQL_SITES`. A hit binds `to_org_id` for this request instead of the node's org. No hit means today's behaviour. - **Holder check.** `_refuse_a_node_not_holding` / `_task_holder_node_ids` add the open carry-over's `node_id` to the holder set, because the worktree row stays in org A and is invisible under B. - **Secret routes (`_require_derived_node`) refuse a carry-over**, except `get_task_git_token` for that task's own project repo. The token is minted exactly as at launch: the task's `started_by` user's git connection, read with `seam.bound_to(from_org_id)` for that one read (D18). Any other secret (credentials, account keys) is refused. - **End.** The existing end edges close the carry-over (`ended_at`) and write the deferred `Erase` intent for its slot or environment teardown in the same transaction: `slot_state.release` (commit=False path included); `user_session.ended`; environment teardown; the task leaving the live states. No reaper. - **Scheduler: no change.** - **ADR.** ADR-0083 "CP-side backstops" and ADR-0118 G3 are amended in place: one sanctioned cross-org arm, per (node, task, session), operator-initiated, audited, closed on end edges. Interface contract (Architecture §4, verbatim — the engine child calls `grant`): ``` carryover.grant(session, *, transfer_id, task_id, node_id, worktree_id=None, environment_id=None, launch_session_id, from_org, to_org) -> None carryover.scope_for(conn, *, node_id, task_id) -> UUID | None # raw read, non-stamping carryover.close(session, *, worktree_id=None, environment_id=None, task_id=None, launch_session_id=None) -> None # exactly one selector: slot_state.release (worktree_id), env teardown (environment_id), # user_session.ended (launch_session_id), task leaving the live states (task_id); writes the erase intent slot_destroy.enqueue_erase(session, *, node_id, path, label) -> None # payload-only erase intent ``` Code anchors: `apps/api/app/routes/agent.py` `_check_token` (~331), `_check_token_for_task` (~465), `_require_derived_node` (~6470), `get_task_git_token` (~6559), `_task_holder_node_ids` (~6905), `_refuse_a_node_not_holding` (~6959). This child owns the ONE schema migration of its wave (the new table); add its tenancy-manifest verdict. ## Acceptance criteria 1. With a carry-over granted (test fixture: task moved to org B, node in org A), the org-A node's token can call the runner routes it already uses for that task (settle, artifact upload, telemetry, transcript, questions) and every write lands in org B; without a carry-over the same calls 404 exactly as today. 2. The holder check admits the carry-over node for that task only; another org-A node is refused. 3. Every `_require_derived_node` secret route refuses the carry-over, except `get_task_git_token` for the task's own project repo, which returns a token minted from the `started_by` user's git connection under `from_org`. 4. Each end edge (slot release incl. commit=False, session ended, env teardown, task leaving live states) sets `ended_at` once and writes exactly one deferred erase intent; after that the node is back to 404 for the task. 5. Bounded: a carry-over never widens to another task, another node or another session (tests for each). 6. `scope_for` is a census-listed raw site; existing tenancy/bypass census tests pass. 7. ADR-0083 and ADR-0118 G3 amended in place (current-state wording, no "superseded" trail). Credentials & reach (Architect §8, verbatim rows: oracle | credential/identity | status): - AC4 and unit/integration tests | worktree only (`./dc check full`) | n/a Out of scope: - Do NOT write the transfer engine or the move API; `grant` is called by the engine child — test it via a direct fixture. - Do NOT change the scheduler/dispatch logic (the org floor already keeps the task off org-A capacity). - Do NOT add a reaper; close only on the existing end edges. - Do NOT let a carry-over fetch any secret other than its own task's git token. - Do NOT touch `seam.py` beyond binding an existing UUID scope (the filter child owns `seam.py`). 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: - AC1–AC5 — new API tests in `apps/api/` (one per route family + one per end edge + the three widening negatives) pass. - AC6 — `apps/api/test_tenancy_bypass_sites.py` and `test_authz_policy.py` pass with `scope_for` listed. - AC7 — `docs/adrs/README.md` ADR-0083 and ADR-0118 entries describe the carry-over arm; `python3 tools/check_context.py` green. - `./dc check full` green.

Outcome

Live-runner carry-over built: table, _check_token rebind on the carried task's routes only, holder check, git-token arm, end edges, erase on release. Full gate green; PR #4968 + census follow-up #4969 merged, both deploys succeeded.

Pull request

Artifacts

Activity

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