diff --git a/docs/agent-lifecycle/agent-roster.md b/docs/agent-lifecycle/agent-roster.md index c8e1ec65..14189b60 100644 --- a/docs/agent-lifecycle/agent-roster.md +++ b/docs/agent-lifecycle/agent-roster.md @@ -80,19 +80,14 @@ module docs for the exact call chain. The **manager (`ruth`)** differs from other agents in naming/bootstrap, its socket flavour and its default permissions; its prompt, tool -allow-list and state dirs work as for every other agent. A hive with -`services.hyperhive.ruthless = true` runs no manager: `hive-c0re` skips -its root-agent create/start sweep -(`hive-c0re/src/workers/auto_update.rs`), and this section doesn't -apply. +allow-list and state dirs work as for every other agent. - **Naming/bootstrap** — the manager's broker recipient name, state-dir key, and nixos-container name are all `ruth` (container `h-ruth`). - `hive-c0re` spawns it directly at boot if missing, with no operator - approval step — every other agent is created at swarm level - (`swarmctl agent create`). + Like every agent it comes from the swarm dashboard or + `swarmctl agent create`; `hive-c0re` creates nothing at boot. Roster-wise, `ruth` is just another entry. - **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum is the same on both socket flavours. Container lifecycle diff --git a/docs/agent-lifecycle/config-changes.md b/docs/agent-lifecycle/config-changes.md index 03364edc..c39ca99f 100644 --- a/docs/agent-lifecycle/config-changes.md +++ b/docs/agent-lifecycle/config-changes.md @@ -106,9 +106,8 @@ notes) is reuses prior config + login. With `purge = true` the agent's `/var/lib/hyperhive/{agents,applied}//` trees are also wiped (config history + creds + notes gone forever). The -root/bootstrap container is destroyable like any other — hive-c0re -recreates it on the next startup if it's absent, so destroying it's -transient. +root/bootstrap container is destroyable like any other, and stays gone +until you create ruth again from the swarm dashboard. ## Meta flake @@ -143,7 +142,7 @@ autoupdate flake-rev bump (one shot before per-agent rebuilds). `meta::sync_agents(hive: &HiveEnv, agents: &[AgentSpec])` — `hive` carries `hyperhive_flake`, `dashboard_port`, and the rest of the -per-hive config — is the idempotent reconciler called by `spawn`, +per-hive config — is the idempotent reconciler called by `provision_container`, `destroy`, `rebuild`, and the startup migration. Renders `flake.nix` from the agent list; if it differs from disk, runs `nix flake lock` + commits as `regenerate meta flake` (or @@ -314,11 +313,12 @@ No state loss in either migration: claude creds, `/state/` notes, the events DB, and both proposed + applied history all survive. The root agent keeps its session; sub-agents stay logged in. -## The root/bootstrap container is hive-c0re-managed +## The root/bootstrap container The root agent container runs through the **same lifecycle as -sub-agents**. On `hive-c0re serve` startup, if `ruth` is missing, -hive-c0re creates it. The root agent's flake lives at +sub-agents**. Nothing creates it at hive boot: you create ruth from the +swarm dashboard like any agent, and an existing ruth container starts with +the hive like every other. The root agent's flake lives at `/var/lib/hyperhive/applied/ruth/`; its proposed config at `/var/lib/hyperhive/agents/ruth/config/`. The root agent can edit its own `agent.nix` (visible inside the container at `/agents/ruth/config/`) @@ -336,14 +336,13 @@ 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. **Migration note** (for older hosts): drop any `containers.root = -{ ... }` block from your host NixOS config. hyperhive creates and -updates the root agent itself. +{ ... }` block from your host NixOS config, and any `services.hyperhive.ruthless` +definition, then create ruth from the swarm dashboard. ## Root-agent policy diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index 06b255b4..bdcc6bf8 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -31,9 +31,9 @@ Beyond that: restart and rebuild** — only an explicit purge (or a hive `--purge`-style host operation, or the agent's own choices) touches them. -- The **root/bootstrap agent is special**: it isn't really destroyable - in practice — hive-c0re recreates it automatically on its next - startup if it's ever gone. +- The **root/bootstrap agent has no guard**: destroying ruth is as final as + for any agent, and she stays gone until you create her again from the + swarm dashboard. Everything below this point is implementation detail: exact table schemas, file layouts, and internal migration mechanics. @@ -432,11 +432,9 @@ an agent's state. The mechanics, for completeness: - `PURG3` wipes `/var/lib/hyperhive/{agents,applied}//` — the union of everything `DESTR0Y` left behind. -`actions::destroy` implements the root/bootstrap agent's specialness as a soft policy -guard that refuses to destroy it, backstopped by -`auto_update::ensure_root_agent`, which recreates it on the next -hive-c0re startup if it's ever absent — so even without the guard, -destroying it would only be transient. +`actions::destroy` has no guard for the root/bootstrap agent: it destroys ruth +like any other agent, and she stays gone until you create her again from the +swarm dashboard. ### btrfs subvolumes for `/var/lib/hyperhive/agents/` diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md index 11b3fb72..973fdf8a 100644 --- a/docs/getting-started/setup.md +++ b/docs/getting-started/setup.md @@ -89,24 +89,20 @@ swarmctl forge make-admin mara If the forge already has a local account with your username, the first SSO login asks for its forge password once, to link the two. -## 4 · Ruth's store identity +## 4 · Ruth -hive-c0re creates the manager agent, ruth, on its own at startup — so -unlike agents made through the swarm, she has no store identity yet: +hive-c0re creates no agent at startup. You create ruth, the manager agent, +like any other: open the swarm UI start page and click the paperclip, which +opens the create form with `ruth` filled in. The form declares her, creates +her config repo and deploys her to the hive you pick. -```bash -swarmctl agent create ruth --hive # on the swarm-controller's host -hivectl agent ruth rebuild # on ruth's hive: hands her the identity -``` +The swarm declares a new agent `paused`, so without a further step ruth runs +but drives no turns. Open `/agents` in the swarm UI and set her `up` from her +card. -`agent create` declares an agent the swarm has no declaration for as -`paused`, so without a further step ruth keeps running but drives no turns. -`swarmctl` has no verb for wanted state: open `/agents` in the swarm UI and set -her `up` from her card. - -Within about ten minutes the controller creates her forge user and matrix -account and mints their tokens into the store, where she fetches them. -`swarmctl agent mint-forge-token ruth` skips the wait for the forge token. +On a hive that already has a ruth container from an earlier release, create +her once the same way, so that her config repo and store identity exist. The +container keeps starting with the hive meanwhile. ## 5 · Matrix 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/docs/scheduler/coordinator.md b/docs/scheduler/coordinator.md index 82010b29..366b9638 100644 --- a/docs/scheduler/coordinator.md +++ b/docs/scheduler/coordinator.md @@ -178,7 +178,7 @@ Power toggles never commit to the meta repo. Every operator power surface — dashboard buttons, the MCP tools, and `hivectl stop/start/restart/kill` — rides the queue through that power layer, so intent, lease serialization, and crash-watch suppression can't drift per surface; the only direct starts -left are the root-agent bootstrap and infra containers (no lease, no +left are infra containers (no lease, no harness). Cancelling a still-queued power DAG reverts `wanted` to the observed state — a cancel means "don't do it," not "do it later." hive-c0re seeds agents without a row from observed state on first touch (running ⇒ diff --git a/docs/turn-loop/README.md b/docs/turn-loop/README.md index f3a70e16..b686c942 100644 --- a/docs/turn-loop/README.md +++ b/docs/turn-loop/README.md @@ -98,7 +98,7 @@ variants regardless of who sends them): A small `Surface` trait, with one zero-sized impl, factors `hive-agent`'s wire types (`hive_core_agent_sock::{Request, Response}` — -one unified enum shared by the agent and manager sockets) and its turn +one unified enum shared by every agent socket) and its turn loop, so the loop itself has no per-role branches. See `hive-agent/src/main.rs`'s module doc for the trait shape. 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-agent/src/web_ui/state.rs b/hive-agent/src/web_ui/state.rs index 1a3190bc..e86a00b7 100644 --- a/hive-agent/src/web_ui/state.rs +++ b/hive-agent/src/web_ui/state.rs @@ -394,7 +394,7 @@ struct ExtraLink { } /// Best-effort: pull the last 30 messages addressed to us via the -/// per-agent / manager socket. Empty list on any transport / decode +/// per-agent socket. Empty list on any transport / decode /// failure — the inbox section is decorative, not authoritative. async fn recent_inbox(socket: &std::path::Path) -> Vec { const LIMIT: u64 = 30; 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/actions.rs b/hive-c0re/src/actions.rs index 841e123e..dd9d72e6 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -126,10 +126,9 @@ async fn ff_applied_main( /// is cleared because its contents (the mcp socket) don't survive restarts /// anyway. With `purge=true` the persistent trees are also wiped — config /// history, claude creds, notes — there is no undo. -/// The bootstrap/root container is destroyable like any other: it's -/// imperative infra that `auto_update::ensure_root_agent` recreates on the -/// next hive-c0re startup if absent, so destroying it is transient rather -/// than something to refuse at the API. +/// The bootstrap/root container is destroyable like any other. Nothing +/// recreates it afterwards: ruth stays gone until she is created again from +/// the swarm dashboard. /// /// Submits the teardown DAG and returns — it does not wait for the container to /// go away. Same contract as every other lifecycle op (`rebuild`, `kill`, 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/coordinator.rs b/hive-c0re/src/coordinator.rs index 1592ed09..8fe5730c 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -496,7 +496,7 @@ impl Coordinator { } /// Snapshot the hive-wide configuration fields as a [`HiveEnv`]. - /// Pass the result to `lifecycle::spawn` / `rebuild` / `meta::sync_agents` + /// Pass the result to `lifecycle::provision_container` / `rebuild` / `meta::sync_agents` /// instead of threading the individual fields separately. #[must_use] pub fn hive_env(&self) -> HiveEnv { 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 580e509b..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"; @@ -225,15 +219,6 @@ async fn port_collision(self_name: &str) -> Result> { Ok(None) } -pub async fn spawn(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> { - create_container(name, hive, paths).await?; - // Runtime dir must exist before nixos-container start (nspawn bind-mount - // source). Create it here so callers don't need a separate preamble step. - ensure_agent_runtime_dir(name)?; - write_dropins(name, hive, paths).await?; - priv_run("start", name).await -} - /// First-spawn pre-create provisioning — the job queue's `Provision` /// node. Fail fast on a port collision, set up the proposed/applied /// repos + state subvolume + claude/notes dirs, then register the new @@ -266,16 +251,6 @@ pub async fn create_only(name: &str) -> Result<()> { priv_run("create", name).await } -/// First-spawn provisioning + `nixos-container create`, without the -/// drop-in write or the start. `spawn` composes this with -/// `write_dropins` + start for direct callers (root-agent bootstrap); -/// the job queue instead runs `provision_container` (`Provision` node) -/// and `create_only` (`Create` node) as separate DAG steps. -pub async fn create_container(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> { - provision_container(name, hive, paths).await?; - create_only(name).await -} - /// Rebuild-path preamble, run by the job queue's `Prebuild` node: fail fast on /// a port collision, then make sure the applied repo + state dirs exist. /// Container untouched. 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 761d32ea..a4f4bfaa 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -267,26 +267,17 @@ 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 // `meta#` (one-shot, guarded by a marker file). - // Runs before manager auto-spawn so the new manager is - // built against meta from the first attempt. + // Runs before the boot reconcile, so agents are built against meta. if let Err(e) = migrate::run(&coord).await { tracing::warn!(error = ?e, "startup migration failed"); } // Before any agent starts, so the bind mounts it gets from the stored // permissions are the meta repo's grants until the swarm answers. crate::workers::permissions_import::seed_local(); - // Auto-create the root agent container if it isn't there yet. Block - // on this — without root the system has no manager harness. - // Failures are logged but allowed: a broken auto-spawn shouldn't - // make the dashboard unreachable for debugging. - if let Err(e) = auto_update::ensure_root_agent(&coord).await { - tracing::warn!(error = ?e, "auto-spawn root agent failed"); - } // Auto-update in the background — don't block service start. // Sub-agent rebuilds can take tens of seconds; we want the admin // socket up immediately. 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-c0re/src/workers/auto_update.rs b/hive-c0re/src/workers/auto_update.rs index b02305b8..a11fbafd 100644 --- a/hive-c0re/src/workers/auto_update.rs +++ b/hive-c0re/src/workers/auto_update.rs @@ -22,7 +22,7 @@ use std::sync::Arc; use anyhow::Result; use crate::coordinator::Coordinator; -use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME}; +use crate::lifecycle::{self, AGENT_PREFIX}; /// Resolve the current rev of `hyperhive_flake`. For a path on disk we /// canonicalize (following symlinks) so a /etc/hyperhive → /nix/store/... @@ -66,150 +66,6 @@ pub async fn agent_config_pending(name: &str, deployed_sha: Option<&str>) -> boo } } -/// Whether this hive is "ruthless" — running with no root/manager agent at -/// all (no ruth). When true, hive-c0re skips the root-agent create/start -/// sweep entirely. Controlled by the host option -/// `services.hyperhive.ruthless`, threaded in via the `HYPERHIVE_RUTHLESS` -/// env var. Defaults to `false` when the var is unset (back-compat: the -/// root agent was always auto-managed before this opt-out existed); only -/// an explicit `true` / `1` / `yes` enables ruthless mode. -fn ruthless() -> bool { - match std::env::var("HYPERHIVE_RUTHLESS") { - Ok(v) => matches!(v.trim().to_ascii_lowercase().as_str(), "true" | "1" | "yes"), - Err(_) => false, - } -} - -/// What `ensure_root_agent` does with a `lifecycle::list()` result, before -/// it even gets to check whether the manager is present. -#[derive(Debug, PartialEq, Eq)] -enum RootAgentPlan { - /// The manager is in this readable list. - Present, - /// The manager is absent from this readable list: spawn it. - Absent, - /// The list could not be read. That is not "manager absent" — it - /// proves nothing either way, so the auto-spawn decision is skipped - /// for this attempt rather than risking a spawn over a manager that's - /// actually there. - Unreadable, -} - -/// Turn a `lifecycle::list()` result into `ensure_root_agent`'s plan. -fn plan_root_agent(list_result: anyhow::Result>) -> RootAgentPlan { - match list_result { - Ok(names) - if names - .iter() - .any(|c| c.strip_prefix(AGENT_PREFIX) == Some(MANAGER_NAME)) => - { - RootAgentPlan::Present - } - Ok(_) => RootAgentPlan::Absent, - Err(_) => RootAgentPlan::Unreadable, - } -} - -/// Auto-create the manager container on startup if it isn't already there. -/// hive-c0re manages the manager end-to-end: operators no longer declare -/// `containers.h-ruth` in their host NixOS config. The root/manager is -/// auto-managed by default. Operators who -/// don't want a root agent at all set `services.hyperhive.ruthless = true`, -/// which short-circuits this whole function. Idempotent. -pub async fn ensure_root_agent(coord: &Arc) -> Result<()> { - if ruthless() { - tracing::info!( - "ruthless mode (services.hyperhive.ruthless = true) - skipping root agent create/start" - ); - return Ok(()); - } - let list_result = lifecycle::list().await; - if let Err(e) = &list_result { - tracing::warn!( - error = ?e, - "manager container list unreadable — skipping auto-spawn check for this attempt" - ); - } - let plan = plan_root_agent(list_result); - let current_rev = current_flake_rev(&coord.hyperhive_flake); - if plan == RootAgentPlan::Unreadable { - // An unreadable list is not "manager absent" — spawning on it would - // both waste `provision_container`'s work and hit `nixos-container - // create`'s "already exists" failure if the manager is actually - // there. Skip the decision this attempt; there is no periodic - // retry for this boot-time call, so the next chance is the next - // `hive-c0re` restart. - return Ok(()); - } - if plan == RootAgentPlan::Present { - // Container exists already. If it predates the unified lifecycle - // (no applied flake on disk) we must rebuild — otherwise it's - // running whatever the host-declarative config was at create - // time, with a wrong systemd unit and port. - let applied_flake = crate::paths::applied_dir(MANAGER_NAME).join("flake.nix"); - if !applied_flake.exists() && current_rev.is_some() { - tracing::warn!( - "manager container exists but no applied flake — forcing rebuild to migrate" - ); - if let Err(e) = coord.job_queue.insert_job(|b| { - crate::job_queue::templates::rebuild(b, MANAGER_NAME, true); - Vec::new() - }) { - tracing::warn!(error = ?e, "manager migration rebuild insert failed"); - } - } else { - tracing::debug!("manager container already present"); - } - // hive-c0re auto-manages the root/manager by default, so a - // present-but-stopped root (e.g. a first-start failure on a fresh - // install) is brought back up here: the startup sweep's rebuild only - // restarts a container that was already running, so without this it - // stays down until a manual `nixos-container start`. The sub-agent - // `was_running` guard is intentionally left untouched. (Operators - // opt out of this whole auto-management with - // `services.hyperhive.ruthless = true`, gated at the top of - // this function.) - if !lifecycle::is_running(MANAGER_NAME).await { - tracing::info!("manager container present but not running — starting"); - if let Err(e) = coord.power.set(MANAGER_NAME, crate::power::Wanted::Up) { - tracing::warn!(error = ?e, "agent_power: set manager wanted=up failed"); - } - // Through the preamble: after a reboot its bind sources and limits - // drop-in in `/run` are gone, and nothing else recreates them. - let hive = coord.hive_env(); - let started = async { - let paths = Coordinator::agent_paths( - MANAGER_NAME, - crate::paths::agent_runtime_dir(MANAGER_NAME), - )?; - let token = lifecycle::converge_start_preamble(MANAGER_NAME, &hive, &paths).await?; - lifecycle::start_with_fallback(token).await - } - .await; - if let Err(e) = started { - tracing::warn!(error = ?e, "manager start failed"); - } - } - return Ok(()); - } - tracing::info!("manager container missing — spawning"); - // lifecycle::spawn creates the runtime dir internally; no manual - // ensure_agent_runtime_dir needed here. - let runtime = crate::paths::agent_runtime_dir(MANAGER_NAME); - let hive = coord.hive_env(); - let paths = Coordinator::agent_paths(MANAGER_NAME, runtime)?; - lifecycle::spawn(MANAGER_NAME, &hive, &paths).await?; - if let Err(e) = coord.power.set(MANAGER_NAME, crate::power::Wanted::Up) { - tracing::warn!(error = ?e, "agent_power: set manager wanted=up failed"); - } - if let Some(rev) = current_rev - && let Err(e) = std::fs::write(crate::paths::applied_rev_marker(MANAGER_NAME), &rev) - { - tracing::warn!(name = MANAGER_NAME, error = ?e, "write rev marker failed"); - } - Ok(()) -} - /// Boot reconcile (see the module doc): classify every agent by rev /// freshness + persisted `wanted` intent, submit one `Boot` DAG /// (hyperhive lock bump growing an in-DAG rebuild subgraph per stale @@ -463,35 +319,9 @@ fn submit_boot_tree( #[cfg(test)] mod tests { - use super::{BootAction, MANAGER_NAME, RootAgentPlan, boot_action, plan_root_agent}; - use crate::lifecycle::AGENT_PREFIX; + use super::{BootAction, boot_action}; use crate::power::Wanted; - // ----------------------------------------------------------------------- - // `ensure_root_agent`'s spawn-or-not decision. An unreadable list must - // never be read as "manager absent" — see `plan_root_agent`'s doc. - - /// The regression this fix exists for: a read failure must not spawn. - #[test] - fn unreadable_list_skips_without_spawning() { - let result: anyhow::Result> = - Err(anyhow::anyhow!("connect to hive-priv socket")); - assert_eq!(plan_root_agent(result), RootAgentPlan::Unreadable); - } - - /// Control: a genuinely empty, readable list still plans a spawn. - #[test] - fn readable_list_missing_manager_spawns() { - let result: anyhow::Result> = Ok(Vec::new()); - assert_eq!(plan_root_agent(result), RootAgentPlan::Absent); - } - - #[test] - fn readable_list_with_manager_present_is_a_noop() { - let result: anyhow::Result> = Ok(vec![format!("{AGENT_PREFIX}{MANAGER_NAME}")]); - assert_eq!(plan_root_agent(result), RootAgentPlan::Present); - } - // The regression the `Unreadable` variant exists for: the arm used to // substitute `from_running(running)`, which is `Noop` against BOTH // observations — so no combination of inputs could reach a reconcile. diff --git a/hive-core-agent-sock/README.md b/hive-core-agent-sock/README.md index 37bb158b..b249cb42 100644 --- a/hive-core-agent-sock/README.md +++ b/hive-core-agent-sock/README.md @@ -1,8 +1,8 @@ # hive-core-agent-sock -Wire types for the **per-agent + manager socket** (`/run/hive/mcp.sock`) — the +Wire types for the **per-agent socket** (`/run/hive/mcp.sock`) — the unified `Request` / `Response` protocol spoken between an agent's in-container -harness (and the manager) and the `hive-c0re` daemon. +harness and the `hive-c0re` daemon. ## Why it's its own crate diff --git a/hive-core-agent-sock/src/lib.rs b/hive-core-agent-sock/src/lib.rs index 426d430d..ad7d4c36 100644 --- a/hive-core-agent-sock/src/lib.rs +++ b/hive-core-agent-sock/src/lib.rs @@ -1,7 +1,7 @@ -//! Per-agent + manager socket wire types (`/run/hive/mcp.sock`). +//! Per-agent socket wire types (`/run/hive/mcp.sock`). //! //! The unified `Request` / `Response` protocol spoken between an agent's -//! in-container harness (and the manager) and the `hive-c0re` daemon over the +//! in-container harness and the `hive-c0re` daemon over the //! per-agent mcp socket. Re-homed out of `hive-sh4re` so this one socket owns //! its own protocol crate, mirroring `hive-host-sock` / `hive-priv-sock`. The //! shared payload types it references (`Message`, `LooseEnd`, …) @@ -17,7 +17,7 @@ fn default_true() -> bool { true } -/// Unified request enum for both agent and manager sockets. The agent's +/// Unified request enum for the per-agent socket. The agent's /// identity is the socket it arrived on. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "cmd", rename_all = "snake_case")] @@ -59,9 +59,7 @@ pub enum Request { /// Non-mutating — pulls from the broker without delivering. The /// per-agent web UI uses this to render its own inbox section. Recent { limit: u64 }, - /// Loose-ends view — always the caller's own. Same shape on the agent - /// and manager sockets; the manager (`ruth`) is a normal agent here, it - /// just sees its own threads like anyone else. See + /// Loose-ends view — always the caller's own. See /// `docs/process/conventions.md::Loose-ends wire shape`. GetLooseEnds, /// Set a free-text status string visible on the dashboard. The harness @@ -106,7 +104,7 @@ pub enum Request { PauseAcknowledged, } -/// Unified response enum for both agent and manager sockets. +/// Unified response enum for the per-agent socket. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum Response { 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/hive-sh4re/README.md b/hive-sh4re/README.md index 8d4c01da..f5bbd947 100644 --- a/hive-sh4re/README.md +++ b/hive-sh4re/README.md @@ -13,7 +13,7 @@ split into their own smaller crates so specialised binaries don't have to pull in all of `hive-sh4re`: - `hive-host-sock` — host admin socket (`hivectl` ↔ `hive-c0re`) -- `hive-core-agent-sock` — per-agent/manager socket (`/run/hive/mcp.sock`) +- `hive-core-agent-sock` — per-agent socket (`/run/hive/mcp.sock`) - `hive-priv-sock` — the privileged-helper socket Those crates re-export or reference the payload types that still live here diff --git a/hive-sh4re/src/manager.rs b/hive-sh4re/src/manager.rs index 50689dbd..f5b24920 100644 --- a/hive-sh4re/src/manager.rs +++ b/hive-sh4re/src/manager.rs @@ -1,5 +1,3 @@ -//! Manager socket — `/run/hyperhive/manager/mcp.sock` on the host, -//! bind-mounted into the manager container at `/run/hive/mcp.sock`. //! Reserved recipient names/senders and the out-of-band `HelperEvent` //! payload hive-c0re pushes into the manager's inbox. 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. ''; }; diff --git a/nix/host-modules/hive-c0re/environment.nix b/nix/host-modules/hive-c0re/environment.nix index ad8ba96a..818afe55 100644 --- a/nix/host-modules/hive-c0re/environment.nix +++ b/nix/host-modules/hive-c0re/environment.nix @@ -46,10 +46,6 @@ in # prompts). `hive_sh4re::assets::*` reads paths underneath. # `forge/users.rs` reads the core avatar PNG from here on startup. HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive"; - # Whether this hive runs ruthless — no root/manager agent at all - # (`auto_update::ensure_root_agent`). Default false = root - # auto-managed; true makes the sweep a no-op. - HYPERHIVE_RUTHLESS = lib.boolToString config.services.hyperhive.ruthless; } // { # Identity env vars threaded into c0re's own service env and diff --git a/nix/host-modules/hyperhive.nix b/nix/host-modules/hyperhive.nix index 8d92f381..8741446a 100644 --- a/nix/host-modules/hyperhive.nix +++ b/nix/host-modules/hyperhive.nix @@ -19,6 +19,11 @@ in [ "services" "hyperhive" "swarmName" ] [ "services" "hyperhive" "swarm" "name" ] ) + (lib.mkRemovedOptionModule [ "services" "hyperhive" "ruthless" ] '' + hive-c0re no longer creates ruth at boot. Remove this definition, and create + ruth from the swarm dashboard like any other agent; a ruth container that + already exists keeps starting with the hive. + '') ]; # Canonical hive DNS domain shared by every subsystem that needs a @@ -194,23 +199,6 @@ in # the values it asserts. It is a deployment mode rather than a setting # this module's options read, so it lives with its consequences. - # Whether this hive runs "ruthless" — with no root/manager agent at - # all. Some hives don't want a root agent — see issue tracker - # "scope concept: special agents". - options.services.hyperhive.ruthless = lib.mkOption { - type = lib.types.bool; - default = false; - example = true; - description = '' - Run this hive "ruthless" — with no root (manager) agent at all (no - ruth). When `true`, hive-c0re skips the root-agent auto-management - sweep entirely (it otherwise creates the root agent's container when - missing and restarts it when present but stopped). Defaults to - `false` (the root agent is auto-managed as required - infrastructure). Exposed to hive-c0re as `HYPERHIVE_RUTHLESS`. - ''; - }; - options.services.hyperhive.github.enable = lib.mkOption { type = lib.types.bool; default = true; diff --git a/nix/module-eval/core-toggle.nix b/nix/module-eval/core-toggle.nix index 38837ea0..15d4f69b 100644 --- a/nix/module-eval/core-toggle.nix +++ b/nix/module-eval/core-toggle.nix @@ -1044,6 +1044,26 @@ let } && !(refusedHiveName { deploy.hive-controller.enable = false; }); } + { + # The control is a host that defines no such option; the removal message + # must not appear for it. + name = "a definition of the removed services.hyperhive.ruthless option fails evaluation, pointing at the swarm dashboard"; + ok = + let + refused = + m: + lib.any ( + a: + !a.assertion + && lib.hasInfix "services.hyperhive.ruthless" a.message + && lib.hasInfix "swarm dashboard" a.message + ) m.assertions; + in + refused (hive { + ruthless = true; + }) + && !(refused allLocal); + } { # Refused wherever any hyperhive service runs, the hive or a swarm # service alone. The host enabling nothing is the control that it is