hive-c0re: per-agent subdir socket layout (#809 design fix)
was: /run/hive-agent/<name>.sock (flat single-file bind-mount).
issue: file bind-mounts don't survive the harness's 'unlink stale
socket then bind(2) a new one' cycle. The unlink drops the bind
inside the container; the rebind happens in private container
namespace; host never sees the new inode → gateway can't connect.
now: /run/hive-agent/<name>/web.sock (per-agent SUBDIR + fixed
filename). Lifecycle bind-mounts the parent dir per agent (step 2b)
so both sides see the same dir inode; the socket appears on the
host the moment the harness binds it.
new helpers:
- AGENT_SOCKET_DIR const (parent, gateway binds this whole tree)
- SOCKET_FILENAME const ("web.sock")
- agent_dir_for(name) (per-agent subdir, lifecycle bind-mounts this)
- socket_path_for(name) (= agent_dir_for(name).join(SOCKET_FILENAME))
per-agent dir isolation also satisfies mara on #800 directly:
agent's container only sees its own subdir + socket, never siblings'.
8 tests now (added agent_dir_for_is_socket_parent invariant).
This commit is contained in:
parent
c7a8cec2b5
commit
91f5588134
1 changed files with 58 additions and 15 deletions
|
|
@ -37,6 +37,22 @@
|
|||
//!
|
||||
//! Atomicity: same `<path>.tmp` + `rename()` shape as `agent_ports.rs`
|
||||
//! so the gateway's nginx worker never reads a partial file.
|
||||
//!
|
||||
//! ## Per-agent subdir layout
|
||||
//!
|
||||
//! `<sockets-root>/<name>/web.sock`, NOT `<sockets-root>/<name>.sock`.
|
||||
//! Each agent's container bind-mounts the per-agent SUBDIR
|
||||
//! (`/run/hive-agent/<name>/`), and the harness binds the socket
|
||||
//! inside it. File-level bind-mounts don't survive the harness's
|
||||
//! "unlink stale socket then `bind(2)` a new one" cycle — the unlink
|
||||
//! drops the bind, the rebind happens in private container
|
||||
//! namespace, host never sees the new inode. Bind-mounting the
|
||||
//! parent dir keeps both sides looking at the same dir inode so the
|
||||
//! socket appears on the host the moment the harness binds it.
|
||||
//!
|
||||
//! Per-agent dir isolation (one dir per agent rather than a shared
|
||||
//! `/run/hive-agent/` bind) satisfies mara on #800: an agent's
|
||||
//! container only sees its own dir + socket, never siblings'.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -47,25 +63,38 @@ use crate::lifecycle::MANAGER_NAME;
|
|||
|
||||
const HOST_SOCKETS_PATH: &str = "/var/lib/hyperhive/agent-sockets.json";
|
||||
|
||||
/// Host-side directory that holds per-agent unix sockets. Each
|
||||
/// agent's container bind-mounts only its own `<name>.sock` from
|
||||
/// this dir, scoping access per mara's #800 directive ("agents can
|
||||
/// only access their own sockets"). The gateway container gets the
|
||||
/// whole dir mounted read-only so it can proxy to every agent.
|
||||
/// Host-side parent directory holding per-agent socket subdirs. The
|
||||
/// gateway container bind-mounts this whole tree (read-only) so it
|
||||
/// can `proxy_pass` to any agent. Each agent's container bind-mounts
|
||||
/// only its own `<name>/` subdir, scoping access per mara's #800
|
||||
/// directive ("agents can only access their own sockets").
|
||||
pub const AGENT_SOCKET_DIR: &str = "/run/hive-agent";
|
||||
|
||||
/// Socket filename inside each per-agent subdir. Fixed so the path
|
||||
/// derives entirely from `(AGENT_SOCKET_DIR, name)` — no second
|
||||
/// degree of freedom for callers to get wrong.
|
||||
pub const SOCKET_FILENAME: &str = "web.sock";
|
||||
|
||||
#[must_use]
|
||||
pub fn host_sockets_path() -> PathBuf {
|
||||
PathBuf::from(HOST_SOCKETS_PATH)
|
||||
}
|
||||
|
||||
/// Per-agent socket subdir on the host. Lifecycle pre-creates this
|
||||
/// before container start so the bind-mount source exists; the
|
||||
/// harness binds the socket inside it as `web.sock`.
|
||||
#[must_use]
|
||||
pub fn agent_dir_for(name: &str) -> PathBuf {
|
||||
Path::new(AGENT_SOCKET_DIR).join(name)
|
||||
}
|
||||
|
||||
/// Compute the deterministic socket path for an agent. Pure function
|
||||
/// of the agent name so the value matches whatever
|
||||
/// [`agent_sockets::write`] writes for that agent, and whatever the
|
||||
/// harness binds via `HIVE_WEB_SOCKET` post-#784 phase 1.
|
||||
#[must_use]
|
||||
pub fn socket_path_for(name: &str) -> PathBuf {
|
||||
Path::new(AGENT_SOCKET_DIR).join(format!("{name}.sock"))
|
||||
agent_dir_for(name).join(SOCKET_FILENAME)
|
||||
}
|
||||
|
||||
/// Compute the agent-socket map for the given logical agent names.
|
||||
|
|
@ -142,12 +171,26 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn socket_path_for_uses_agent_dir_constant() {
|
||||
// The path is derived from `AGENT_SOCKET_DIR` — pin both ends
|
||||
// so a future move (e.g. to `/run/hyperhive/sockets/`)
|
||||
// requires updating both the constant and the consumers.
|
||||
fn socket_path_for_uses_subdir_layout() {
|
||||
// Per-agent subdir + fixed socket filename — see module-level
|
||||
// "Per-agent subdir layout" for why this isn't a flat
|
||||
// `<name>.sock`. Pin both ends so a future move (e.g. to
|
||||
// `/run/hyperhive/sockets/`) requires updating both the
|
||||
// constant and the consumers.
|
||||
let p = socket_path_for("iris");
|
||||
assert_eq!(p, Path::new("/run/hive-agent/iris.sock"));
|
||||
assert_eq!(p, Path::new("/run/hive-agent/iris/web.sock"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_dir_for_is_socket_parent() {
|
||||
// `agent_dir_for` is what lifecycle bind-mounts per agent;
|
||||
// `socket_path_for` lives inside it. Keep them in lockstep so
|
||||
// a divergence (e.g. typo in one constant) surfaces here
|
||||
// rather than as a confusing nspawn bind-source-not-found at
|
||||
// container start.
|
||||
let dir = agent_dir_for("iris");
|
||||
let sock = socket_path_for("iris");
|
||||
assert_eq!(sock.parent(), Some(dir.as_path()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -197,8 +240,8 @@ mod tests {
|
|||
#[test]
|
||||
fn render_is_pretty_and_sorted() {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("zeta".to_owned(), PathBuf::from("/run/hive-agent/zeta.sock"));
|
||||
map.insert("alpha".to_owned(), PathBuf::from("/run/hive-agent/alpha.sock"));
|
||||
map.insert("zeta".to_owned(), PathBuf::from("/run/hive-agent/zeta/web.sock"));
|
||||
map.insert("alpha".to_owned(), PathBuf::from("/run/hive-agent/alpha/web.sock"));
|
||||
let body = render(&map);
|
||||
// Pretty-print = newlines between keys + indentation.
|
||||
assert!(body.contains('\n'));
|
||||
|
|
@ -218,9 +261,9 @@ mod tests {
|
|||
// gateway-side reader can deserialise into String values
|
||||
// without nested struct logic.
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("iris".to_owned(), PathBuf::from("/run/hive-agent/iris.sock"));
|
||||
map.insert("iris".to_owned(), PathBuf::from("/run/hive-agent/iris/web.sock"));
|
||||
let body = render(&map);
|
||||
assert!(body.contains("\"iris\""));
|
||||
assert!(body.contains("\"/run/hive-agent/iris.sock\""));
|
||||
assert!(body.contains("\"/run/hive-agent/iris/web.sock\""));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue