Node org move: remove the old org's
## Defect
PRD R3.4 (epic #5680) requires: "After the move the node holds no org-A secret … and it runs only org-B tasks." The node move (#5697, PR #4982, `apps/api/app/tenancy/transfer_node.py`) erases slots and account dirs and retires NodeVault grants. Its own report lists what it leaves behind on a node that is now org B's:
1. **Rescue bundles.** A forced `erase_tree` leaves erase-rescue bundles of org A's code on the node. The project move does the same on old-org nodes.
2. **Compose stacks.** Org A's compose stacks and volumes stay up, because erase sends no `stack_down_cmd`. A stack's `.env` can hold org-A secrets.
3. **Console access.** Org A's console relay tickets and direct-console grants on the node are not revoked.
4. **Clones.** Org A's project clones that have no slot-0 row are not erased.
5. **Tailnet tags.** The node is not retagged onto org B's tailnet tags.
6. **Fence lift.** A node that never confirms its cleanup stays fenced, and no operator control lifts the fence.
#5695 also deferred: (7) dynamic environments of non-live tasks follow their task into org B but still name the old-org node, and no teardown is ordered. (8) A refused or failed move is not audited.
## Fix
Extend the existing node rule and its erase intents. Do not add a new mechanism:
- Items 1, 2 and 4: send `stack_down_cmd` (volumes included) and erase the clones, all in the same durable intent the slot erase uses. Ask jaahd to erase without a rescue bundle (add `rescue: false` to the erase payload and honour it in jaahd).
- Item 3: revoke old-org console tickets and grants for the node in the move transaction.
- Item 5: request a tailnet retag through the existing node tag path.
- Item 6: add an operator "Lift fence" action on the node page, with an audit row.
- Item 7: order a teardown for the moved environment rows that name an old-org node.
- Item 8: write an `org_transfer_refused` activity row in the source org.
## Done means
- An API test (`apps/api/tests/test_org_transfer_node.py`) shows that an idle-node move writes a stack-down intent, a clone erase and an erase with `rescue: false`; revokes console tickets and grants; and requests the retag. Each is asserted on the DB/intent rows.
- A jaahd test shows that `rescue: false` leaves no rescue bundle.
- An API test shows that a project move with a non-live dynamic environment on an old-org node orders that environment's teardown.
- An API test shows that a refused move (409 blocked) writes exactly one refused-audit row and moves nothing else.
- On the node page, an operator can lift a fence, and an audit row is written (component test plus a dev check).
- `./dc check full` is green, the PR is merged, and its Jaah Deploy succeeded.
Out of scope: do NOT change which entities move or the manifest contract. Do NOT touch `offboard.py` logic beyond calling it. Do NOT allow any other path to change `nodes.org_id`.
Outcome
Node move now purges stacks+volumes, erases with no rescue bundle and erases clones, revokes console access, and retags the tailnet. Added operator Lift fence, idle-env teardown and refused-move audit. PR #5014 merged, deploy OK. Full gate passes except the develop NodeLink selftest. Deferred UI nits are in the report.
Artifacts
Activity
- Ready
- Runner started
- Runner ended (done)