From ad6b39b425bc0c554a3d584656dd75ea5e9c40ba Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 22 Jun 2026 11:59:05 +0200 Subject: [PATCH] remove vestigial agent-ports.json tcp web-port map --- docs/gateway.md | 39 ++----- frontend/packages/dashboard/src/tabs.js | 4 +- hive-c0re/src/agent_ports.rs | 131 ------------------------ hive-c0re/src/agent_sockets.rs | 25 ++--- hive-c0re/src/lib.rs | 1 - hive-c0re/src/meta.rs | 27 ++--- hive-c0re/src/paths.rs | 11 +- 7 files changed, 31 insertions(+), 207 deletions(-) delete mode 100644 hive-c0re/src/agent_ports.rs diff --git a/docs/gateway.md b/docs/gateway.md index 8bee3097..db9f4921 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -120,36 +120,19 @@ idempotent — skips the rename when content is unchanged. Failed reloads are retried automatically on subsequent poll ticks via `gateway_nginx::reload_if_pending`. -## Agent port map (`agent-ports.json`) +## TCP loopback fallback -`/var/lib/hyperhive/run/agent-ports.json` is a flat JSON object keyed by -logical agent name → TCP web port: +While an agent's unix-socket marker is absent, the gateway routes its +`/agent//` traffic to a TCP loopback upstream in `agents.conf`. The +port is derived on the fly from `lifecycle::agent_web_port(name)` — a pure +FNV-1a hash of the name, reproducible from the name alone, no name +special-cased (so no on-disk port map is needed). Once the agent binds its +unix socket — every agent does, via `HIVE_WEB_SOCKET` — the gateway +switches to the socket upstream from `agent-sockets.json`. The root agent's +UI is routed at `/agent/root/`. -```json -{ - "iris": 8178, - "atlas": 8304, - "argus": 8267, - "damocles": 8549 -} -``` - -Written alongside `agents.conf` on every topology change. Ports come from -`lifecycle::agent_web_port(name)` — a pure FNV-1a hash of the name, -reproducible from the name alone. Every agent gets an entry — no name is -special-cased. Note this TCP map is now a fallback the gateway no longer -reaches: all agents bind a unix-socket web UI (`HIVE_WEB_SOCKET`) and the -gateway routes via `agent-sockets.json`, picking the socket upstream -whenever one exists. The root agent's UI is routed at `/agent/root/`. - -The file doubles as a human-readable audit artifact — `cat agent-ports.json` -shows every registered sub-agent and its deterministic port assignment. TCP -loopback upstreams in `agents.conf` reference these ports for agents that -haven't opted into unix-socket mode yet. - -Both `agent-ports.json` and `agents.conf` use atomic `.tmp` + -`rename()` writes so a crashing c0re process never leaves a partial or -unparseable file behind. +`agents.conf` uses atomic `.tmp` + `rename()` writes so a crashing +c0re process never leaves a partial or unparseable file behind. ## Dashboard link shape (gateway vs direct) diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 16db5f26..3cb099e5 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -846,8 +846,8 @@ window.marked = marked; // When hive-gateway is in front of the dashboard, build same-origin // `/agent//` URLs instead of the direct `http://:/` // TCP fallback — the gateway proxies the prefix to the per-agent - // harness (TCP via `agent-ports.json` or unix-domain via - // `agent-sockets.json`). See + // harness (unix-domain via `agent-sockets.json`, or a computed TCP + // loopback port while the socket marker is absent). See // `docs/web-ui.md::Container row` + `docs/gateway.md::Vhost map`. const gatewayLinks = !!(s && s.gateway_enabled); // Forge public URL: prefer state.forge_public_url (set by the NixOS diff --git a/hive-c0re/src/agent_ports.rs b/hive-c0re/src/agent_ports.rs deleted file mode 100644 index 3d73ae50..00000000 --- a/hive-c0re/src/agent_ports.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! `/var/lib/hyperhive/run/agent-ports.json` writer — flat map of -//! agent name → TCP web port. Written alongside `agents.conf` on -//! every topology change. JSON shape, port derivation (FNV-1a hash), -//! and atomicity: `docs/gateway.md::Agent port map`. - -use std::collections::BTreeMap; -use std::path::PathBuf; - -use anyhow::{Context, Result}; - -use crate::lifecycle; - -#[must_use] -pub fn host_ports_path() -> PathBuf { - crate::paths::agent_ports_file() -} - -/// Compute the agent-port map for the given logical agent names. Every -/// agent gets an entry — no name is special-cased. (All agents now bind -/// a unix-socket web UI via `HIVE_WEB_SOCKET`, so this TCP map is a -/// fallback the gateway no longer reaches; tracked for removal.) -/// -/// `BTreeMap` keeps the JSON output sorted by key so a re-emit -/// without churn produces byte-identical output (helpful when the -/// operator inspects the file by hand or diffs deploys). -#[must_use] -pub fn build_map(names: &[String]) -> BTreeMap { - names - .iter() - .map(|n| (n.clone(), lifecycle::agent_web_port(n))) - .collect() -} - -/// Render the map as pretty-printed JSON. Pretty so a human peek at -/// `cat /var/lib/hyperhive/run/agent-ports.json` shows one row per agent -/// — keeps the file readable without a separate jq step. -fn render(map: &BTreeMap) -> String { - // BTreeMap → serde_json::to_string_pretty preserves key order, - // so the output is deterministic across calls with the same - // agent set. - serde_json::to_string_pretty(map).expect("BTreeMap is always serialisable") -} - -/// Atomically write the JSON for `names` to -/// `/var/lib/hyperhive/run/agent-ports.json`. Writes via a sibling -/// `.tmp` + rename so a crashing process never leaves a -/// partial file behind that the gateway worker would fail to parse. -/// -/// Idempotent — if the rendered content matches what's already on -/// disk, the write + rename are skipped so the file's mtime stays -/// stable and inotify watchers in nginx (or any future watchers) -/// don't fire spurious reload events. -pub fn write(names: &[String]) -> Result<()> { - let map = build_map(names); - let body = render(&map); - let path = host_ports_path(); - if std::fs::read_to_string(&path).ok().as_deref() == Some(&body) { - return Ok(()); - } - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; - } - let tmp = path.with_extension("json.tmp"); - std::fs::write(&tmp, &body).with_context(|| format!("write {}", tmp.display()))?; - std::fs::rename(&tmp, &path).with_context(|| { - format!( - "rename {} -> {} (atomic publish)", - tmp.display(), - path.display() - ) - })?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn build_map_includes_every_agent() { - // No name is special-cased: the bootstrap/root container gets a - // port entry like any other agent. Use the bootstrap name in the - // input so this guards against re-introducing an exclusion. - let names: Vec = ["iris", lifecycle::MANAGER_NAME, "argus"] - .iter() - .map(|s| (*s).to_owned()) - .collect(); - let map = build_map(&names); - assert!(map.contains_key(lifecycle::MANAGER_NAME)); - assert!(map.contains_key("iris")); - assert!(map.contains_key("argus")); - } - - #[test] - fn build_map_uses_agent_web_port() { - let names = vec!["iris".to_owned()]; - let map = build_map(&names); - assert_eq!(map.get("iris"), Some(&lifecycle::agent_web_port("iris"))); - } - - #[test] - fn build_map_handles_empty_input() { - let map = build_map(&[]); - assert!(map.is_empty()); - } - - #[test] - fn build_map_dedupes_via_btreemap_key_collision() { - // Duplicate inputs collapse via the map; the last value wins. - // No callsite passes dups today, but guarding the invariant - // here means a future bug doesn't surface as a corrupt JSON - // doc (two `"iris":` keys). - let names = vec!["iris".to_owned(), "iris".to_owned()]; - let map = build_map(&names); - assert_eq!(map.len(), 1); - } - - #[test] - fn render_is_pretty_and_sorted() { - let mut map = BTreeMap::new(); - map.insert("zeta".to_owned(), 9000u16); - map.insert("alpha".to_owned(), 8000u16); - let body = render(&map); - // Pretty-print = newlines between keys + indentation. - assert!(body.contains('\n')); - // BTreeMap sorts → alpha before zeta in output. - let alpha_pos = body.find("alpha").expect("alpha in output"); - let zeta_pos = body.find("zeta").expect("zeta in output"); - assert!(alpha_pos < zeta_pos, "sorted order broken:\n{body}"); - } -} diff --git a/hive-c0re/src/agent_sockets.rs b/hive-c0re/src/agent_sockets.rs index df4192fb..6e0dbfda 100644 --- a/hive-c0re/src/agent_sockets.rs +++ b/hive-c0re/src/agent_sockets.rs @@ -1,12 +1,12 @@ -//! `/var/lib/hyperhive/run/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. Includes -//! manager and sub-agents so the gateway can route -//! `/agent//` for all containers with a bound unix socket. +//! `/var/lib/hyperhive/run/agent-sockets.json` writer. Atomic +//! `.tmp` + `rename()` write so 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`, -//! 10s poll loop: `docs/gateway.md::Per-agent unix-socket upstream`. +//! marker gate, gateway UDS upstream, 10s poll loop: +//! `docs/gateway.md::Per-agent unix-socket upstream`. use std::collections::BTreeMap; use std::path::{Path, PathBuf}; @@ -74,8 +74,7 @@ pub fn socket_path_for(name: &str) -> PathBuf { /// new marker name). /// /// `BTreeMap` keeps the JSON output sorted by key so a re-emit -/// without churn produces byte-identical output — same idempotency -/// shape `agent_ports::write` relies on. +/// without churn produces byte-identical output for idempotent writes. #[must_use] pub fn build_map(names: &[String]) -> BTreeMap { build_map_with(names, |name| { @@ -110,8 +109,7 @@ pub fn ready_marker_for(name: &str) -> PathBuf { /// Render the map as pretty-printed JSON. Pretty so a human peek at /// `cat /var/lib/hyperhive/run/agent-sockets.json` shows one row per agent -/// — keeps the file readable without a separate jq step (mirrors -/// `agent_ports::render`). +/// — keeps the file readable without a separate jq step. fn render(map: &BTreeMap) -> String { // Serialize as strings (PathBuf → JSON string via the Display // impl). BTreeMap → serde_json::to_string_pretty preserves key @@ -133,8 +131,7 @@ fn render(map: &BTreeMap) -> String { /// Idempotent — if the rendered content matches what's already on /// disk, the write + rename are skipped so the file's mtime stays /// stable and inotify watchers in the gateway (or any future -/// watchers) don't fire spurious reload events. Mirrors the -/// `agent_ports::write` shape — keep them in lockstep. +/// watchers) don't fire spurious reload events. pub fn write(names: &[String]) -> Result<()> { let map = build_map(names); let body = render(&map); @@ -268,7 +265,7 @@ mod tests { // Duplicate inputs collapse via the map; no callsite passes // dups today, but guarding the invariant here means a future // bug doesn't surface as a corrupt JSON doc (two `"iris":` - // keys). Mirrors agent_ports test. + // keys). let names = vec!["iris".to_owned(), "iris".to_owned()]; let map = build_map_with(&names, |_| true); assert_eq!(map.len(), 1); diff --git a/hive-c0re/src/lib.rs b/hive-c0re/src/lib.rs index 4d624832..531d50f5 100644 --- a/hive-c0re/src/lib.rs +++ b/hive-c0re/src/lib.rs @@ -13,7 +13,6 @@ //! surface beyond "this is where the modules live". pub mod actions; -pub mod agent_ports; pub mod agent_server; pub mod agent_sockets; pub mod approvals; diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 0e7d7ade..1479b842 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -119,27 +119,12 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> { crate::topology::reconcile(&agent_names, &pending) .with_context(|| format!("reconcile {}", crate::topology::topology_path().display()))?; - // Refresh /var/lib/hyperhive/run/agent-ports.json so the hive-gateway - // nginx sees the new agent set. The file is the single source of - // truth for which agents the gateway proxies to, since the - // gateway container lives in system config and can't be rebuilt - // from meta-flake events. Atomic write (tmp + rename) so a - // partial write never trips the gateway's read. - if let Err(e) = crate::agent_ports::write(&agent_names) { - // Best-effort: a failed write doesn't block the meta-flake - // regen + container ops that follow. The gateway falls back - // to whatever map is currently on disk (or an empty map on - // first boot, meaning no per-agent routing yet). - tracing::warn!(error = ?e, "agent_ports::write failed (non-fatal)"); - } - - // Refresh /var/lib/hyperhive/run/agent-sockets.json — sibling to the - // ports map, drives the gateway's unix-socket upstreams once - // agents opt in to `HIVE_WEB_SOCKET`. Coexists with the TCP-port - // map during the transition: the gateway picks the socket - // upstream when one exists, falls back to the TCP port otherwise. - // Same best-effort + non-fatal shape. See - // `docs/gateway.md::Per-agent unix-socket upstream`. + // Refresh /var/lib/hyperhive/run/agent-sockets.json — drives the + // gateway's unix-socket upstreams. Every agent binds a unix socket + // (`HIVE_WEB_SOCKET`); the gateway routes via this map, falling back + // to a computed TCP loopback port (`lifecycle::agent_web_port`) only + // while an agent's socket marker is absent. Best-effort + non-fatal. + // See `docs/gateway.md::Per-agent unix-socket upstream`. if let Err(e) = crate::agent_sockets::write(&agent_names) { tracing::warn!(error = ?e, "agent_sockets::write failed (non-fatal)"); } diff --git a/hive-c0re/src/paths.rs b/hive-c0re/src/paths.rs index df63d19c..70be660a 100644 --- a/hive-c0re/src/paths.rs +++ b/hive-c0re/src/paths.rs @@ -1,7 +1,7 @@ //! Central host-side state paths under `/var/lib/hyperhive`. //! //! Historically these were flat string literals scattered across many -//! modules (`broker.sqlite`, `matrix-admin-token`, `agent-ports.json`, +//! modules (`broker.sqlite`, `matrix-admin-token`, `agent-sockets.json`, //! …) directly under the state root. This module groups the **strictly //! host-side** ones (read/written by hive-c0re alone, no nix-module or //! container coupling) into subdirs: `db/`, `forge/`, `matrix/`, `run/`. @@ -111,12 +111,6 @@ pub fn run_dir() -> PathBuf { state_root().join("run") } -/// `run/agent-ports.json` — name→port map the gateway routing reads. -#[must_use] -pub fn agent_ports_file() -> PathBuf { - run_dir().join("agent-ports.json") -} - /// `run/agent-sockets.json` — name→socket-path map for UDS upstreams. #[must_use] pub fn agent_sockets_file() -> PathBuf { @@ -150,9 +144,6 @@ pub fn relocate_legacy_state() { for (old_rel, new) in &moves { move_if_legacy(&root.join(old_rel), new); } - // agent-ports.json handled here too (kept out of the array only to - // keep the fixed-size literal tidy). - move_if_legacy(&root.join("agent-ports.json"), &agent_ports_file()); // `forge-email-aligned-` markers: glob the flat root. if let Ok(rd) = std::fs::read_dir(&root) { for ent in rd.flatten() {