hive-c0re: extract agent-socket-plumbing prose (#715 batch 5)
This commit is contained in:
parent
f8e0061f3f
commit
d2f1a9d291
3 changed files with 44 additions and 98 deletions
|
|
@ -1,58 +1,12 @@
|
|||
//! `/var/lib/hyperhive/agent-sockets.json` writer (#784 phase 2,
|
||||
//! prerequisite to #14 container netns isolation).
|
||||
//! `/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>/`).
|
||||
//!
|
||||
//! Sibling to `agent_ports.rs`. The gateway needs to know which unix
|
||||
//! socket to `proxy_pass` to per agent once the per-agent web UI
|
||||
//! flips off TCP and on to `UnixListener::bind` (#784 phase 1
|
||||
//! landed via PR #800). This file is the source of truth for
|
||||
//! "which agents exist + where to reach their web UI over a domain
|
||||
//! socket" from the gateway's POV — read at request-handling time,
|
||||
//! not at gateway build time, so a `nixos-container update` of the
|
||||
//! gateway isn't needed every time an agent spawns / moves /
|
||||
//! destroys.
|
||||
//!
|
||||
//! Shape (flat object keyed by logical agent name → socket path):
|
||||
//!
|
||||
//! ```json
|
||||
//! {
|
||||
//! "iris": "/run/hive-agent/iris.sock",
|
||||
//! "atlas": "/run/hive-agent/atlas.sock",
|
||||
//! "argus": "/run/hive-agent/argus.sock",
|
||||
//! "damocles": "/run/hive-agent/damocles.sock"
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Socket paths are deterministic from the agent name —
|
||||
//! [`socket_path_for`] computes them, so a name alone resolves to a
|
||||
//! reproducible path. Manager is intentionally excluded from the map
|
||||
//! (same reasoning as `agent_ports.rs`: the gateway routes the
|
||||
//! manager's UI at `/` straight to the dashboard upstream, not via
|
||||
//! per-agent `/agent/<name>/`).
|
||||
//!
|
||||
//! Coexists with `agent-ports.json` during the #784 phase-3
|
||||
//! transition: agents that haven't opted in to `HIVE_WEB_SOCKET` yet
|
||||
//! still appear in both files; the gateway picks the socket upstream
|
||||
//! when one exists, falls back to the TCP port otherwise. Step 4
|
||||
//! drops the TCP path entirely once every agent's web UI has flipped.
|
||||
//!
|
||||
//! 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'.
|
||||
//! Full mechanism — per-agent subdir bind-mount, `.bound` marker
|
||||
//! gate, gateway UDS upstream, transition vs `agent-ports.json`,
|
||||
//! 10s poll loop: `docs/gateway.md::Per-agent unix-socket upstream`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -66,8 +20,8 @@ const HOST_SOCKETS_PATH: &str = "/var/lib/hyperhive/agent-sockets.json";
|
|||
/// 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").
|
||||
/// only its own `<name>/` subdir — 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
|
||||
|
|
@ -79,10 +33,9 @@ pub const SOCKET_FILENAME: &str = "web.sock";
|
|||
/// successful `bind_unix`. Presence = "this agent has opted in to
|
||||
/// `hyperhive.web.useUnixSocket = true` and its harness has bound
|
||||
/// the socket"; absence = "the harness is still on TCP, don't
|
||||
/// publish the unix upstream for this agent yet". Atlas's gate on
|
||||
/// PR #813 — without it, the gateway would `proxy_pass` to a
|
||||
/// non-existent socket for every sub-agent that hasn't flipped the
|
||||
/// option yet.
|
||||
/// publish the unix upstream for this agent yet". Without this gate
|
||||
/// the gateway would `proxy_pass` to a non-existent socket for every
|
||||
/// sub-agent that hasn't flipped the option yet.
|
||||
pub const READY_MARKER: &str = ".bound";
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -101,7 +54,7 @@ pub fn agent_dir_for(name: &str) -> PathBuf {
|
|||
/// 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.
|
||||
/// harness binds via `HIVE_WEB_SOCKET`.
|
||||
#[must_use]
|
||||
pub fn socket_path_for(name: &str) -> PathBuf {
|
||||
agent_dir_for(name).join(SOCKET_FILENAME)
|
||||
|
|
@ -115,10 +68,9 @@ pub fn socket_path_for(name: &str) -> PathBuf {
|
|||
///
|
||||
/// Also filters by `READY_MARKER` presence: only agents whose
|
||||
/// harness has actually bound the unix socket (and dropped the
|
||||
/// marker) appear in the map. Atlas's gate on #813 — 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`.
|
||||
/// marker) 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`.
|
||||
///
|
||||
/// `BTreeMap` keeps the JSON output sorted by key so a re-emit
|
||||
/// without churn produces byte-identical output — same idempotency
|
||||
|
|
@ -270,8 +222,7 @@ mod tests {
|
|||
// 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 (same pattern as #748 fix on
|
||||
// agent_ports::build_map). All-ready predicate bypasses the
|
||||
// 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"]
|
||||
.iter()
|
||||
|
|
@ -320,9 +271,8 @@ mod tests {
|
|||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect();
|
||||
// Pretend only `atlas` has flipped + bound (mara on PR #813:
|
||||
// "agents can only access their own sockets" — the gate
|
||||
// makes sure only opted-in agents get a UDS upstream).
|
||||
// Pretend only `atlas` has flipped + bound — the gate makes
|
||||
// sure only opted-in agents get a UDS upstream.
|
||||
let map = build_map_with(&names, |name| name == "atlas");
|
||||
assert!(map.contains_key("atlas"));
|
||||
assert!(!map.contains_key("iris"));
|
||||
|
|
|
|||
Loading…
Reference in a new issue