Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb108f9dd8 | ||
|
|
2f25131403 | ||
|
|
e7a5718e83 | ||
|
|
7af29b3249 | ||
|
|
0ac05f0638 | ||
|
|
4435666c00 | ||
|
|
8b946a67c6 |
3 changed files with 41 additions and 78 deletions
|
|
@ -1,8 +1,8 @@
|
|||
//! `/var/lib/hyperhive/agent-sockets.json` writer. Sibling to
|
||||
//! `agent_ports.rs`; same atomic `<path>.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/<name>/`).
|
||||
//! the gateway's nginx worker never reads a partial file. Includes
|
||||
//! manager and sub-agents so the gateway can route
|
||||
//! `/agent/<name>/` 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/<name>/`).
|
||||
///
|
||||
/// 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<String> = ["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\""));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<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'. 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/<name>/` 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<String> = original
|
||||
.lines()
|
||||
|
|
@ -1469,3 +1440,4 @@ mod tests {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<name>/`, 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/<name>/ 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";
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue