Live-runner carry-over at node arm
## 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.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)