From 746c522f4eaec7a1912755e7b1dc94d37a001d29 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 16 Oct 2026 18:30:31 +0200 Subject: [PATCH] drop the manager socket, the no-op migrate branch and other ruth special cases start_manager bound the same path register_agent binds. The migrate branch, hive-priv-sock::MANAGER_NAME and MANAGER_CONTAINER were no-ops or dead. The send allow-list docs promised a manager exemption the code never had. Tests that used ruth as the manager fixture use plain names. --- docs/agent-lifecycle/config-changes.md | 3 +- docs/process/conventions.md | 4 +- hive-agent-mcp/src/mcp/args.rs | 4 +- hive-agent-mcp/src/mcp/mod.rs | 2 +- hive-agent-mcp/src/send_allow.rs | 3 +- hive-c0re/README.md | 4 +- hive-c0re/src/agent_config/topology.rs | 7 +-- hive-c0re/src/gateway_nginx.rs | 19 +++----- hive-c0re/src/lifecycle/mod.rs | 6 --- hive-c0re/src/loose_ends.rs | 2 +- hive-c0re/src/main.rs | 1 - hive-c0re/src/migrate.rs | 8 +-- hive-c0re/src/socket_server/mod.rs | 67 ++++---------------------- hive-c0re/src/workers/agent_sockets.rs | 7 ++- hive-priv-sock/src/lib.rs | 4 -- nix/agent-modules/mcp.nix | 12 ++--- 16 files changed, 36 insertions(+), 117 deletions(-) diff --git a/docs/agent-lifecycle/config-changes.md b/docs/agent-lifecycle/config-changes.md index 76f218dd..c39ca99f 100644 --- a/docs/agent-lifecycle/config-changes.md +++ b/docs/agent-lifecycle/config-changes.md @@ -336,8 +336,7 @@ Differences from sub-agents: and `/var/lib/hyperhive/applied` → `/applied` (RO) so the root agent can `git fetch` deployed/failed/denied tags from any agent's authoritative applied repo (see "Root-agent view of applied" below). -- `socket_server::start_manager` binds the root agent's socket, - pure transport with no dedicated helpers — it uses the same +- Its socket comes from the generic per-agent registration, in the same per-agent runtime dir as any other agent (`/run/hyperhive/agents/ruth/`), not a special manager-only path. diff --git a/docs/process/conventions.md b/docs/process/conventions.md index 5fa052b6..0e5ad74a 100644 --- a/docs/process/conventions.md +++ b/docs/process/conventions.md @@ -174,9 +174,7 @@ Each row carries enough context that the caller renders it directly as a bulleted list, no follow-up fetch needed. `GetLooseEnds` takes no target — it always returns the caller's own -rows, on both the agent and manager sockets alike (the manager, `ruth`, -is a normal agent here, just deployed automatically with different -default permissions). `Reminder` rows always carry `owner == self`. +rows. `Reminder` rows always carry `owner == self`. Per-variant fields: diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index cce44c42..a8f30f25 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -6,7 +6,7 @@ use rmcp::schemars; #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct SendArgs { - /// Logical agent name to deliver the message to (e.g. `"manager"`, + /// Logical agent name to deliver the message to (e.g. `"bob"`, /// `"alice"`). `"operator"` is refused: reach the operator over matrix /// if you have it. pub to: String, @@ -106,7 +106,7 @@ pub struct SetStatusArgs { #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct GetAgentMetaArgs { - /// Logical name of the agent to query (e.g. `"iris"`, `"manager"`). + /// Logical name of the agent to query (e.g. `"iris"`, `"bob"`). /// Omit to query your own identity + status — replaces the /// previous `whoami` self-introspection tool. #[serde(default)] diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index d3fc036c..11e27d9c 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -148,7 +148,7 @@ impl AgentServer { let log = format!("{args:?}"); let to = args.to.clone(); // Check per-agent allow-list (services.hyperhive.agent.allowedRecipients). When no - // policy file is present (e.g. manager containers) the check is a no-op. + // policy file is present the check is a no-op. if let Err(refusal) = crate::send_allow::check_send_allowed(&to) { return run_tool_envelope("send", log, async move { refusal }).await; } diff --git a/hive-agent-mcp/src/send_allow.rs b/hive-agent-mcp/src/send_allow.rs index 585c1ac3..5d3fbb8c 100644 --- a/hive-agent-mcp/src/send_allow.rs +++ b/hive-agent-mcp/src/send_allow.rs @@ -7,8 +7,7 @@ /// Where the NixOS module writes the per-agent send allow-list (see /// `nix/agent-modules/mcp.nix`). Empty list = unrestricted (the /// default). Non-empty list constrains `mcp__hyperhive__send`'s `to` -/// field; the manager is always implicitly permitted regardless of -/// the list contents. +/// field. const SEND_ALLOW_PATH: &str = "/etc/hyperhive/send-allow.json"; /// Enforce the per-agent send allow-list. Returns `Ok` when the diff --git a/hive-c0re/README.md b/hive-c0re/README.md index 758aed69..fb8c3ed7 100644 --- a/hive-c0re/README.md +++ b/hive-c0re/README.md @@ -37,8 +37,8 @@ to track it: - **`agent_config/`** — per-agent registries (permissions, resource limits, topology). - **`stats/`** — dashboard metrics aggregation + OTEL export. -- **`socket_server/`** — the unix-socket request server shared by - per-agent + manager sockets. +- **`socket_server/`** — the unix-socket request server for the + per-agent sockets. - **`forge/`** — optional Forgejo wiring (`docs/integrations/forge.md`). - **`coordinator.rs`** — top-level wiring for `serve`. - **`meta.rs`**, **`migrate.rs`** — the meta flake + schema/state diff --git a/hive-c0re/src/agent_config/topology.rs b/hive-c0re/src/agent_config/topology.rs index e7b13b84..e2374596 100644 --- a/hive-c0re/src/agent_config/topology.rs +++ b/hive-c0re/src/agent_config/topology.rs @@ -165,14 +165,11 @@ mod tests { #[test] fn apply_reconcile_adds_missing_live_agent() { - let live = vec![ - crate::lifecycle::MANAGER_NAME.to_owned(), - "newbie".to_owned(), - ]; + let live = vec!["alice".to_owned(), "newbie".to_owned()]; let (next, changed) = apply_reconcile(&BTreeSet::new(), &live, &[]); assert!(changed); assert!(next.contains("newbie")); - assert!(next.contains(crate::lifecycle::MANAGER_NAME)); + assert!(next.contains("alice")); } #[test] diff --git a/hive-c0re/src/gateway_nginx.rs b/hive-c0re/src/gateway_nginx.rs index 40ffd856..26824385 100644 --- a/hive-c0re/src/gateway_nginx.rs +++ b/hive-c0re/src/gateway_nginx.rs @@ -296,7 +296,6 @@ async fn reload_gateway_nginx() { #[cfg(test)] mod tests { use super::*; - use crate::lifecycle::MANAGER_NAME; // ── legacy mode (frontend_dir = None) ────────────────────────────── @@ -309,13 +308,10 @@ mod tests { } #[test] - fn render_includes_manager() { - let names: Vec = [MANAGER_NAME, "iris"] - .iter() - .map(|s| (*s).to_owned()) - .collect(); + fn render_includes_every_agent() { + let names: Vec = ["alice", "iris"].iter().map(|s| (*s).to_owned()).collect(); let body = render(&names, None); - assert!(body.contains(&format!("/agent/{MANAGER_NAME}/"))); + assert!(body.contains("/agent/alice/")); assert!(body.contains("/agent/iris/")); } @@ -461,13 +457,10 @@ mod tests { } #[test] - fn split_mode_includes_manager() { - let names: Vec = [MANAGER_NAME, "iris"] - .iter() - .map(|s| (*s).to_owned()) - .collect(); + fn split_mode_includes_every_agent() { + let names: Vec = ["alice", "iris"].iter().map(|s| (*s).to_owned()).collect(); let body = render(&names, Some(FAKE_FRONTEND)); - assert!(body.contains(&format!("/agent/{MANAGER_NAME}/"))); + assert!(body.contains("/agent/alice/")); assert!(body.contains("/agent/iris/")); } diff --git a/hive-c0re/src/lifecycle/mod.rs b/hive-c0re/src/lifecycle/mod.rs index 4decfdac..e488b7af 100644 --- a/hive-c0re/src/lifecycle/mod.rs +++ b/hive-c0re/src/lifecycle/mod.rs @@ -34,12 +34,6 @@ pub const MAX_AGENT_NAME: usize = 9; /// Logical name of the manager agent (broker recipient, state-dir key, /// meta flake attribute). All persistent state lives under `ruth/`. pub const MANAGER_NAME: &str = "ruth"; -/// Container name of the manager. Uses the same `h-` prefix as sub-agents -/// so `nixos-container list` output is uniform and the list filter is -/// a single `starts_with(AGENT_PREFIX)` check. Logical name → container -/// name: `ruth` → `h-ruth`. -pub const MANAGER_CONTAINER: &str = "h-ruth"; - /// Mount point of the per-agent runtime directory inside the container. pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive"; diff --git a/hive-c0re/src/loose_ends.rs b/hive-c0re/src/loose_ends.rs index 28c57329..e783edd5 100644 --- a/hive-c0re/src/loose_ends.rs +++ b/hive-c0re/src/loose_ends.rs @@ -1,6 +1,6 @@ //! Loose-ends aggregator. Assembles a `Vec` for a single agent //! (`for_agent`) — always the caller's own. `Request::GetLooseEnds` from -//! either the agent or manager socket lands here. Reminders and todos are +//! an agent socket lands here. Reminders and todos are //! agent-local (in-container `hive-agent` stores) and never sourced from //! here. //! diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 06c8a8ac..a4f4bfaa 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -267,7 +267,6 @@ async fn cmd_serve( // Copy value first for the dashboard + knowledge-webhook tasks. let dashboard_port = env.dashboard_port; let coord = Arc::new(Coordinator::open(&db, env, model_prices, build_slots)?); - socket_server::start_manager(coord.clone())?; // Idempotent pre-flight: rewrite pre-meta-layout applied // repos, ensure proposed repos carry the `applied` // remote, bootstrap the meta repo, repoint containers at diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index 29bce958..002ae255 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -27,7 +27,7 @@ use anyhow::{Context, Result}; use tokio::process::Command; use crate::coordinator::Coordinator; -use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_CONTAINER, MANAGER_NAME}; +use crate::lifecycle::{self, AGENT_PREFIX}; use crate::meta; const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION"; @@ -157,11 +157,7 @@ async fn enumerate_agents() -> Result> { Ok(containers .into_iter() .filter_map(|c| { - let name = if c == MANAGER_CONTAINER { - MANAGER_NAME - } else { - c.strip_prefix(AGENT_PREFIX)? - }; + let name = c.strip_prefix(AGENT_PREFIX)?; hive_types::Ident::parse(name).ok() }) .collect()) diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index b90145ed..7920801e 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -1,11 +1,8 @@ -//! Unix-socket request server, shared by the per-agent sockets and the -//! (pure-transport) manager socket. The socket file's existence on disk -//! authenticates the caller: connecting to `<.../agents/foo/mcp.sock>` means -//! you are `foo`; the manager socket simply serves as `ruth`. There is no -//! privilege flag — both transports run the same [`serve`] / [`dispatch`] -//! code, and authority derives uniformly from the caller's identity. An -//! agent-targeting verb may name any agent, so `ruth` reaches every agent -//! exactly the way every other agent does, not via any hardcoded name match. +//! Unix-socket request server for the per-agent sockets. The socket file's +//! existence on disk authenticates the caller: connecting to +//! `<.../agents/foo/mcp.sock>` means you are `foo`. There is no privilege +//! flag; [`serve`] / [`dispatch`] derive authority uniformly from the caller's +//! identity. An agent-targeting verb may name any agent. use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -13,7 +10,6 @@ use std::sync::Arc; use anyhow::{Context, Result}; use hive_core_agent_sock::{Request, Response}; use hive_sh4re::inbox::Message; -use hive_sh4re::manager::MANAGER_AGENT; use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; use tokio::task::JoinHandle; @@ -77,48 +73,6 @@ pub fn start(agent: &str, socket_path: &Path, coord: Arc) -> Result Ok(AgentSocket { path, handle }) } -/// Bind + serve the manager socket. This is now **pure transport**: it grants -/// no authority of its own — it just serves requests as `agent = MANAGER_AGENT` -/// ("ruth"), and ruth's reach comes entirely from the permissions it -/// holds, identical to connecting on a per-agent socket — ruth -/// uses the standard per-agent runtime dir + socket, with no dedicated helpers. -pub fn start_manager(coord: Arc) -> Result<()> { - use std::os::unix::fs::PermissionsExt as _; - let dir = crate::paths::agent_runtime_dir(crate::lifecycle::MANAGER_NAME); - std::fs::create_dir_all(&dir) - .with_context(|| format!("create manager dir {}", dir.display()))?; - let socket = Coordinator::socket_path(crate::lifecycle::MANAGER_NAME); - if socket.exists() { - std::fs::remove_file(&socket).context("remove stale manager socket")?; - } - let listener = UnixListener::bind(&socket) - .with_context(|| format!("bind manager socket {}", socket.display()))?; - // 0666 so the in-container root user (non-root) can connect; the bind - // source dir is manager-only on host (see the per-agent socket above). - std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o666)) - .with_context(|| format!("chmod manager socket {}", socket.display()))?; - tracing::info!(socket = %socket.display(), "manager socket listening"); - - tokio::spawn(async move { - let err = accept_until_fatal( - MANAGER_AGENT, - || async { listener.accept().await.map(|(stream, _)| stream) }, - |stream| { - let coord = coord.clone(); - tokio::spawn(async move { - // Pure transport: serve as `ruth`, no privilege grant. - if let Err(e) = serve(stream, MANAGER_AGENT.to_owned(), coord).await { - tracing::warn!(error = ?e, "manager connection failed"); - } - }); - }, - ) - .await; - exit_on_dead_listener(MANAGER_AGENT, &err); - }); - Ok(()) -} - /// Pause after an accept error that is not about a single connection. EMFILE /// and ENFILE leave the listener readable, so retrying at once would spin until /// some fd is closed. Same policy as axum's `serve`, which the dashboard runs. @@ -164,9 +118,9 @@ where } /// Exit on a listener that can no longer accept. A running hive-c0re re-binds -/// an agent socket only on that agent's next start, and the manager socket -/// never; on restart (`Restart = "on-failure"` on the unit) `start_manager` and -/// `mcp_sockets::sync_on_start` re-bind every one. +/// an agent socket only on that agent's next start; on restart +/// (`Restart = "on-failure"` on the unit) `mcp_sockets::sync_on_start` re-binds +/// every one. fn exit_on_dead_listener(agent: &str, e: &std::io::Error) -> ! { tracing::error!( %agent, @@ -176,7 +130,7 @@ fn exit_on_dead_listener(agent: &str, e: &std::io::Error) -> ! { std::process::exit(1) } -/// Longest request line accepted on the agent and manager sockets, newline +/// Longest request line accepted on the agent sockets, newline /// included. The largest request a client sends is an `OperatorMsg` from the /// agent web UI's `/send` (`hive-agent/src/web_ui/actions.rs:21`), whose body /// axum's default limit caps at 2 MiB; JSON escaping can roughly double that to @@ -286,8 +240,7 @@ pub(crate) fn recv_timeout(wait_seconds: Option) -> std::time::Duration { } } -/// Unified dispatch for every socket connection — per-agent sockets and the -/// (now pure-transport) manager socket alike. There is no privilege bit; +/// Unified dispatch for every socket connection. There is no privilege bit; /// authority derives uniformly from the caller's identity. async fn dispatch( req: &hive_core_agent_sock::Request, diff --git a/hive-c0re/src/workers/agent_sockets.rs b/hive-c0re/src/workers/agent_sockets.rs index 3de04282..60f71d98 100644 --- a/hive-c0re/src/workers/agent_sockets.rs +++ b/hive-c0re/src/workers/agent_sockets.rs @@ -209,7 +209,6 @@ pub fn spawn_poll() { #[cfg(test)] mod tests { use super::*; - use crate::lifecycle::MANAGER_NAME; #[test] fn socket_path_for_uses_subdir_layout() { @@ -235,13 +234,13 @@ mod tests { } #[test] - fn build_map_includes_manager() { - let names: Vec = ["iris", MANAGER_NAME, "argus"] + fn build_map_includes_every_agent() { + let names: Vec = ["iris", "alice", "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("alice")); assert!(map.contains_key("iris")); assert!(map.contains_key("argus")); } diff --git a/hive-priv-sock/src/lib.rs b/hive-priv-sock/src/lib.rs index 24844cf5..3de8f572 100644 --- a/hive-priv-sock/src/lib.rs +++ b/hive-priv-sock/src/lib.rs @@ -25,10 +25,6 @@ pub const PRIV_SOCK: &str = "/run/hive/priv.sock"; /// exactly the failure mode a shared constant exists to prevent. pub const PAUSED_MARKER_FILE: &str = "paused"; -/// Manager logical agent name. The manager's system container name is -/// `h-ruth` (same `h-` prefix convention as every other agent). -pub const MANAGER_NAME: &str = "ruth"; - /// Sub-agent container prefix. System container name = `h-`. pub const AGENT_PREFIX: &str = "h-"; diff --git a/nix/agent-modules/mcp.nix b/nix/agent-modules/mcp.nix index d88831d2..82e32433 100644 --- a/nix/agent-modules/mcp.nix +++ b/nix/agent-modules/mcp.nix @@ -27,23 +27,19 @@ in default = [ ]; example = [ "alice" - "manager" + "bob" ]; description = '' Names this agent is allowed to `send` to via `mcp__hyperhive__send`. Empty list (the default) means - unrestricted — the agent can message any peer or the - manager. Non-empty list constrains the - surface: only the listed names + the manager (always - allowed) get through; anything else returns an error + unrestricted — the agent can message any peer. A non-empty list + allows only the listed names; anything else returns an error string to claude without touching the broker. `operator` is refused either way: agents reach the operator over matrix. Useful for sandboxing untrusted sub-agents — set - `[ "manager" ]` to scope them to manager-only chatter. - The manager itself is always exempt; this option only - affects sub-agent `send`. + `[ "alice" ]` to scope one to chatter with alice. ''; };