From 2ad4b431181a92d41682566f9f73f61e76f78350 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 27 Jul 2026 13:21:11 +0200 Subject: [PATCH] refactor(#2693): null, not "", for the unpinned claude-code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mara on PR #2769: "make the default null instead of special casing """. `claude_code_path` was a `String` whose empty value meant "no host-level pin". That is a sentinel doing an `Option`'s job — the same shape argus and mara already rejected on #2755's weights, and the same empty-field cruft mara called out on #2756. So it is `Option` end to end: - host module: `claudeCodePath` evaluates to `null` when `claudeCodePackage` is unset, so `serve.json` carries JSON `null` rather than `""`. - `Coordinator` + `HiveEnv`: `Option`, defaulting to `None`. - `render_flake`/`render_flake_with_lookup`: `Option<&str>`, and the emission is an `if let Some(path)` instead of an `is_empty()` guard. - agent module: `hyperhive.claudeCodePath` is `nullOr str`, default `null`. Behaviour is unchanged in both directions; only the way "unset" is spelled moves. The `builtins.hasContext` assertion still guards the pinned case (short-circuited by the null check, so an unpinned hive never evaluates it). 16/16 `meta::` tests, clippy clean, `nix fmt` no-op, `nix build .#docs` green. --- hive-c0re/src/coordinator.rs | 12 +++---- hive-c0re/src/meta.rs | 43 ++++++++++++-------------- nix/agent-modules/default.nix | 8 ++--- nix/host-modules/hive-c0re/default.nix | 4 +-- 4 files changed, 32 insertions(+), 35 deletions(-) diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index ee561c89..7029cbf3 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -65,14 +65,14 @@ pub struct Coordinator { /// 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. + /// (which resolves the package and hands us its path). `None` = 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, + pub claude_code_path: Option, /// 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. @@ -224,11 +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 + /// Store path of the `claude-code` agents run, or `None` 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 claude_code_path: Option, pub dashboard_port: u16, pub operator_pronouns: String, pub context_window_tokens: std::collections::HashMap, @@ -251,7 +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(), + claude_code_path: None, dashboard_port: 7000, operator_pronouns: "she/her".to_string(), context_window_tokens: std::collections::HashMap::from([ diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index b36183fc..167d0908 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -92,7 +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.claude_code_path.as_deref(), hive.dashboard_port, &hive.operator_pronouns, &hive.context_window_tokens, @@ -651,7 +651,7 @@ fn render_flake( hyperhive_flake: &str, docs_flake: &str, nixpkgs_flake: &str, - claude_code_path: &str, + claude_code_path: Option<&str>, dashboard_port: u16, operator_pronouns: &str, context_window_tokens: &std::collections::HashMap, @@ -940,7 +940,7 @@ fn render_flake_with_lookup( hyperhive_flake: &str, docs_flake: &str, nixpkgs_flake: &str, - claude_code_path: &str, + claude_code_path: Option<&str>, dashboard_port: u16, operator_pronouns: &str, context_window_tokens: &std::collections::HashMap, @@ -1063,12 +1063,9 @@ where // 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}\";" - ); + // `None` = no override; agents keep their own nixpkgs' `claude-code`. + if let Some(path) = claude_code_path { + let _ = writeln!(out, " hyperhive.claudeCodePath = \"{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) @@ -1580,7 +1577,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -1625,7 +1622,7 @@ mod tests { "github:example/hyperhive", "", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -1649,7 +1646,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "/nix/store/cccc-claude-code-2.1.220", + Some("/nix/store/cccc-claude-code-2.1.220"), 8000, "she/her", &std::collections::HashMap::new(), @@ -1668,7 +1665,7 @@ mod tests { #[test] fn render_flake_omits_claude_path_when_unset() { - // Empty = no host-level pin: the option is left undefined so the + // `None` = 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). @@ -1676,7 +1673,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -1697,7 +1694,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -1728,7 +1725,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -1762,7 +1759,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -1798,7 +1795,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -1837,7 +1834,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -1880,7 +1877,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -1964,7 +1961,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -2050,7 +2047,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), @@ -2093,7 +2090,7 @@ mod tests { "github:example/hyperhive", "path:/nix/store/bbbb-hyperhive-docs-source", "path:/nix/store/aaaa-nixpkgs-source", - "", + None, 8000, "she/her", &std::collections::HashMap::new(), diff --git a/nix/agent-modules/default.nix b/nix/agent-modules/default.nix index ffec918e..5b282fb0 100644 --- a/nix/agent-modules/default.nix +++ b/nix/agent-modules/default.nix @@ -62,11 +62,11 @@ }; options.hyperhive.claudeCodePath = lib.mkOption { - type = lib.types.str; - default = ""; + type = lib.types.nullOr lib.types.str; + default = null; example = "/nix/store/…-claude-code-2.1.220"; description = '' - Store path of the `claude-code` this agent runs, or `""` (the + Store path of the `claude-code` this agent runs, or `null` (the default) to use the `claude-code` from the container's own nixpkgs. @@ -212,7 +212,7 @@ ] ++ [ ( - if config.hyperhive.claudeCodePath == "" then + if config.hyperhive.claudeCodePath == null then pkgs.claude-code else # Host-pinned claude: a symlink farm around a path the diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index 06aab90e..bbcb528c 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -59,7 +59,7 @@ let fi ''; - # Store path of the `claude-code` every agent runs, or "" for "each + # Store path of the `claude-code` every agent runs, or null for "each # agent keeps the one out of its own nixpkgs". meta.rs writes it into # each agent's generated flake as a plain string literal, and the agent # module puts its `bin/` on the harness PATH. @@ -73,7 +73,7 @@ let # do not hand meta.rs the path by a route that drops it. The failure # mode is a garbage-collected `claude` and a hive that can't take a # turn, weeks after the commit that caused it. - claudeCodePath = if cfg.claudeCodePackage == null then "" else "${cfg.claudeCodePackage}"; + claudeCodePath = if cfg.claudeCodePackage == null then null else "${cfg.claudeCodePackage}"; # The `hive-c0re serve` config JSON. Keys are snake_case to match the # `ServeConfig` serde shape the daemon deserialises (the