feat(#2693): let the operator pin the claude-code every agent runs
Agents run whatever `claude-code` the meta flake's `nixpkgs` resolves to, and that is normally a release channel. This one package moves fast enough that stable trails unstable by weeks — 26.05 is on 2.1.187 while unstable carries 2.1.220 — and an agent cannot fix it for itself: it only ever sees the single nixpkgs hive-c0re injects, so an `agent.nix` has no other tree to reach for. New host option `services.hyperhive.c0re.claudeCodePackage` takes the package directly and rides the existing `hyperhiveDocs` threading path — serveConfigJson -> HiveEnv -> render_flake — to reach each agent as `hyperhive.claudeCodePath`. Null (the default) is today's behaviour. What travels is the store *path*, as a plain string literal, not a flake input: containers share the host's `/nix/store`, so the build is already reachable inside them with its whole closure and has nothing to travel. An input would be worse than useless — a `path:/nix/store/<pkg>` input is re-copied as a reference-less `-source`, which strips exactly the closure the binary needs. The catch is that a path written into a generated flake is text, so nothing in the container's closure keeps the binary alive. The host does that instead, and gets it for free: the package is interpolated into `/etc/hyperhive/serve.json`, `builtins.toJSON` preserves string context, so the /etc entry references it and the system closure gc-roots it for as long as that generation is the one the agents were rendered from. An assertion pins that property, because losing the context is invisible at eval and at deploy — it would surface only as every agent failing to spawn `claude` whenever the next gc ran. Container side wraps the path in a symlink farm rather than putting it on PATH directly: `systemd.services.<name>.path` and `environment.systemPackages` both coerce a store-path *string* through `lib.toDerivation`, i.e. `builtins.storePath`, which pure evaluation rejects. Interpolating the path into a builder is just text and evaluates anywhere. `claude-code` drops out of systemPackages when a pin is set, so there is exactly one claude in the container. Refs #2693
This commit is contained in:
parent
ca7146e4f0
commit
b08176f089
7 changed files with 281 additions and 1 deletions
|
|
@ -62,6 +62,17 @@ pub struct Coordinator {
|
|||
/// "nixpkgs"` is set in the host flake). Empty string = legacy
|
||||
/// `follows = "hyperhive/nixpkgs"` behaviour.
|
||||
pub nixpkgs_flake: String,
|
||||
/// Store path of the `claude-code` build every agent runs, written
|
||||
/// into each per-agent flake as `hyperhive.claudeCodePath`. Set by
|
||||
/// the NixOS module option `services.hyperhive.c0re.claudeCodePackage`
|
||||
/// (which resolves the package and hands us its path). Empty string =
|
||||
/// every agent keeps the `claude-code` from its own nixpkgs.
|
||||
///
|
||||
/// A path rather than a flake input because containers share the
|
||||
/// host's `/nix/store`: the binary is already reachable inside them,
|
||||
/// closure and all. The host module is what keeps it from being
|
||||
/// garbage-collected — see that option.
|
||||
pub claude_code_path: String,
|
||||
/// TCP port the host's hive-c0re dashboard listens on. Inlined into
|
||||
/// each per-agent flake so the agent's web UI can build the right
|
||||
/// rebuild-button URL pointing back at the dashboard.
|
||||
|
|
@ -213,6 +224,11 @@ pub struct HiveEnv {
|
|||
/// so a doc edit only re-locks this input, not the whole source.
|
||||
pub hyperhive_docs_flake: String,
|
||||
pub nixpkgs_flake: String,
|
||||
/// Store path of the `claude-code` agents run, or empty for "each
|
||||
/// agent keeps the one out of its own nixpkgs". Travels into the
|
||||
/// container as `hyperhive.claudeCodePath` — a plain string, kept
|
||||
/// alive host-side by the module that resolved it.
|
||||
pub claude_code_path: String,
|
||||
pub dashboard_port: u16,
|
||||
pub operator_pronouns: String,
|
||||
pub context_window_tokens: std::collections::HashMap<String, u64>,
|
||||
|
|
@ -235,6 +251,7 @@ impl Default for HiveEnv {
|
|||
hyperhive_flake: "/etc/hyperhive".to_string(),
|
||||
hyperhive_docs_flake: String::new(),
|
||||
nixpkgs_flake: String::new(),
|
||||
claude_code_path: String::new(),
|
||||
dashboard_port: 7000,
|
||||
operator_pronouns: "she/her".to_string(),
|
||||
context_window_tokens: std::collections::HashMap::from([
|
||||
|
|
@ -474,6 +491,7 @@ impl Coordinator {
|
|||
hyperhive_flake,
|
||||
hyperhive_docs_flake,
|
||||
nixpkgs_flake,
|
||||
claude_code_path,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
|
|
@ -520,6 +538,7 @@ impl Coordinator {
|
|||
hyperhive_flake,
|
||||
hyperhive_docs_flake,
|
||||
nixpkgs_flake,
|
||||
claude_code_path,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
|
|
@ -553,6 +572,7 @@ impl Coordinator {
|
|||
hyperhive_flake: self.hyperhive_flake.clone(),
|
||||
hyperhive_docs_flake: self.hyperhive_docs_flake.clone(),
|
||||
nixpkgs_flake: self.nixpkgs_flake.clone(),
|
||||
claude_code_path: self.claude_code_path.clone(),
|
||||
dashboard_port: self.dashboard_port,
|
||||
operator_pronouns: self.operator_pronouns.clone(),
|
||||
context_window_tokens: self.context_window_tokens.clone(),
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> {
|
|||
&hive.hyperhive_flake,
|
||||
&hive.hyperhive_docs_flake,
|
||||
&hive.nixpkgs_flake,
|
||||
&hive.claude_code_path,
|
||||
hive.dashboard_port,
|
||||
&hive.operator_pronouns,
|
||||
&hive.context_window_tokens,
|
||||
|
|
@ -650,6 +651,7 @@ fn render_flake(
|
|||
hyperhive_flake: &str,
|
||||
docs_flake: &str,
|
||||
nixpkgs_flake: &str,
|
||||
claude_code_path: &str,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
|
|
@ -660,6 +662,7 @@ fn render_flake(
|
|||
hyperhive_flake,
|
||||
docs_flake,
|
||||
nixpkgs_flake,
|
||||
claude_code_path,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
|
|
@ -937,6 +940,7 @@ fn render_flake_with_lookup<F>(
|
|||
hyperhive_flake: &str,
|
||||
docs_flake: &str,
|
||||
nixpkgs_flake: &str,
|
||||
claude_code_path: &str,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
|
|
@ -1051,6 +1055,21 @@ where
|
|||
if !docs_flake.is_empty() {
|
||||
out.push_str(" hyperhive.docs.source = inputs.\"hyperhive-docs\".outPath;\n");
|
||||
}
|
||||
// The `claude-code` agents run, as a bare store path rather than a
|
||||
// flake input: containers share the host's `/nix/store`, so the
|
||||
// binary is already there with its closure and has nothing to
|
||||
// travel. A string literal is also the only shape that evaluates —
|
||||
// `lib.types.package` on a bare path runs `builtins.storePath`,
|
||||
// which pure eval rejects. The agent module puts its `bin/` on the
|
||||
// harness PATH; the host module holds the gc root, since a path
|
||||
// spelled out here is text and references nothing.
|
||||
// Empty = no override; agents keep their own nixpkgs' `claude-code`.
|
||||
if !claude_code_path.is_empty() {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" hyperhive.claudeCodePath = \"{claude_code_path}\";"
|
||||
);
|
||||
}
|
||||
// CA trust: embed every hive-trusted CA so each agent validates them at
|
||||
// build time. The list is the hive's own self-signed CA (when active)
|
||||
// plus every peer-hive root CA (`swarm.peers.<d>.caCert`) — a peer CA is
|
||||
|
|
@ -1561,6 +1580,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1605,6 +1625,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1617,6 +1638,57 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_flake_pins_claude_path_without_adding_an_input() {
|
||||
// The host-pinned claude travels as a bare store path assigned to
|
||||
// an option — deliberately NOT as a flake input. Containers share
|
||||
// the host store, so the build is already reachable; making it an
|
||||
// input would re-copy it as a reference-less `-source` and strip
|
||||
// the closure the binary actually needs.
|
||||
let out = render_flake(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"/nix/store/cccc-claude-code-2.1.220",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
out.contains("hyperhive.claudeCodePath = \"/nix/store/cccc-claude-code-2.1.220\";"),
|
||||
"claude path assigned as a plain string literal:\n{out}"
|
||||
);
|
||||
assert!(
|
||||
!out.contains("claude-code-2.1.220\".url"),
|
||||
"the pinned claude must not become a flake input:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_flake_omits_claude_path_when_unset() {
|
||||
// Empty = no host-level pin: the option is left undefined so the
|
||||
// agent module keeps its own nixpkgs' `claude-code` (and keeps it
|
||||
// in `environment.systemPackages`, which is what makes the
|
||||
// unpinned case self-contained).
|
||||
let out = render_flake(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
!out.contains("claudeCodePath"),
|
||||
"no claude assignment when unpinned:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_flake_falls_back_to_follows_when_nixpkgs_flake_empty() {
|
||||
// Empty nixpkgs_flake → legacy follows behaviour (backward compat
|
||||
|
|
@ -1625,6 +1697,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1655,6 +1728,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1688,6 +1762,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1723,6 +1798,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1761,6 +1837,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1803,6 +1880,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1886,6 +1964,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -1971,6 +2050,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -2013,6 +2093,7 @@ mod tests {
|
|||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue