NodeLink attach doors on nodes
## Context
Epic: refactor NodeLink into many shared filesystems per org (PRD Task 5687, Architecture Task 5688). The original request, verbatim:
> Refactor NodeLink to:
> - NodeLink is a way to share a file system between multiple nodes (self-hosted, cloud, Jaah hosted, etc.)
> - Any tenant (that has NodeLink feature enabled) can create one or more NodeLinks - it is just an EFS with a default mount point
> - Org can have multiple NodeLinks
> - Any node can have multiple NodeLinks associated with it
> - Shared accounts as we have them now - turns into a one instance of NodeLink with mount point at {jaah-home}/auth . This is the only thing that has an effect of sharing accounts between nodes
> - Main purpose is to have a swarm of agents operating on a shared file system
The node-side ATTACH half of the Architect's **C1** (planner split). It calls `app/nodelinks.py` (`set_node_attachments`, `set_attachments`, the `node_group_nodelinks` table) from the core child — dependency.
## Acceptance criteria this child carries (PRD, verbatim)
- **AC2.1** "Node detail shows a **NodeLinks** section listing attached NodeLinks with per-node mount status (mounted / not mounted + reason) and an Attach/Detach control offering only the node's own org's NodeLinks. The old \"NodeLink\" on/off switch in Properties is gone (replaced by the accounts NodeLink's attachment)." (API half.)
- **AC2.4** "Cross-org is impossible: the API refuses attaching a NodeLink to a node of another org (403/404) …" (node-route half.)
- **AC2.5** "Node wizard (every create door: cloud, node-group, self-hosted, Jaah-hosted, co-located) offers the org's NodeLinks to attach; the accounts NodeLink is checked by default when the org has the feature (today's behaviour); create APIs accept the attachment list (absent = accounts only iff allowed)." (API half.)
- **AC3.4** "Detaching the accounts NodeLink from a node has today's switch-off effect (node stops being offered shared accounts); attaching requires the plan feature. *Verify:* API + dev CP."
- **AC6.1** "… Orgs without it see no write controls and every write door refuses with the existing message." (these doors.)
## How (Architecture §5 C1 row, §2.2b)
1. `GET/PUT /api/nodes/{id}/nodelinks` — GET lists the org's NodeLinks the node can attach (its org; a co-located node's TENANT org) with `attached: bool` and the per-node state from the wire child's snapshot ingest if available (degrade to `unknown` until it ships — do not block on it); PUT sets the node's attachment set via `nodelinks.set_node_attachments`. Tier = today's node-switch tier (org admin of the node's org / superadmin). Attaching needs the plan term; detaching always works. Cross-org ids → 403/404.
2. `PUT /api/nodes/{id}` compat: `nodelink_enabled` maps to attach/detach of the accounts NodeLink; **`nodelink_root` is no longer accepted** from the body (`routes/nodes.py` ~2750 `_NODELINK_COLUMNS`) — the root is node-reported only.
3. Every create door takes `nodelinks: list[uuid] | None` (absent = accounts only iff allowed; the legacy `nodelink: bool` still honoured): `POST /api/node-orders`, `/api/nodes/cloud`, `/api/nodes/self-hosted`, vnode create (`app/vnodes.py` ~517 `nodelink` param), and **node groups** — store the set in `node_group_nodelinks` and attach it to every member reconcile creates (`apps/api/app/node_group_reconcile.py`, `routes/node_groups.py`). Same-org check on every door.
4. Detaching accounts keeps `nodes.shared_accounts_enabled` dual-written (the core service does it) so every existing shared-account consumer sees today's switch-off effect.
Quality bar: a create door given a NodeLink of another org must refuse cleanly with a human message, never silently drop it. Detaching accounts must have exactly today's switch-off effect — prove it with the existing switch-off tests, not new weaker ones.
Credentials & reach:
- Unit, migration and guard tests — **verified**: worktree only.
Out of scope: Do NOT allow cross-org attachment. Do NOT change billing/plan terms beyond `nodelink_allowed`. Do NOT add a migration (the core child owns the only one; `node_group_nodelinks` exists already). Do NOT add project/task binding or scheduler affinity. Do NOT drop `nodes.shared_accounts_enabled` (deferred F1). Do NOT build the UI, the agent endpoint or the prompt line (sibling children).
Done means:
- Node routes — API tests: GET lists only the node's org's (tenant org's for co-located) NodeLinks with `attached`; PUT attaches 3 at once; PUT with another org's id → 403/404; attach without plan term → 403 with the existing message; detach without plan term → works.
- Compat — tests: `nodelink_enabled: false` detaches accounts and the node stops being offered shared accounts (existing switch-off assertions unchanged); `nodelink_root` in the body is refused/ignored and the column unchanged.
- Every create door — one test per door (node-order, cloud, self-hosted, vnode, node group) that a node is born with accounts + a chosen general NodeLink attached; absent list → accounts only iff allowed; legacy `nodelink: false` → none; other-org id refused; node-group reconcile attaches the group's set to a new member.
- `./dc check full` green; PR merged to develop; Jaah Deploy of the PR succeeded — cite PR number, merge sha, deploy run.
Outcome
NodeLink attach doors: GET/PUT /api/nodes/{n}/nodelinks plus a `nodelinks` list on every create door, same-org enforced (cloud, self-hosted, vnode, orders, groups and reconcile). nodelink_root is no longer accepted. Full gate green; PR #5011 merged as 5fafd4d01; Jaah Deploy run 38156532605 succeeded. Deferred: a group can't say "none" (needs a column).
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)