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
|
//! `/var/lib/hyperhive/agent-sockets.json` writer. Sibling to
|
||||||
//! `agent_ports.rs`; same atomic `<path>.tmp` + `rename()` shape so
|
//! `agent_ports.rs`; same atomic `<path>.tmp` + `rename()` shape so
|
||||||
//! the gateway's nginx worker never reads a partial file. Manager
|
//! the gateway's nginx worker never reads a partial file. Includes
|
||||||
//! excluded from the map (manager UI routes via the dashboard
|
//! manager and sub-agents so the gateway can route
|
||||||
//! upstream, not per-agent `/agent/<name>/`).
|
//! `/agent/<name>/` for all containers with a bound unix socket.
|
||||||
//!
|
//!
|
||||||
//! Full mechanism — per-agent subdir bind-mount, `hyperhive-socket-bound`
|
//! Full mechanism — per-agent subdir bind-mount, `hyperhive-socket-bound`
|
||||||
//! marker gate, gateway UDS upstream, transition vs `agent-ports.json`,
|
//! 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.
|
/// Compute the agent-socket map for the given logical agent names.
|
||||||
/// Sub-agents only — manager is filtered out at the call boundary
|
/// Includes manager and sub-agents. Filters by `READY_MARKER`
|
||||||
/// for the same reason it's filtered from `agent_ports::build_map`
|
/// presence: only agents whose harness has actually bound the unix
|
||||||
/// (manager UI is routed via the c0re dashboard upstream, not via
|
/// socket appear in the map. Without this, the gateway would
|
||||||
/// `/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
|
|
||||||
/// `proxy_pass` to a non-existent socket for every sub-agent that
|
/// `proxy_pass` to a non-existent socket for every sub-agent that
|
||||||
/// hasn't yet flipped `hyperhive.web.useUnixSocket = true`.
|
/// hasn't yet flipped `hyperhive.web.useUnixSocket = true`.
|
||||||
///
|
///
|
||||||
|
|
@ -105,7 +100,6 @@ where
|
||||||
{
|
{
|
||||||
names
|
names
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|n| n.as_str() != MANAGER_NAME)
|
|
||||||
.filter(|n| is_ready(n))
|
.filter(|n| is_ready(n))
|
||||||
.map(|n| (n.clone(), socket_path_for(n)))
|
.map(|n| (n.clone(), socket_path_for(n)))
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -241,18 +235,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_map_filters_manager() {
|
fn build_map_includes_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.
|
|
||||||
let names: Vec<String> = ["iris", MANAGER_NAME, "argus"]
|
let names: Vec<String> = ["iris", MANAGER_NAME, "argus"]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| (*s).to_owned())
|
.map(|s| (*s).to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
let map = build_map_with(&names, |_| true);
|
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("iris"));
|
||||||
assert!(map.contains_key("argus"));
|
assert!(map.contains_key("argus"));
|
||||||
}
|
}
|
||||||
|
|
@ -343,3 +332,4 @@ mod tests {
|
||||||
assert!(body.contains("\"/run/hive-agent/iris/web.sock\""));
|
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}"))?;
|
std::fs::create_dir_all(&config_dir).with_context(|| format!("create {config_dir}"))?;
|
||||||
let _ = write!(binds, " --bind-ro={config_dir}:/agents/{agent_name}/config");
|
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 bind_flag = format!("EXTRA_NSPAWN_FLAGS=\"{binds}\"");
|
||||||
let mut lines: Vec<String> = original
|
let mut lines: Vec<String> = original
|
||||||
.lines()
|
.lines()
|
||||||
|
|
@ -1469,3 +1440,4 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,9 @@ in
|
||||||
4. eventually drop this option once every agent is on unix and
|
4. eventually drop this option once every agent is on unix and
|
||||||
the TCP fallback is removed from the harness.
|
the TCP fallback is removed from the harness.
|
||||||
|
|
||||||
Sub-agent-only by design: the manager's UI serves at `/` via
|
Sub-agents only: the manager always has `HIVE_WEB_SOCKET` set
|
||||||
the c0re dashboard upstream, not via `/agent/<name>/`, so this
|
unconditionally in the `isManager` env block, so this toggle
|
||||||
option has no effect when `hyperhive.role = "manager"` (the
|
has no effect when `hyperhive.role = "manager"`.
|
||||||
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).
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1325,6 +1322,9 @@ in
|
||||||
# HIVE_PORT = FNV-1a("hm1nd") % 900 + 8100.
|
# HIVE_PORT = FNV-1a("hm1nd") % 900 + 8100.
|
||||||
HIVE_PORT = "8875";
|
HIVE_PORT = "8875";
|
||||||
HIVE_LABEL = "hm1nd";
|
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 = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/${binary} serve";
|
ExecStart = "${pkgs.hyperhive}/bin/${binary} serve";
|
||||||
|
|
@ -1343,3 +1343,4 @@ in
|
||||||
system.stateVersion = "25.11";
|
system.stateVersion = "25.11";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue