From 1b60084bf0ef6adbf407ca040332698b8866a69b Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 17 Oct 2026 00:12:01 +0200 Subject: [PATCH] build every agent from the agent-base template Delete nix/templates/ruth.nix, the ruth nixosConfiguration, module and toplevel, the hive-c0re managerToplevel option, and the meta flake's isManager switch with lifecycle::MANAGER_NAME. Every agent, ruth included, now builds from nix/templates/agent.nix. The template's only content beyond agent.nix was docs.enable = mkDefault true; docs.enable is off by default for every agent and ruth's own agent.nix turns it on. Refs #5377 --- CLAUDE.md | 4 +- docs/agent-lifecycle/config-changes.md | 6 +- docs/turn-loop/README.md | 9 ++- docs/turn-loop/config.md | 6 +- flake.nix | 8 +-- hive-c0re/src/lifecycle/host_config.rs | 2 +- hive-c0re/src/lifecycle/mod.rs | 5 -- hive-c0re/src/meta.rs | 80 ++++++++++++++++---------- nix/agent-modules/default.nix | 5 +- nix/agent-modules/docs.nix | 5 +- nix/agent-modules/packages.nix | 6 +- nix/host-modules/hive-c0re/default.nix | 3 +- nix/host-modules/hive-c0re/options.nix | 18 +++--- nix/packages/default.nix | 9 ++- nix/templates/ruth.nix | 14 ----- 15 files changed, 85 insertions(+), 95 deletions(-) delete mode 100644 nix/templates/ruth.nix diff --git a/CLAUDE.md b/CLAUDE.md index 1c5d073b..5f8f5ab2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,8 +174,8 @@ Swarm-level: `nix/packages/swarm-ui.nix`, not in `packages.default`'s closure). - **`nix/`** — `host-modules/` (the host stack), `agent-modules/` (per-agent harness features), `container-modules/` (guest side of - service containers), `templates/{agent,ruth}.nix` (container entry - points), `packages/`, `docs/` (options doc), `module-eval/` (NixOS + service containers), `templates/agent.nix` (the container entry + point), `packages/`, `docs/` (options doc), `module-eval/` (NixOS module-eval tests), `builder/` (`check-flake`), `script-tests/`, plus `sources.nix` / `rust.nix` / `checks.nix` / `devshell.nix` / `treefmt.nix` behind the thin `flake.nix`. diff --git a/docs/agent-lifecycle/config-changes.md b/docs/agent-lifecycle/config-changes.md index d6f02a9b..e08cac73 100644 --- a/docs/agent-lifecycle/config-changes.md +++ b/docs/agent-lifecycle/config-changes.md @@ -330,8 +330,10 @@ same as any other agent. Differences from sub-agents: -- `flake.nix` extends `hyperhive.nixosConfigurations.ruth` - (vs `agent-base`). +- Reference docs: her `agent.nix` turns on `services.hyperhive.agent.docs.enable`. + Her container builds from `agent-base` like every agent, and the seeded + `agent.nix` is the same for every name, so a newly created ruth has no docs + until a config PR adds that line. - Web UI port via `lifecycle::agent_web_port("ruth")` — same FNV-1a hash as every other agent (8100..8999 range). - The root agent holds `manage_root_agent` (the swarm UI's setup diff --git a/docs/turn-loop/README.md b/docs/turn-loop/README.md index 8d8da6f6..47716fd7 100644 --- a/docs/turn-loop/README.md +++ b/docs/turn-loop/README.md @@ -183,11 +183,10 @@ sibling), one shared `nix/agent-modules/` tree, one service unit differences live in the agent's swarm permissions and the broker socket (which tools and manager-surface calls each agent receives). -`agent.nix` and `ruth.nix` both import the shared `nix/agent-modules/`. -`ruth.nix` additionally defaults -`services.hyperhive.agent.docs.enable` to `true`, so the root/manager -gets the hyperhive reference docs readable at `$HIVE_DOCS_DIR` by -default; other agents opt in per `agent.nix` (see +`nix/templates/agent.nix` is the one entry point every agent builds from, and +it imports the shared `nix/agent-modules/`. +`services.hyperhive.agent.docs.enable` defaults to `false` for every agent, +ruth included; an agent opts in from its own `agent.nix` (see [config.md](config.md), "Reference docs"). ### Environment variables set on the unit diff --git a/docs/turn-loop/config.md b/docs/turn-loop/config.md index 9dace98b..62815699 100644 --- a/docs/turn-loop/config.md +++ b/docs/turn-loop/config.md @@ -8,7 +8,7 @@ Absent means the default. (The claude spawn + compaction themselves live in ## Reference docs (`services.hyperhive.agent.docs.enable`) ```nix -services.hyperhive.agent.docs.enable = true; # default: false (true for the manager agent) +services.hyperhive.agent.docs.enable = true; # default: false ``` Makes the hyperhive `docs/` tree available inside the container at a nix @@ -17,8 +17,8 @@ sentence into the agent's system prompt so it knows the docs exist and where to find them. `claude --add-dir` serves the tree so the full markdown is readable during every turn. -Enabled by default only for the root/manager agent (`nix/templates/ruth.nix`). Any -agent can opt in by adding the line above to its `agent.nix`. +Off by default for every agent, ruth included. An agent opts in by adding the +line above to its `agent.nix`. The `docs/` source is a narrow flake input (`hyperhive-docs`) tracked separately from the main `hyperhive` flake so editing docs re-locks only diff --git a/flake.nix b/flake.nix index bc3d24f0..1549da16 100644 --- a/flake.nix +++ b/flake.nix @@ -109,10 +109,6 @@ ./nix/templates/agent.nix agentPackages ]; - ruth.imports = [ - ./nix/templates/ruth.nix - agentPackages - ]; # The full host stack (nix/host-modules/default.nix aggregator) plus # the package/source wiring from this flake. The wiring is a # plain config module setting the `services.hyperhive.c0re.*` @@ -135,7 +131,7 @@ # Narrow docs/ source, threaded as its own meta-flake input # so doc edits don't re-hash the whole flake source. hyperhiveDocs = lib.mkDefault "${sources.hyperhiveDocsSource}"; - # Per-container toplevels — wired into + # The agent-base toplevel every container builds from — wired into # `system.extraDependencies` when # `services.hyperhive.c0re.preBuildAgentTemplates` is on so # the host system closure pre-fetches the heavy build @@ -143,7 +139,6 @@ # hardcoded to that system); the gate keeps aarch64 hosts # from pulling them in via cross-build. agentBaseToplevel = lib.mkDefault self.packages.x86_64-linux.agent-base-toplevel; - managerToplevel = lib.mkDefault self.packages.x86_64-linux.ruth-toplevel; }; services.hyperhive.deploy.swarm-controller.package = lib.mkDefault @@ -208,7 +203,6 @@ in { agent-base = mkContainer self.nixosModules.agent-base; - ruth = mkContainer self.nixosModules.ruth; }; devShells = forAllSystems ({ pkgs, rust, ... }: import ./nix/devshell.nix { inherit pkgs rust; }); diff --git a/hive-c0re/src/lifecycle/host_config.rs b/hive-c0re/src/lifecycle/host_config.rs index 6b848ba8..df60f3a9 100644 --- a/hive-c0re/src/lifecycle/host_config.rs +++ b/hive-c0re/src/lifecycle/host_config.rs @@ -530,7 +530,7 @@ async fn set_nspawn_flags( // Claude credentials land at `/home//.claude` so the // `claude` CLI (which reads `$HOME/.claude`) finds them. The // harness service's environment sets `HOME` to the same path - // (`agent.nix` / `ruth.nix` templates), so no `--setenv` plumbing + // (`agent.nix` template), so no `--setenv` plumbing // is needed here — the bind alone is enough. let claude_mount = container_claude_mount(agent_name); diff --git a/hive-c0re/src/lifecycle/mod.rs b/hive-c0re/src/lifecycle/mod.rs index e488b7af..693d5703 100644 --- a/hive-c0re/src/lifecycle/mod.rs +++ b/hive-c0re/src/lifecycle/mod.rs @@ -31,9 +31,6 @@ pub use hive_host_sock::{AGENT_PREFIX, container_name}; /// 11 chars (it gets encoded into network interface names) and `AGENT_PREFIX` /// (`h-`) takes 2, so the agent name itself is at most `MAX_AGENT_NAME` chars. 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"; /// Mount point of the per-agent runtime directory inside the container. pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive"; @@ -327,7 +324,6 @@ async fn agents_for_meta(name_to_add: Option<&str>) -> Result) -> Result AgentSpec { + fn sample_spec(name: &str, port: u16) -> AgentSpec { AgentSpec { name: name.to_owned(), - is_manager, port, } } @@ -1259,7 +1252,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ); // nixpkgs is a top-level input with an explicit URL; hyperhive // follows it. @@ -1303,7 +1296,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ); assert!( !out.contains("hyperhive-docs"), @@ -1319,7 +1312,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ); assert!( out.contains(r#"networking.hostName = "h-${name}";"#), @@ -1357,7 +1350,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", Some("/nix/store/cccc-claude-code-2.1.220"), 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ); assert!( out.contains( @@ -1383,7 +1376,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ); assert!( !out.contains("claudeCodePath"), @@ -1401,7 +1394,7 @@ mod tests { "", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ); assert!( out.contains("nixpkgs.follows = \"hyperhive/nixpkgs\""), @@ -1430,9 +1423,9 @@ mod tests { None, 8000, &[ - sample_spec("argus", false, 9001), - sample_spec("bitburner", false, 9002), - sample_spec("dmatrix", false, 9003), + sample_spec("argus", 9001), + sample_spec("bitburner", 9002), + sample_spec("dmatrix", 9003), ], lookup, ); @@ -1452,6 +1445,33 @@ mod tests { ); } + #[test] + fn render_flake_builds_every_agent_from_agent_base() { + let out = render_flake( + "github:example/hyperhive", + "path:/nix/store/bbbb-hyperhive-docs-source", + "path:/nix/store/aaaa-nixpkgs-source", + None, + 8000, + &[sample_spec("alice", 9001), sample_spec("ruth", 9002)], + ); + assert_eq!( + out.matches("hyperhive.nixosConfigurations.").count(), + 1, + "one base for every agent:\n{out}" + ); + assert!( + out.contains("base = hyperhive.nixosConfigurations.agent-base;"), + "{out}" + ); + for line in [ + "alice = mkAgent { name = \"alice\"; port = 9001; };", + "ruth = mkAgent { name = \"ruth\"; port = 9002; };", + ] { + assert!(out.contains(line), "missing `{line}`:\n{out}"); + } + } + #[test] fn render_flake_skips_canonical_follows_when_lookup_returns_empty() { let out = render_flake_with_lookup( @@ -1460,7 +1480,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], |_| Vec::new(), ); // No agent-side follows when the lookup reports nothing @@ -1496,7 +1516,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ); unsafe { std::env::remove_var("HIVE_FORGE_URL"); @@ -1539,7 +1559,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ) }; let alpha = render_on("alpha-hive", "alpha-hive.example.test"); @@ -1717,7 +1737,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ); unsafe { std::env::remove_var("HIVE_FORGE_URL"); @@ -1751,7 +1771,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ); let want = format!( "agent-alice.url = \"git+file://{}\"", @@ -1793,7 +1813,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ) }; @@ -1883,7 +1903,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ) }; unsafe { @@ -1955,7 +1975,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", false, 9001)], + &[sample_spec("alice", 9001)], ) }; unsafe { @@ -1987,7 +2007,7 @@ mod tests { agent_memory_max: cap.to_owned(), ..HiveEnv::default() }; - let agents = [sample_spec("alice", false, 9001)]; + let agents = [sample_spec("alice", 9001)]; let small = render_hive_flake(&hive("4G"), &agents); let large = render_hive_flake(&hive("16G"), &agents); assert_eq!( diff --git a/nix/agent-modules/default.nix b/nix/agent-modules/default.nix index 03f889fb..5bc06991 100644 --- a/nix/agent-modules/default.nix +++ b/nix/agent-modules/default.nix @@ -1,7 +1,6 @@ # Shared scaffolding for every hyperhive harness container. -# `../templates/agent.nix` and `../templates/ruth.nix` both import -# agents use the same service unit regardless of which entry-point -# they came from. +# `../templates/agent.nix`, the one entry point every agent builds from, +# imports it. # # This is the core module: container plumbing (boot/nix/nixpkgs), # base tooling, and the cross-cutting `services.hyperhive.agent.icon` option. Each diff --git a/nix/agent-modules/docs.nix b/nix/agent-modules/docs.nix index 0020b582..3f51d64c 100644 --- a/nix/agent-modules/docs.nix +++ b/nix/agent-modules/docs.nix @@ -14,9 +14,8 @@ via `claude --add-dir`, so the markdown is readable at `$HIVE_DOCS_DIR/`, and appends a single pointer sentence to the agent's system prompt so it knows the docs exist (see - `hive-agent::prompt::render`). Default-on for the root/manager agent - (see `../templates/ruth.nix`), off elsewhere; any agent can flip it from its - `agent.nix`. + `hive-agent::prompt::render`). Off by default for every agent; an agent + turns it on from its `agent.nix`. ''; options.services.hyperhive.agent.docs.source = lib.mkOption { diff --git a/nix/agent-modules/packages.nix b/nix/agent-modules/packages.nix index 762cd25b..fadd5af7 100644 --- a/nix/agent-modules/packages.nix +++ b/nix/agent-modules/packages.nix @@ -1,6 +1,6 @@ # The hyperhive-built packages the harness modules consume, threaded # in explicitly as an option — no overlay. The flake's -# `nixosModules.{agent-base,ruth}` set this to the flake's own package +# `nixosModules.agent-base` sets this to the flake's own package # outputs via `lib.mkDefault`, so a per-agent override of an # individual key still wins. { lib, ... }: @@ -15,8 +15,8 @@ `hive-forge`, `hive-forge-notify`, `hive-github-notify`, `hive-matrix-daemon`, `hive-metric`, `hive-screen-mcp`) plus the `assets`, `frontend`, - `reference-docs` and `claude-plugins` trees. Wired by the flake's agent-base/ruth - nixosModules to `hyperhive.packages..*`; override an + `reference-docs` and `claude-plugins` trees. Wired by the flake's agent-base + nixosModule to `hyperhive.packages..*`; override an individual key per-agent to swap in a patched binary. ''; }; diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index f11e5c44..82799991 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -189,7 +189,7 @@ in # above for the rationale. environment.etc."hyperhive/serve.json".text = serveConfigJson; - # Pull the per-container toplevels into the host system closure. + # Pull the agent-base toplevel into the host system closure. # `system.extraDependencies` adds paths to the system build # without referencing them at runtime — nixos-rebuild fetches / # builds them, they end up in /nix/store, and the first @@ -197,7 +197,6 @@ in # do. Gated because the closure is sizeable and pinned to x86_64. system.extraDependencies = lib.optionals cfg.preBuildAgentTemplates [ cfg.agentBaseToplevel - cfg.managerToplevel ]; # Unprivileged coordinator user. hive-c0re runs as this user; diff --git a/nix/host-modules/hive-c0re/options.nix b/nix/host-modules/hive-c0re/options.nix index 68687a9d..108fb512 100644 --- a/nix/host-modules/hive-c0re/options.nix +++ b/nix/host-modules/hive-c0re/options.nix @@ -10,6 +10,12 @@ ... }: { + imports = [ + (lib.mkRemovedOptionModule [ "services" "hyperhive" "c0re" "managerToplevel" ] '' + Every agent, ruth included, builds from `agentBaseToplevel`. Remove this definition. + '') + ]; + options.services.hyperhive.c0re = { package = lib.mkOption { type = lib.types.package; @@ -92,14 +98,6 @@ enabled. ''; }; - managerToplevel = lib.mkOption { - type = lib.types.package; - defaultText = lib.literalExpression "hyperhive.packages.x86_64-linux.ruth-toplevel"; - description = '' - Pre-built manager (ruth) container system closure — see - `agentBaseToplevel`. - ''; - }; nixpkgsFlake = lib.mkOption { type = lib.types.str; default = "path:${pkgs.path}"; @@ -176,8 +174,8 @@ default = false; example = true; description = '' - Pre-fetch the per-container system closures (agent-base + - manager toplevels) into the host's /nix/store as part of this + Pre-fetch the agent-base container system closure, which every + agent builds from, into the host's /nix/store as part of this host's NixOS build, instead of letting the first agent spawn do all the work. diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 3601539c..cca594f5 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -273,7 +273,7 @@ in # is exposed too since it's independently useful/inspectable. inherit swagger-ui-dist swagger-ui-theme; - # Pre-built per-container system closures. Exposed as packages + # Pre-built system closure every agent container builds from. Exposed as a package # so operators can `nix build .#agent-base-toplevel` (or wire # them into their host system closure via the # `preBuildAgentTemplates` option on the hive-c0re module — @@ -283,13 +283,12 @@ in # when the meta evaluator goes to build the container. # # nixosConfigurations are pinned to x86_64-linux (nixos- - # containers only run native arch), so these toplevels are + # containers only run native arch), so this toplevel is # only useful on an x86_64-linux host — flake check across - # systems still tolerates evaluating them on aarch64 because - # they're plain derivations, but `nix build` from a non-x86 + # systems still tolerates evaluating it on aarch64 because + # it's a plain derivation, but `nix build` from a non-x86 # host would only succeed via a remote x86 builder. agent-base-toplevel = self.nixosConfigurations.agent-base.config.system.build.toplevel; - ruth-toplevel = self.nixosConfigurations.ruth.config.system.build.toplevel; # Auto-generated nix options reference for hyperhive. # `docs` bundles host + swarm + deploy + agent pages into one tree; diff --git a/nix/templates/ruth.nix b/nix/templates/ruth.nix deleted file mode 100644 index 89c19ddc..00000000 --- a/nix/templates/ruth.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ lib, ... }: -{ - # Entry-point for the privileged root agent (ruth). Referenced from - # `flake.nix` (`nixosConfigurations.ruth`) and the meta-flake's - # `applied/ruth/flake.nix`. - imports = [ ../agent-modules ]; - - # The root/manager bootstraps a fresh hive, so it gets the hyperhive - # reference docs made available by default (readable at - # `$HIVE_DOCS_DIR/`, added via `claude --add-dir`). `mkDefault` so a - # manager's own `agent.nix` can still turn it off. Other agents default - # off. - services.hyperhive.agent.docs.enable = lib.mkDefault true; -}