diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 73032c4e..9303a79d 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -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//` 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//`, 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 = original