diff --git a/CLAUDE.md b/CLAUDE.md index 5f8f5ab2..1c5d073b 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.nix` (the container entry - point), `packages/`, `docs/` (options doc), `module-eval/` (NixOS + service containers), `templates/{agent,ruth}.nix` (container entry + points), `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 e08cac73..d6f02a9b 100644 --- a/docs/agent-lifecycle/config-changes.md +++ b/docs/agent-lifecycle/config-changes.md @@ -330,10 +330,8 @@ same as any other agent. Differences from sub-agents: -- 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. +- `flake.nix` extends `hyperhive.nixosConfigurations.ruth` + (vs `agent-base`). - 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/scheduler/coordinator.md b/docs/scheduler/coordinator.md index 8fc18d45..366b9638 100644 --- a/docs/scheduler/coordinator.md +++ b/docs/scheduler/coordinator.md @@ -566,8 +566,8 @@ config — all nspawn containers live in `machine.slice`. ### Pre-building agent templates `preBuildAgentTemplates` (default `false`) causes the host NixOS -build to pre-fetch the `agent-base` container system closure, which every -agent builds from, into `/nix/store`, instead of +build to pre-fetch the per-container system closures +(`agent-base` + manager toplevels) into `/nix/store`, instead of leaving that work to the first `nixos-container start`. The trade-off: @@ -581,7 +581,7 @@ trade-off: does all the eval + fetch work at runtime (can take a few minutes on a fresh store). -**Note**: the meta flake pins the toplevel to `x86_64-linux`. Enabling on an +**Note**: the meta flake pins toplevels to `x86_64-linux`. Enabling on an `aarch64` host forces a cross-compilation or remote-builder build, which is almost never desired. Leave off on non-x86 hosts. diff --git a/docs/turn-loop/README.md b/docs/turn-loop/README.md index 47716fd7..8d8da6f6 100644 --- a/docs/turn-loop/README.md +++ b/docs/turn-loop/README.md @@ -183,10 +183,11 @@ 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). -`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 +`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 [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 62815699..9dace98b 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 +services.hyperhive.agent.docs.enable = true; # default: false (true for the manager agent) ``` 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. -Off by default for every agent, ruth included. An agent opts in by adding the -line above to its `agent.nix`. +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`. 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 1549da16..bc3d24f0 100644 --- a/flake.nix +++ b/flake.nix @@ -109,6 +109,10 @@ ./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.*` @@ -131,7 +135,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}"; - # The agent-base toplevel every container builds from — wired into + # Per-container toplevels — wired into # `system.extraDependencies` when # `services.hyperhive.c0re.preBuildAgentTemplates` is on so # the host system closure pre-fetches the heavy build @@ -139,6 +143,7 @@ # 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 @@ -203,6 +208,7 @@ 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 df60f3a9..6b848ba8 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` template), so no `--setenv` plumbing + // (`agent.nix` / `ruth.nix` templates), 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 693d5703..e488b7af 100644 --- a/hive-c0re/src/lifecycle/mod.rs +++ b/hive-c0re/src/lifecycle/mod.rs @@ -31,6 +31,9 @@ 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"; @@ -324,6 +327,7 @@ async fn agents_for_meta(name_to_add: Option<&str>) -> Result) -> Result AgentSpec { + fn sample_spec(name: &str, is_manager: bool, port: u16) -> AgentSpec { AgentSpec { name: name.to_owned(), + is_manager, port, } } @@ -1252,7 +1259,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ); // nixpkgs is a top-level input with an explicit URL; hyperhive // follows it. @@ -1296,7 +1303,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ); assert!( !out.contains("hyperhive-docs"), @@ -1312,7 +1319,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ); assert!( out.contains(r#"networking.hostName = "h-${name}";"#), @@ -1350,7 +1357,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", Some("/nix/store/cccc-claude-code-2.1.220"), 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ); assert!( out.contains( @@ -1376,7 +1383,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ); assert!( !out.contains("claudeCodePath"), @@ -1394,7 +1401,7 @@ mod tests { "", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ); assert!( out.contains("nixpkgs.follows = \"hyperhive/nixpkgs\""), @@ -1423,9 +1430,9 @@ mod tests { None, 8000, &[ - sample_spec("argus", 9001), - sample_spec("bitburner", 9002), - sample_spec("dmatrix", 9003), + sample_spec("argus", false, 9001), + sample_spec("bitburner", false, 9002), + sample_spec("dmatrix", false, 9003), ], lookup, ); @@ -1445,33 +1452,6 @@ 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( @@ -1480,7 +1460,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], |_| Vec::new(), ); // No agent-side follows when the lookup reports nothing @@ -1516,7 +1496,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ); unsafe { std::env::remove_var("HIVE_FORGE_URL"); @@ -1559,7 +1539,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ) }; let alpha = render_on("alpha-hive", "alpha-hive.example.test"); @@ -1737,7 +1717,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ); unsafe { std::env::remove_var("HIVE_FORGE_URL"); @@ -1771,7 +1751,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ); let want = format!( "agent-alice.url = \"git+file://{}\"", @@ -1813,7 +1793,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ) }; @@ -1903,7 +1883,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ) }; unsafe { @@ -1975,7 +1955,7 @@ mod tests { "path:/nix/store/aaaa-nixpkgs-source", None, 8000, - &[sample_spec("alice", 9001)], + &[sample_spec("alice", false, 9001)], ) }; unsafe { @@ -2007,7 +1987,7 @@ mod tests { agent_memory_max: cap.to_owned(), ..HiveEnv::default() }; - let agents = [sample_spec("alice", 9001)]; + let agents = [sample_spec("alice", false, 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 5bc06991..03f889fb 100644 --- a/nix/agent-modules/default.nix +++ b/nix/agent-modules/default.nix @@ -1,6 +1,7 @@ # Shared scaffolding for every hyperhive harness container. -# `../templates/agent.nix`, the one entry point every agent builds from, -# imports it. +# `../templates/agent.nix` and `../templates/ruth.nix` both import +# agents use the same service unit regardless of which entry-point +# they came from. # # 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 3f51d64c..0020b582 100644 --- a/nix/agent-modules/docs.nix +++ b/nix/agent-modules/docs.nix @@ -14,8 +14,9 @@ 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`). Off by default for every agent; an agent - turns it on from its `agent.nix`. + `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`. ''; options.services.hyperhive.agent.docs.source = lib.mkOption { diff --git a/nix/agent-modules/packages.nix b/nix/agent-modules/packages.nix index fadd5af7..762cd25b 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` sets this to the flake's own package +# `nixosModules.{agent-base,ruth}` set 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 - nixosModule to `hyperhive.packages..*`; override an + `reference-docs` and `claude-plugins` trees. Wired by the flake's agent-base/ruth + nixosModules 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 82799991..f11e5c44 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 agent-base toplevel into the host system closure. + # Pull the per-container toplevels 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,6 +197,7 @@ 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 f53cc448..68687a9d 100644 --- a/nix/host-modules/hive-c0re/options.nix +++ b/nix/host-modules/hive-c0re/options.nix @@ -10,12 +10,6 @@ ... }: { - 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; @@ -98,6 +92,14 @@ 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}"; @@ -174,8 +176,8 @@ default = false; example = true; description = '' - Pre-fetch the agent-base container system closure, which every - agent builds from, into the host's /nix/store as part of this + Pre-fetch the per-container system closures (agent-base + + manager toplevels) 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. @@ -185,7 +187,7 @@ for any agent then completes in seconds instead of minutes because nothing's left to fetch. - Off by default because the toplevel is pinned to + Off by default because the toplevels are pinned to `x86_64-linux` (nixos-containers run native arch). Enabling on an aarch64 host would force nix to build the x86 closure via cross or a remote builder, which is rarely what you want. diff --git a/nix/packages/default.nix b/nix/packages/default.nix index cca594f5..3601539c 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 system closure every agent container builds from. Exposed as a package + # Pre-built per-container system closures. Exposed as packages # 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,12 +283,13 @@ in # when the meta evaluator goes to build the container. # # nixosConfigurations are pinned to x86_64-linux (nixos- - # containers only run native arch), so this toplevel is + # containers only run native arch), so these toplevels are # only useful on an x86_64-linux host — flake check across - # systems still tolerates evaluating it on aarch64 because - # it's a plain derivation, but `nix build` from a non-x86 + # systems still tolerates evaluating them on aarch64 because + # they're plain derivations, 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 new file mode 100644 index 00000000..89c19ddc --- /dev/null +++ b/nix/templates/ruth.nix @@ -0,0 +1,14 @@ +{ 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; +}