diff --git a/hive-c0re/src/agent_sockets.rs b/hive-c0re/src/agent_sockets.rs index 4ea40ed1..9a715f36 100644 --- a/hive-c0re/src/agent_sockets.rs +++ b/hive-c0re/src/agent_sockets.rs @@ -1,8 +1,8 @@ //! `/var/lib/hyperhive/agent-sockets.json` writer. Sibling to //! `agent_ports.rs`; same atomic `.tmp` + `rename()` shape so -//! the gateway's nginx worker never reads a partial file. Manager -//! excluded from the map (manager UI routes via the dashboard -//! upstream, not per-agent `/agent//`). +//! the gateway's nginx worker never reads a partial file. Includes +//! manager and sub-agents so the gateway can route +//! `/agent//` for all containers with a bound unix socket. //! //! Full mechanism — per-agent subdir bind-mount, `hyperhive-socket-bound` //! marker gate, gateway UDS upstream, transition vs `agent-ports.json`, @@ -67,14 +67,9 @@ pub fn socket_path_for(name: &str) -> PathBuf { } /// Compute the agent-socket map for the given logical agent names. -/// Sub-agents only — manager is filtered out at the call boundary -/// for the same reason it's filtered from `agent_ports::build_map` -/// (manager UI is routed via the c0re dashboard upstream, not via -/// `/agent//`). -/// -/// Also filters by `READY_MARKER` presence: only agents whose -/// harness has actually bound the unix socket (and dropped the -/// marker) appear in the map. Without this, the gateway would +/// Includes manager and sub-agents. Filters by `READY_MARKER` +/// presence: only agents whose harness has actually bound the unix +/// socket appear in the map. Without this, the gateway would /// `proxy_pass` to a non-existent socket for every sub-agent that /// hasn't yet flipped `hyperhive.web.useUnixSocket = true`. /// @@ -105,7 +100,6 @@ where { names .iter() - .filter(|n| n.as_str() != MANAGER_NAME) .filter(|n| is_ready(n)) .map(|n| (n.clone(), socket_path_for(n))) .collect() @@ -241,18 +235,13 @@ mod tests { } #[test] - fn build_map_filters_manager() { - // Use `MANAGER_NAME` in the input so the assert actually - // exercises the filter path — a literal `"hm1nd"` would pass - // trivially if the constant ever changed and the filter - // silently became a no-op. All-ready predicate bypasses the - // marker check so we exercise the manager filter in isolation. + fn build_map_includes_manager() { let names: Vec = ["iris", MANAGER_NAME, "argus"] .iter() .map(|s| (*s).to_owned()) .collect(); let map = build_map_with(&names, |_| true); - assert!(!map.contains_key(MANAGER_NAME)); + assert!(map.contains_key(MANAGER_NAME)); assert!(map.contains_key("iris")); assert!(map.contains_key("argus")); } @@ -343,3 +332,4 @@ mod tests { assert!(body.contains("\"/run/hive-agent/iris/web.sock\"")); } } + diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index afb66009..69178473 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -1174,60 +1174,31 @@ fn set_nspawn_flags( 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. 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'. See `docs/gateway.md::Per-agent - // unix-socket upstream`. - // - // 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()))?; - // chown to the in-container agent user so its harness can - // `bind(2)` web.sock here. `create_dir_all` lands the dir at - // 0755 root:root and the harness runs as the non-root agent - // user; without this chown the bind fails with EACCES, the - // gateway's agent-sockets.json stays empty, and the agent - // looks unreachable. uid resolution can return None on the - // very first spawn (container's /etc/passwd not yet rendered) - // — fall back to a permissive 0777 in that window so the - // first harness boot still binds. nspawn shares uids with the - // host (no PrivateUsers), so the in-container uid is the same - // uid we chown to here. - if let Some((uid, gid)) = agent_uid_gid(agent_name) { - std::os::unix::fs::chown(&socket_dir, Some(uid), Some(gid)) - .with_context(|| format!("chown {} to {uid}:{gid}", socket_dir.display()))?; - } else { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&socket_dir, std::fs::Permissions::from_mode(0o777)) - .with_context(|| { - format!("chmod 0777 {} (uid lookup failed)", socket_dir.display()) - })?; - } - let _ = write!( - binds, - " --bind={socket_dir}:{socket_dir}", - socket_dir = socket_dir.display(), - ); } + // Web-socket subdir: bind-mount `/run/hive-agent//` into the + // container so the harness can bind `web.sock` there and the host-side + // gateway sees it. Subdir bind (not socket file) keeps the inode + // visible after the harness unlinks a stale socket on rebind. + // Applies to manager and sub-agents alike. + 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()))?; + // Chown to the agent user so the non-root harness can bind(2) here. + // Falls back to 0777 on first spawn when uid lookup returns None + // (container /etc/passwd not yet rendered). + if let Some((uid, gid)) = agent_uid_gid(agent_name) { + std::os::unix::fs::chown(&socket_dir, Some(uid), Some(gid)) + .with_context(|| format!("chown {} to {uid}:{gid}", socket_dir.display()))?; + } else { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&socket_dir, std::fs::Permissions::from_mode(0o777)) + .with_context(|| format!("chmod 0777 {}", 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 .lines() @@ -1469,3 +1440,4 @@ mod tests { ); } } + diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 9d098608..53ffc9f1 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -95,12 +95,9 @@ in 4. eventually drop this option once every agent is on unix and the TCP fallback is removed from the harness. - Sub-agent-only by design: the manager's UI serves at `/` via - the c0re dashboard upstream, not via `/agent//`, so this - option has no effect when `hyperhive.role = "manager"` (the - env var is set unconditionally for clarity, but the manager's - web UI doesn't route through the gateway's per-agent unix - upstream — its bind socket would just sit unused). + Sub-agents only: the manager always has `HIVE_WEB_SOCKET` set + unconditionally in the `isManager` env block, so this toggle + has no effect when `hyperhive.role = "manager"`. ''; }; @@ -1325,6 +1322,9 @@ in # HIVE_PORT = FNV-1a("hm1nd") % 900 + 8100. HIVE_PORT = "8875"; HIVE_LABEL = "hm1nd"; + # Manager always uses a unix socket so the gateway can route + # /agent// to it the same way it routes sub-agents. + HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock"; }; serviceConfig = { ExecStart = "${pkgs.hyperhive}/bin/${binary} serve"; @@ -1343,3 +1343,4 @@ in system.stateVersion = "25.11"; }; } +