hive-c0re: bind-mount /run/hive-agent/<name>/ per sub-agent (#784 phase 2 step 2b)

builds on step 2a (#809). lifecycle::set_nspawn_flags now adds a
--bind={socket_dir}:{socket_dir} flag per sub-agent so the harness's
HIVE_WEB_SOCKET bind (PR #800) lives in a dir both the agent
container and the host can see.

design (matches #809's a1a601d explanation):
- bind the SUBDIR, not the socket file. file bind-mounts drop on
  unlink; the harness's bind_unix unlinks any stale socket before
  binding, so a file bind would land the new socket in the agent's
  private namespace, invisible to the gateway. dir bind keeps both
  sides on the same dir inode.
- per-agent dir (one /run/hive-agent/<name>/ per agent, not a shared
  /run/hive-agent/ mount). The agent's container only sees its own
  subdir — never siblings' (mara on #800).
- manager skipped — the manager's UI serves at / via the c0re
  dashboard upstream, not via /agent/<name>/, so it never needs the
  per-agent socket dir.

mkdir source defensively before bind: nspawn refuses to start when
the bind source is missing, and /run/hive-agent/ doesn't exist on
fresh hosts.

remaining work in this phase:
- step 3 (atlas): gateway proxy_pass http://unix:/run/hive-agent/<name>/web.sock:/
- per-agent: flip HIVE_WEB_SOCKET in agent.nix to opt in (separate PRs)
- step 4 (later): drop TCP fallback once everyone's flipped
This commit is contained in:
damocles 2026-05-31 15:36:04 +02:00 committed by mara
commit 35a7ff03b7

View file

@ -1141,6 +1141,40 @@ fn set_nspawn_flags(
// empty RO dir instead of a container that won't boot.
std::fs::create_dir_all(&config_dir).with_context(|| format!("create {config_dir}"))?;
let _ = write!(binds, " --bind-ro={config_dir}:/agents/{agent_name}/config");
// Per-agent socket subdir (#784 phase 2 step 2b). Bind-mounts
// `/run/hive-agent/<name>/` into the container at the same
// path so the harness's `HIVE_WEB_SOCKET` bind has a stable
// location both sides can see. Sub-agents only — the
// manager's UI is served at `/` via the c0re dashboard
// upstream, not via `/agent/<name>/`, so it never needs the
// per-agent socket dir.
//
// Bind-mounting the SUBDIR (not the socket file) is mandatory:
// the harness's `bind_unix` helper unlinks any stale socket
// before calling `bind(2)`, and a file bind-mount drops its
// host-side anchor on unlink — the rebind would land in the
// container's private namespace, invisible to the gateway.
// Dir bind keeps the same dir inode visible on both sides, so
// the new `web.sock` shows up on the host the moment the
// harness binds it.
//
// Per-agent dir (rather than a shared `/run/hive-agent/`
// mount) means the agent's container only sees its own
// subdir — never siblings' (mara on #800: "agents can only
// access their own sockets").
//
// mkdir source defensively: nspawn refuses to start when the
// bind source is missing, and on a fresh host `/run/hive-agent/`
// doesn't exist yet.
let socket_dir = crate::agent_sockets::agent_dir_for(agent_name);
std::fs::create_dir_all(&socket_dir)
.with_context(|| format!("create {}", socket_dir.display()))?;
let _ = write!(
binds,
" --bind={socket_dir}:{socket_dir}",
socket_dir = socket_dir.display(),
);
}
let bind_flag = format!("EXTRA_NSPAWN_FLAGS=\"{binds}\"");
let mut lines: Vec<String> = original