Repository navigation
Description
Task
| slice | size | difficulty | model | who | estimated lines |
|---|---|---|---|---|---|
| Escape into the VM reaches only the declared union | L | needs care | strongest | an agent | 1060 (580 code, 480 tests) |
On a VM-backed host, two nodes under separate state dirs answer their names from the one machine-global answerer the service manager holds; the advisory's privileged step installs it as the operator from a root-owned copy and hands the hook port over from the running daemons within a bounded window, re-binding the interim if the unit fails; a stopped session leaves the answerer answering; a node that finds the channel publishes its rows instead of binding and says so at start; a channel peer with another uid is refused; a second node's claim on a held name is refused and reported; an address outside the answer rule is refused; both names answer again after a service restart with no session action, and the negative TTL served across the restart is no longer than the positive one; a native host is not offered the step and its session-start advisory goes quiet on the interim. Known residual under the single-operator premise: another local process can take the hook port in the gap between the release and the unit binding; the command's failure path names it as a collision and the daemons re-bind.
Covers:
- NET-122
Layers:
- vmhost:
- crates/minvmd/src/bin/min-answerer.rs (new): a dedicated
min-answererbin target built from the answerer module, carrying the answerer and nothing else (no VM-management surface on the hooked port, and a native install can ship it without minvmd); it reads no configuration beyond what the units pass it; the service manager runs it as the operator's uid, never root: the host's one box-zone answerer on the host loopback at the hook port, its listener socket and its channel socket both held by the service manager and inherited (launchd socket activation on macOS, a systemd system socket unit with User set to the operator on Linux, never a user-session unit); the channel socket is machine-global, one per host whatever the node's state dir: /run/minimal/answerer.sock on Linux (the socket unit's path, its directory the unit's RuntimeDirectory), /Library/Application Support/minimal/run/answerer.sock on macOS (a persistent root-owned directory the privileged step creates, the LaunchDaemon's socket); answers with the shared zone decision from rows that node daemons write over the channel; a writer is authenticated by peer credentials (SO_PEERCRED on Linux, LOCAL_PEERCRED on macOS) against the operator uid, and the hello carries a node id and a channel protocol version, so a name belongs to the node that first published it (per node, not per uid; the node id is the canonicalized state base dir plus the VM name, stable across restarts and symlinked paths, and self-asserted within the operator uid, so per-node ownership is cooperative under the single-operator premise, not adversarial) and a second node's publish of the same name is refused and reported; every published A address must fall inside the reserved range, the node's addresses or 127.0.0.1 and is refused otherwise; rows live only while the node's connection is up, so a restart holds nothing until nodes reconnect and re-publish; the negative TTL it serves (the SOA minimum) is no longer than the positive TTL (15 s), so a restart's absence window cannot outlive itself in a host resolver's negative cache - crates/minvmd/src/net/answerer.rs: hosting the answerer in the daemon becomes the recorded interim: at start the daemon connects to the machine-global channel socket first and publishes its rows there; whether the service is installed is decided by its install marker (the LaunchDaemon plist on macOS, the systemd unit file on Linux), never by the socket path existing: marker present and the socket refuses or times out is an error surfaced at session start, never a reason to host; marker absent with a stale socket file present counts as absent and the daemon logs the stale path; without the service the daemon next tries the interim's per-state-dir channel (
<state>/providers/local-minvmd0/answerer.sock), which only the node hosting the interim binds, so named VMs of that state dir publish into it; it hosts the answerer itself only when no channel answers and the hook port is free (a port held by anything but the service is a surfaced collision, not a fallback), never both; it keeps the channel connection for the session, reconnects with backoff on loss and re-publishes idempotently, so a service restart is a short absence window and never a session restart; one publish path either way;resolve_channel_sockreturns the machine-global path, overridable by an environment variable only in test builds and the e2e harness, never by production config - crates/minvmd/src/control.rs: the daemon's existing local control socket gains a release-answerer request: the daemon stops its interim answerer and frees the hook port, then waits up to 15 s (longer than the command's whole 7 s handoff, so a daemon never re-binds while the unit is still starting) for the channel socket and switches to channel-client mode (connect, publish its rows); a release-cancel request makes it re-bind its interim answerer at once and say so, and if neither the channel nor a cancel arrives within the 15 s it re-binds on its own, so the hooked port is never left unanswered for longer than the bound; a daemon with no interim answerer answers either request as a no-op; the control socket accepts release and release-cancel only from uid 0 or the operator uid, checked by peer credentials as on the channel; no new signal surface
- crates/minvmd/src/cmd/run.rs: makes the publish-or-host choice at start and surfaces the interim at session start beside the resolver advisory
cargo nextest run -p minvmd answerer_service_answers_rows_from_two_nodescargo nextest run -p minvmd helper_publishes_when_service_holds_the_portcargo nextest run -p minvmd answerer_channel_refuses_foreign_uidcargo nextest run -p minvmd answerer_service_forgets_rows_of_a_gone_nodecargo nextest run -p minvmd second_node_publishing_a_held_name_is_refusedcargo nextest run -p minvmd answerer_refuses_an_address_outside_the_answer_rulecargo nextest run -p minvmd nodes_republish_after_a_service_restartcargo nextest run -p minvmd present_channel_that_refuses_is_an_error_not_a_hostcargo nextest run -p minvmd channel_path_is_machine_global_across_state_dirscargo nextest run -p minvmd name_ownership_is_per_node_not_per_uidcargo nextest run -p minvmd release_frees_the_port_and_switches_to_the_channelcargo nextest run -p minvmd release_rebinds_the_interim_when_the_channel_never_comescargo nextest run -p minvmd release_cancel_rebinds_immediatelycargo nextest run -p minvmd stale_channel_without_install_marker_hosts_the_interim
- crates/minvmd/src/bin/min-answerer.rs (new): a dedicated
- cli:
- crates/minimal/src/resolver.rs: on a VM-backed host the advisory's privileged step also copies the answerer program to a root-owned, non-user-writable path and installs the service units pointing at that copy of
min-answerer(a launchd plist with socket activation on macOS, a systemd system socket and service pair running as the operator on Linux), never at a user-writable binary; the command installs the units without starting the port socket, asks the node daemons the rendering CLI knows (its own state dir and its named VMs) to release the hook port over their control sockets, through amin-answerer release|release-cancel --control <sock>client verb on the root-owned copy (never exposed on the hooked port); a port held by anything else, a daemon of another state dir included, is a surfaced collision and a non-zero exit, waits up to 2 s for the port to be free, then starts the socket unit and waits up to 5 s for it to be active (7 s worst case, well under the macOS dead-answerer stall); if either wait runs out, the command sends a release-cancel to each daemon it released so it re-binds its interim at once, and exits non-zero naming the reason (a port another process took in the gap is named as a collision), so the host is never left with nobody answering; with no daemon running it starts the unit directly; launchd follows the same release-then-load order; uninstall removes the units, the program copy and the channel socket file; on a native host the advisory does not offer the answerer step (the in-daemon answerer stays the interim until the native channel-client task lands), so the native session-start advisory reaches its quiet state once the resolver step is done;answerer_sourceresolves themin-answererbinary and never falls back to a bareminvmd; the hook probe reports whether the answerer is manager-held or session-held and compares the installed copy's channel protocol version with the daemon's, re-surfacing the advisory on a mismatch so an upgrade re-runs the step cargo nextest run -p minimal advisory_installs_manager_held_answerercargo nextest run -p minimal hook_probe_resurfaces_advisory_on_protocol_mismatchcargo nextest run -p minimal advisory_releases_the_port_before_starting_the_unitcargo nextest run -p minimal native_advisory_omits_the_answerer_stepcargo nextest run -p minimal answerer_source_never_names_an_absent_minvmd
- crates/minimal/src/resolver.rs: on a VM-backed host the advisory's privileged step also copies the answerer program to a root-owned, non-user-writable path and installs the service units pointing at that copy of
- docs:
- docs/specs/18-spec-box-networking/18-spec-box-networking.md: NET-122 gains the sub-requirement "WHERE the host is hooked THE SYSTEM SHALL install, by the privileged step, the box-zone answerer as a host service whose listener and channel sockets the service manager holds, running as the operator, from a root-owned non-user-writable program", and NET-138's interim sentence points at it and says the interim channel is per state dir, so two state dirs on a host without the service collide until the service is installed
- e2e:
- scripts/session-e2e.sh: adds the case answerer_survives_session_stop to the standalone VM-case list, run where a lane can install the unit (the KVM lane as root with the systemd variant; the macOS lane only if it can install a LaunchDaemon, else it fails rather than self-skips so the pin is never silent): node A starts and hosts the interim; running the advisory command from A's CLI hands the port to the service and A becomes a channel client, its name still answering; node B then starts under a second state dir (the
--minimal-dirtwo-daemon pattern) and publishes over the one machine-global channel; stopping one node's session leaves the sibling's name answering; a service restart under two connected nodes answers both names again without a session action; a session started afterwards publishes instead of binding; every case that runs the advisory as root tears down the units, the program copy and the channel socket through the EXIT trap, so the shared runner is left as found; the native-lane resolution cases stay green with the step omitted on native hosts ./scripts/session-e2e.sh answerer_survives_session_stop./scripts/session-e2e.sh native_resolution_without_proxy_env./scripts/session-e2e.sh box_name_resolves_natively_without_proxy
- scripts/session-e2e.sh: adds the case answerer_survives_session_stop to the standalone VM-case list, run where a lane can install the unit (the KVM lane as root with the systemd variant; the macOS lane only if it can install a LaunchDaemon, else it fails rather than self-skips so the pin is never silent): node A starts and hosts the interim; running the advisory command from A's CLI hands the port to the service and A becomes a channel client, its name still answering; node B then starts under a second state dir (the
Waits on:
Design basis (architecture ruling; block PASS-with-conditions, conditions in the layer lines)
-
Design §7.1: one always-on box-zone answerer per host, its listener and channel sockets held by the service manager, the answerer running as the operator and never root; node daemons write the map over the authenticated local channel (uid match is the authentication on a single-operator host; no per-boot token); names are first-writer-owned per connection with cross-node collisions reported; the service enforces the answer rule (reserved range, node addresses, 127.0.0.1) itself.
-
Custody: everything the privileged step installs lands at root-owned, non-user-writable paths written by that step; a unit pointing at a user-writable binary is the escalation the rule prevents, so the step copies the program (a dedicated small answerer binary is preferred for size and upgrades) and the copy carries the channel protocol version.
-
Rows are volatile by design: a persisted row would answer for a dead node. Node daemons hold the channel for the session, reconnect with backoff, re-publish idempotently.
-
Order: channel first, host only when the channel socket path is absent and the hook port is free; a present socket that refuses or times out is a surfaced error; a port held by anything but the service is a surfaced collision.
-
Hosting the answerer in the VM host daemon (T65) is the recorded single-operator interim; NET-124 to NET-128 hold wherever the answerer is hosted.
-
Amended after the first build attempt (architecture ruling, PASS):
- The channel is machine-global (
/run/minimal/answerer.sock,/Library/Application Support/minimal/run/answerer.sock), so co-resident nodes share one answerer. Writers are authenticated by peer credentials plus a node id, and names are first-writer-owned per node. - The privileged step hands the hook port over from running daemons: release, then ≤2 s for the port, then ≤5 s for the unit. Each daemon waits 15 s for the channel and re-binds at once on a release-cancel, so the hooked port is never left unanswered.
- The answerer is a dedicated
min-answererbinary, so native installs can ship it without minvmd. - Native hosts are not offered the step until T90 makes minimald a channel client.
- The served negative TTL is no longer than the positive one, so a restart's empty window does not outlive itself in caches.
- The channel is machine-global (
-
Implementation ruling: without the service the interim keeps its per-state-dir channel and nodes try the machine-global channel first; the privileged command releases only the daemons the rendering CLI knows, through a
min-answererclient verb, and the control socket accepts release only from uid 0 or the operator; "installed" is decided by the service's install marker, never by a socket file, so a stale socket outliving an uninstall hosts the interim; on macOS the channel lives in a persistent root-owned directory because /var/run clears at boot; node ids are the canonicalized state dir plus VM name, cooperative within the operator uid.
Plan block
### T71 — Hold the box-zone answerer as a host service installed by the privileged step
slice: S9
kind: task
covers: NET-122
size: L
est: 580 code, 480 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the bundle's VM host daemon log tail says at start whether the node published its rows into the manager-held answerer or hosted the answerer itself as the interim, with the hook port and the channel path; it names each release request it answered and whether it re-bound the interim after a failed handoff; the answerer service's own log names each node that connected (uid and node id) and each row it published or withdrew
observability: one info line at daemon start naming publish-or-host, the hook port and the channel path; one info line per release and per re-bind; one info line per node connection and disconnection at the answerer service; one warn line per refused channel peer naming its uid
layers:
- vmhost:
- crates/minvmd/src/bin/min-answerer.rs (new): a dedicated `min-answerer` bin target built from the answerer module, carrying the answerer and nothing else (no VM-management surface on the hooked port, and a native install can ship it without minvmd); it reads no configuration beyond what the units pass it; the service manager runs it as the operator's uid, never root: the host's one box-zone answerer on the host loopback at the hook port, its listener socket and its channel socket both held by the service manager and inherited (launchd socket activation on macOS, a systemd system socket unit with User set to the operator on Linux, never a user-session unit); the channel socket is machine-global, one per host whatever the node's state dir: /run/minimal/answerer.sock on Linux (the socket unit's path, its directory the unit's RuntimeDirectory), /Library/Application Support/minimal/run/answerer.sock on macOS (a persistent root-owned directory the privileged step creates, the LaunchDaemon's socket); answers with the shared zone decision from rows that node daemons write over the channel; a writer is authenticated by peer credentials (SO_PEERCRED on Linux, LOCAL_PEERCRED on macOS) against the operator uid, and the hello carries a node id and a channel protocol version, so a name belongs to the node that first published it (per node, not per uid; the node id is the canonicalized state base dir plus the VM name, stable across restarts and symlinked paths, and self-asserted within the operator uid, so per-node ownership is cooperative under the single-operator premise, not adversarial) and a second node's publish of the same name is refused and reported; every published A address must fall inside the reserved range, the node's addresses or 127.0.0.1 and is refused otherwise; rows live only while the node's connection is up, so a restart holds nothing until nodes reconnect and re-publish; the negative TTL it serves (the SOA minimum) is no longer than the positive TTL (15 s), so a restart's absence window cannot outlive itself in a host resolver's negative cache
- crates/minvmd/src/net/answerer.rs: hosting the answerer in the daemon becomes the recorded interim: at start the daemon connects to the machine-global channel socket first and publishes its rows there; whether the service is installed is decided by its install marker (the LaunchDaemon plist on macOS, the systemd unit file on Linux), never by the socket path existing: marker present and the socket refuses or times out is an error surfaced at session start, never a reason to host; marker absent with a stale socket file present counts as absent and the daemon logs the stale path; without the service the daemon next tries the interim's per-state-dir channel (`<state>/providers/local-minvmd0/answerer.sock`), which only the node hosting the interim binds, so named VMs of that state dir publish into it; it hosts the answerer itself only when no channel answers and the hook port is free (a port held by anything but the service is a surfaced collision, not a fallback), never both; it keeps the channel connection for the session, reconnects with backoff on loss and re-publishes idempotently, so a service restart is a short absence window and never a session restart; one publish path either way; `resolve_channel_sock` returns the machine-global path, overridable by an environment variable only in test builds and the e2e harness, never by production config
- crates/minvmd/src/control.rs: the daemon's existing local control socket gains a release-answerer request: the daemon stops its interim answerer and frees the hook port, then waits up to 15 s (longer than the command's whole 7 s handoff, so a daemon never re-binds while the unit is still starting) for the channel socket and switches to channel-client mode (connect, publish its rows); a release-cancel request makes it re-bind its interim answerer at once and say so, and if neither the channel nor a cancel arrives within the 15 s it re-binds on its own, so the hooked port is never left unanswered for longer than the bound; a daemon with no interim answerer answers either request as a no-op; the control socket accepts release and release-cancel only from uid 0 or the operator uid, checked by peer credentials as on the channel; no new signal surface
- crates/minvmd/src/cmd/run.rs: makes the publish-or-host choice at start and surfaces the interim at session start beside the resolver advisory
test: cargo nextest run -p minvmd answerer_service_answers_rows_from_two_nodes
test: cargo nextest run -p minvmd helper_publishes_when_service_holds_the_port
test: cargo nextest run -p minvmd answerer_channel_refuses_foreign_uid
test: cargo nextest run -p minvmd answerer_service_forgets_rows_of_a_gone_node
test: cargo nextest run -p minvmd second_node_publishing_a_held_name_is_refused
test: cargo nextest run -p minvmd answerer_refuses_an_address_outside_the_answer_rule
test: cargo nextest run -p minvmd nodes_republish_after_a_service_restart
test: cargo nextest run -p minvmd present_channel_that_refuses_is_an_error_not_a_host
test: cargo nextest run -p minvmd channel_path_is_machine_global_across_state_dirs
test: cargo nextest run -p minvmd name_ownership_is_per_node_not_per_uid
test: cargo nextest run -p minvmd release_frees_the_port_and_switches_to_the_channel
test: cargo nextest run -p minvmd release_rebinds_the_interim_when_the_channel_never_comes
test: cargo nextest run -p minvmd release_cancel_rebinds_immediately
test: cargo nextest run -p minvmd stale_channel_without_install_marker_hosts_the_interim
- cli:
- crates/minimal/src/resolver.rs: on a VM-backed host the advisory's privileged step also copies the answerer program to a root-owned, non-user-writable path and installs the service units pointing at that copy of `min-answerer` (a launchd plist with socket activation on macOS, a systemd system socket and service pair running as the operator on Linux), never at a user-writable binary; the command installs the units without starting the port socket, asks the node daemons the rendering CLI knows (its own state dir and its named VMs) to release the hook port over their control sockets, through a `min-answerer release|release-cancel --control <sock>` client verb on the root-owned copy (never exposed on the hooked port); a port held by anything else, a daemon of another state dir included, is a surfaced collision and a non-zero exit, waits up to 2 s for the port to be free, then starts the socket unit and waits up to 5 s for it to be active (7 s worst case, well under the macOS dead-answerer stall); if either wait runs out, the command sends a release-cancel to each daemon it released so it re-binds its interim at once, and exits non-zero naming the reason (a port another process took in the gap is named as a collision), so the host is never left with nobody answering; with no daemon running it starts the unit directly; launchd follows the same release-then-load order; uninstall removes the units, the program copy and the channel socket file; on a native host the advisory does not offer the answerer step (the in-daemon answerer stays the interim until the native channel-client task lands), so the native session-start advisory reaches its quiet state once the resolver step is done; `answerer_source` resolves the `min-answerer` binary and never falls back to a bare `minvmd`; the hook probe reports whether the answerer is manager-held or session-held and compares the installed copy's channel protocol version with the daemon's, re-surfacing the advisory on a mismatch so an upgrade re-runs the step
test: cargo nextest run -p minimal advisory_installs_manager_held_answerer
test: cargo nextest run -p minimal hook_probe_resurfaces_advisory_on_protocol_mismatch
test: cargo nextest run -p minimal advisory_releases_the_port_before_starting_the_unit
test: cargo nextest run -p minimal native_advisory_omits_the_answerer_step
test: cargo nextest run -p minimal answerer_source_never_names_an_absent_minvmd
- docs:
- docs/specs/18-spec-box-networking/18-spec-box-networking.md: NET-122 gains the sub-requirement "WHERE the host is hooked THE SYSTEM SHALL install, by the privileged step, the box-zone answerer as a host service whose listener and channel sockets the service manager holds, running as the operator, from a root-owned non-user-writable program", and NET-138's interim sentence points at it and says the interim channel is per state dir, so two state dirs on a host without the service collide until the service is installed
- e2e:
- scripts/session-e2e.sh: adds the case answerer_survives_session_stop to the standalone VM-case list, run where a lane can install the unit (the KVM lane as root with the systemd variant; the macOS lane only if it can install a LaunchDaemon, else it fails rather than self-skips so the pin is never silent): node A starts and hosts the interim; running the advisory command from A's CLI hands the port to the service and A becomes a channel client, its name still answering; node B then starts under a second state dir (the `--minimal-dir` two-daemon pattern) and publishes over the one machine-global channel; stopping one node's session leaves the sibling's name answering; a service restart under two connected nodes answers both names again without a session action; a session started afterwards publishes instead of binding; every case that runs the advisory as root tears down the units, the program copy and the channel socket through the EXIT trap, so the shared runner is left as found; the native-lane resolution cases stay green with the step omitted on native hosts
test: ./scripts/session-e2e.sh answerer_survives_session_stop
test: ./scripts/session-e2e.sh native_resolution_without_proxy_env
test: ./scripts/session-e2e.sh box_name_resolves_natively_without_proxy
produces:
- crates/minvmd/src/bin/min-answerer.rs
consumes:
- crates/minvmd/src/net/answerer.rs
- crates/minvmd/src/control.rs
- crates/minimal/src/resolver.rs
proof: On a VM-backed host, two nodes under separate state dirs answer their names from the one machine-global answerer the service manager holds; the advisory's privileged step installs it as the operator from a root-owned copy and hands the hook port over from the running daemons within a bounded window, re-binding the interim if the unit fails; a stopped session leaves the answerer answering; a node that finds the channel publishes its rows instead of binding and says so at start; a channel peer with another uid is refused; a second node's claim on a held name is refused and reported; an address outside the answer rule is refused; both names answer again after a service restart with no session action, and the negative TTL served across the restart is no longer than the positive one; a native host is not offered the step and its session-start advisory goes quiet on the interim. Known residual under the single-operator premise: another local process can take the hook port in the gap between the release and the unit binding; the command's failure path names it as a collision and the daemons re-bind.
depends on:
- blocked by #1710
Activity
minimal-foundry commentedon Oct 4, 2026
Done. #1924 merged.
branch
agent/1803-hold-the-box-zone-answerer-as-a-host-service-ins· epic #1 834E 437 · PR #1924Updated 2026-10-28T06:17:32.453Z.