Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
Author SHA1 Message Date
atlas
8c948434ae say one agent-base closure in the preBuildAgentTemplates docs 2026-10-17 09:24:52 +02:00
atlas
1b60084bf0 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
2026-10-17 09:24:52 +02:00
16 changed files with 89 additions and 99 deletions

View file

@ -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`.

View file

@ -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

View file

@ -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 per-container system closures
(`agent-base` + manager toplevels) into `/nix/store`, instead of
build to pre-fetch the `agent-base` container system closure, which every
agent builds from, 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 toplevels to `x86_64-linux`. Enabling on an
**Note**: the meta flake pins the toplevel 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.

View file

@ -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

View file

@ -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

View file

@ -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; });

View file

@ -530,7 +530,7 @@ async fn set_nspawn_flags(
// Claude credentials land at `/home/<agent>/.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);

View file

@ -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<Vec<crate::meta::A
.filter_map(|c| {
let name = c.strip_prefix(AGENT_PREFIX)?.to_owned();
Some(crate::meta::AgentSpec {
is_manager: name == MANAGER_NAME,
port: agent_web_port(&name),
name,
})
@ -337,7 +333,6 @@ async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<crate::meta::A
&& !out.iter().any(|a| a.name == extra)
{
out.push(crate::meta::AgentSpec {
is_manager: extra == MANAGER_NAME,
port: agent_web_port(extra),
name: extra.to_owned(),
});

View file

@ -41,7 +41,6 @@ pub const CONTAINER_MANAGER_META_MOUNT: &str = "/meta";
#[derive(Debug, Clone)]
pub struct AgentSpec {
pub name: String,
pub is_manager: bool,
pub port: u16,
}
@ -804,13 +803,11 @@ where
out.push_str(" };\n outputs =\n { self, hyperhive, ... }@inputs:\n let\n");
let _ = writeln!(
out,
" dashboardPort = {dashboard_port};\n mkAgent = {{ name, isManager, port }}:"
" dashboardPort = {dashboard_port};\n mkAgent = {{ name, port }}:"
);
out.push_str(
r#" let
base = if isManager
then hyperhive.nixosConfigurations.ruth
else hyperhive.nixosConfigurations.agent-base;
base = hyperhive.nixosConfigurations.agent-base;
input = inputs."agent-${name}";
service = "hive-agent";
in
@ -1013,11 +1010,8 @@ where
for spec in agents {
let _ = writeln!(
out,
" {} = mkAgent {{ name = \"{}\"; isManager = {}; port = {}; }};",
spec.name,
spec.name,
if spec.is_manager { "true" } else { "false" },
spec.port,
" {} = mkAgent {{ name = \"{}\"; port = {}; }};",
spec.name, spec.name, spec.port,
);
}
out.push_str(" };\n };\n}\n");
@ -1229,10 +1223,9 @@ mod tests {
);
}
fn sample_spec(name: &str, is_manager: bool, port: u16) -> 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!(

View file

@ -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

View file

@ -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 {

View file

@ -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.<system>.*`; override an
`reference-docs` and `claude-plugins` trees. Wired by the flake's agent-base
nixosModule to `hyperhive.packages.<system>.*`; override an
individual key per-agent to swap in a patched binary.
'';
};

View file

@ -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;

View file

@ -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.
@ -187,7 +185,7 @@
for any agent then completes in seconds instead of minutes
because nothing's left to fetch.
Off by default because the toplevels are pinned to
Off by default because the toplevel is 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.

View file

@ -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;

View file

@ -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;
}