Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
16 changed files with 99 additions and 89 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.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`.

View file

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

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

View file

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

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

View file

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

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` 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);

View file

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

View file

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

View file

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

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` 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.<system>.*`; override an
`reference-docs` and `claude-plugins` trees. Wired by the flake's agent-base/ruth
nixosModules 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 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;

View file

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

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

14
nix/templates/ruth.nix Normal file
View file

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