diff --git a/docs/swarm/builder.md b/docs/swarm/builder.md index 3fb34f63..3e1e2b83 100644 --- a/docs/swarm/builder.md +++ b/docs/swarm/builder.md @@ -28,8 +28,8 @@ Where it runs and how it's enabled: [`services.md`](services.md#builder). 3. The builder runs `nix build` of `nix/builder/check-flake`'s `check` output with every input overridden: - `hyperhive` and `nixpkgs`: the sources the builder's own host was - deployed with, the same two a hive on that host renders into its meta - flake (`c0re.hyperhiveFlake`, `c0re.nixpkgsFlake`). + deployed with (`c0re.hyperhiveFlake`, `c0re.nixpkgsFlake`). A hive's meta + flake pins `hyperhive` by forge URL and rev instead. - `hyperhive-docs`: the `docs/` source a hive on that host renders into its meta flake (`c0re.hyperhiveDocs`). The check sets `services.hyperhive.agent.docs.source` from it, as the meta flake does, diff --git a/flake.nix b/flake.nix index 667b22c5..63f29f93 100644 --- a/flake.nix +++ b/flake.nix @@ -129,6 +129,7 @@ assets = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.assets; xdgIcons = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.xdg-icons; hyperhiveFlake = lib.mkDefault "${sources.hyperhiveFlakeSource}"; + hyperhiveRev = lib.mkDefault (self.rev or null); # 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}"; diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 5351c473..d276fc1e 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -36,22 +36,18 @@ pub struct Coordinator { /// `get_full` for the per-card chip + side-panel viewer. See /// `build_logs.rs` for retention. pub build_logs: Arc, - /// URL of the hyperhive flake (no fragment). Inlined into per-agent - /// `flake.nix` files as `inputs.hyperhive.url`. + /// Store path of the hyperhive flake source (no fragment). Used to + /// identify the running revision (`current_flake_rev`); the meta flake + /// pins `hyperhive` by forge URL and [`Self::hyperhive_rev`] instead. pub hyperhive_flake: String, /// URL of the narrow `docs/` source (no fragment). Inlined into the /// meta `flake.nix` as `inputs.hyperhive-docs.url` and threaded to /// each agent as `services.hyperhive.agent.docs.source`. Its own store path /// so doc edits don't re-hash `hyperhive_flake`. pub hyperhive_docs_flake: String, - /// Store-path URL of the nixpkgs to wire into the meta flake as - /// `inputs.nixpkgs.url`. Populated by `--nixpkgs-flake` (set by the - /// NixOS module to `"path:${pkgs.path}"` so the meta flake always - /// tracks the same nixpkgs the host evaluated with — which is the - /// host's nixpkgs when `inputs.hyperhive.inputs.nixpkgs.follows = - /// "nixpkgs"` is set in the host flake). Empty string = legacy - /// `follows = "hyperhive/nixpkgs"` behaviour. - pub nixpkgs_flake: String, + /// Git rev of the hyperhive checkout this host runs. Pins the meta + /// flake's `hyperhive` input; `None` makes the meta render fail. + pub hyperhive_rev: Option, /// Store path of the `claude-code` build every agent runs, written /// into each per-agent flake as `services.hyperhive.agent.claudeCodePath`. Set by /// the NixOS module option @@ -203,7 +199,8 @@ pub struct HiveEnv { /// `services.hyperhive.agent.docs.source`. Separate from `hyperhive_flake` /// so a doc edit only re-locks this input, not the whole source. pub hyperhive_docs_flake: String, - pub nixpkgs_flake: String, + /// Git rev of the hyperhive checkout this host runs; see [`Coordinator::hyperhive_rev`]. + pub hyperhive_rev: Option, /// 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 `services.hyperhive.agent.claudeCodePath` — a plain string, kept @@ -228,7 +225,7 @@ impl Default for HiveEnv { Self { hyperhive_flake: "/etc/hyperhive".to_string(), hyperhive_docs_flake: String::new(), - nixpkgs_flake: String::new(), + hyperhive_rev: None, claude_code_path: None, dashboard_port: 7000, agent_cpu_quota: "200%".to_string(), @@ -405,7 +402,7 @@ impl Coordinator { let HiveEnv { hyperhive_flake, hyperhive_docs_flake, - nixpkgs_flake, + hyperhive_rev, claude_code_path, dashboard_port, agent_cpu_quota, @@ -444,7 +441,7 @@ impl Coordinator { build_logs, hyperhive_flake, hyperhive_docs_flake, - nixpkgs_flake, + hyperhive_rev, claude_code_path, dashboard_port, agent_cpu_quota, @@ -475,7 +472,7 @@ impl Coordinator { HiveEnv { hyperhive_flake: self.hyperhive_flake.clone(), hyperhive_docs_flake: self.hyperhive_docs_flake.clone(), - nixpkgs_flake: self.nixpkgs_flake.clone(), + hyperhive_rev: self.hyperhive_rev.clone(), claude_code_path: self.claude_code_path.clone(), dashboard_port: self.dashboard_port, agent_cpu_quota: self.agent_cpu_quota.clone(), diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index ddf6878d..4e4db6c9 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -78,15 +78,10 @@ enum Cmd { /// Path to the sqlite message store. #[arg(long, default_value = crate::paths::BROKER_DB)] db: PathBuf, - /// Override: URL of the hyperhive flake. Inlined into each - /// per-agent `flake.nix` as the `hyperhive` input. + /// Override: store path of the hyperhive flake source, used to + /// identify the running revision. #[arg(long)] hyperhive_flake: Option, - /// Override: store-path URL of the nixpkgs to wire into the meta - /// flake as `inputs.nixpkgs.url`. Empty = legacy - /// `follows = "hyperhive/nixpkgs"` fallback. - #[arg(long)] - nixpkgs_flake: Option, /// Override: dashboard HTTP port. #[arg(long)] dashboard_port: Option, @@ -137,7 +132,6 @@ async fn main() -> Result<()> { config, db, hyperhive_flake, - nixpkgs_flake, dashboard_port, agent_cpu_quota, agent_memory_max, @@ -161,9 +155,6 @@ async fn main() -> Result<()> { if let Some(v) = hyperhive_flake { sc.env.hyperhive_flake = v; } - if let Some(v) = nixpkgs_flake { - sc.env.nixpkgs_flake = v; - } if let Some(v) = dashboard_port { sc.env.dashboard_port = v; } diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 56392479..de18461e 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -74,7 +74,7 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> { let dir = crate::paths::meta_root(); std::fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?; - let new_flake = render_hive_flake(hive, agents); + let new_flake = render_hive_flake(hive, agents)?; let flake_path = dir.join("flake.nix"); let on_disk = std::fs::read_to_string(&flake_path).unwrap_or_default(); let initial = !dir.join(".git").exists(); @@ -327,13 +327,38 @@ pub async fn remove_permission_files() -> Result<()> { Ok(()) } +/// Pinned forge URL of the hyperhive flake: `git+/hyperhive/hyperhive.git?rev=`. +/// Errors when the host has no rev, rather than rendering an unpinned URL. +fn hyperhive_input_url(hive: &HiveEnv) -> Result { + pinned_git_url( + &crate::forge::forge_git_url("hyperhive/hyperhive"), + hive.hyperhive_rev.as_deref(), + ) +} + +/// The pure half of [`hyperhive_input_url`], testable without `HIVE_FORGE_URL`. +fn pinned_git_url(forge_git_url: &str, rev: Option<&str>) -> Result { + let rev = rev.filter(|r| !r.is_empty()).context( + "hyperhive rev unknown: the host was built from a dirty tree \ + (`self.rev` unset) and `services.hyperhive.c0re.hyperhiveRev` is not set", + )?; + Ok(format!("git+{forge_git_url}?rev={rev}")) +} + /// [`render_flake`] with its inputs taken from `hive`, as [`sync_agents`] /// renders it. -fn render_hive_flake(hive: &HiveEnv, agents: &[AgentSpec]) -> String { +fn render_hive_flake(hive: &HiveEnv, agents: &[AgentSpec]) -> Result { + Ok(render_hive_flake_at( + hive, + &hyperhive_input_url(hive)?, + agents, + )) +} + +fn render_hive_flake_at(hive: &HiveEnv, hyperhive_url: &str, agents: &[AgentSpec]) -> String { render_flake( - &hive.hyperhive_flake, + hyperhive_url, &hive.hyperhive_docs_flake, - &hive.nixpkgs_flake, hive.claude_code_path.as_deref(), hive.dashboard_port, agents, @@ -341,17 +366,15 @@ fn render_hive_flake(hive: &HiveEnv, agents: &[AgentSpec]) -> String { } fn render_flake( - hyperhive_flake: &str, + hyperhive_url: &str, docs_flake: &str, - nixpkgs_flake: &str, claude_code_path: Option<&str>, dashboard_port: u16, agents: &[AgentSpec], ) -> String { render_flake_with_lookup( - hyperhive_flake, + hyperhive_url, docs_flake, - nixpkgs_flake, claude_code_path, dashboard_port, agents, @@ -732,9 +755,8 @@ fn agent_canonical_inputs(name: &str) -> Vec<&'static str> { contiguous fmt block, splitting it would just hide the shape" )] fn render_flake_with_lookup( - hyperhive_flake: &str, + hyperhive_url: &str, docs_flake: &str, - nixpkgs_flake: &str, claude_code_path: Option<&str>, dashboard_port: u16, agents: &[AgentSpec], @@ -746,31 +768,12 @@ where use std::fmt::Write as _; let mut out = String::new(); out.push_str("{\n description = \"hyperhive deployed agents\";\n inputs = {\n"); - // `nixpkgs` is a top-level meta input with an explicit store-path URL. - // `hyperhive` then follows it via `hyperhive.inputs.nixpkgs.follows`. - // This cascades through to every agent because - // `agent-.inputs.nixpkgs.follows = "nixpkgs"` resolves to the same - // top-level node. - // - // Why an explicit `path:` URL instead of - // `nixpkgs.follows = "hyperhive/nixpkgs"`: - // meta points to hyperhive's *store path* as its flake input, so nix - // reads hyperhive's own pinned lock when evaluating that input — the - // host-level `follows` the operator set never propagates. Injecting the - // evaluated `pkgs.path` directly at nix-module evaluation time is the - // only reliable way to honour the host's channel choice. - // - // Fallback (flake arg empty): legacy `follows` wiring — used when - // hive-c0re is not built with this option wired up. - if nixpkgs_flake.is_empty() { - // Legacy path: meta defers to hyperhive's own lock. - let _ = writeln!(out, " hyperhive.url = \"{hyperhive_flake}\";"); - out.push_str(" nixpkgs.follows = \"hyperhive/nixpkgs\";\n"); - } else { - let _ = writeln!(out, " nixpkgs.url = \"{nixpkgs_flake}\";"); - let _ = writeln!(out, " hyperhive.url = \"{hyperhive_flake}\";"); - out.push_str(" hyperhive.inputs.nixpkgs.follows = \"nixpkgs\";\n"); - } + // `hyperhive` is pinned by forge URL and rev, so the flake resolves on + // any machine that can reach the forge. `nixpkgs` follows hyperhive's + // own pin, which cascades to every agent because + // `agent-.inputs.nixpkgs.follows = "nixpkgs"` resolves to this node. + let _ = writeln!(out, " hyperhive.url = \"{hyperhive_url}\";"); + out.push_str(" nixpkgs.follows = \"hyperhive/nixpkgs\";\n"); // Narrow `docs/` source as its own input so a doc edit only // re-locks THIS input instead of re-hashing the whole `hyperhive` // source. Threaded to each agent below as `services.hyperhive.agent.docs.source`. @@ -1283,28 +1286,43 @@ mod tests { } #[test] - fn render_flake_uses_explicit_nixpkgs_url_when_provided() { + fn pinned_git_url_carries_the_rev_and_refuses_without_one() { + assert_eq!( + pinned_git_url( + "https://forge.example/hyperhive/hyperhive.git", + Some("abc123") + ) + .unwrap(), + "git+https://forge.example/hyperhive/hyperhive.git?rev=abc123" + ); + for missing in [None, Some("")] { + let err = pinned_git_url("https://forge.example/x.git", missing).unwrap_err(); + assert!(err.to_string().contains("rev unknown"), "{err}"); + } + } + + #[test] + fn render_flake_pins_hyperhive_by_url_and_follows_its_nixpkgs() { let out = render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], ); - // nixpkgs is a top-level input with an explicit URL; hyperhive - // follows it. assert!( - out.contains("nixpkgs.url = \"path:/nix/store/aaaa-nixpkgs-source\""), - "expected explicit nixpkgs.url:\n{out}" + out.contains( + "hyperhive.url = \"git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd\";" + ), + "expected hyperhive pinned by forge URL:\n{out}" ); assert!( - out.contains("hyperhive.inputs.nixpkgs.follows = \"nixpkgs\""), - "expected hyperhive.inputs.nixpkgs.follows:\n{out}" + out.contains("nixpkgs.follows = \"hyperhive/nixpkgs\";"), + "expected nixpkgs to follow hyperhive:\n{out}" ); assert!( - !out.contains("nixpkgs.follows = \"hyperhive"), - "old-style follows must not appear when flake args are set:\n{out}" + !out.contains("nixpkgs.url") && !out.contains("hyperhive.inputs.nixpkgs"), + "no separate nixpkgs input:\n{out}" ); // the narrow docs source is its own non-flake input, and each // agent's docs dir resolves from it rather than services.hyperhive.agent.packages.reference-docs. @@ -1329,9 +1347,8 @@ mod tests { // Legacy / not-wired-up: empty docs_flake emits no docs input and // leaves each agent on the harness default (services.hyperhive.agent.packages.reference-docs). let out = render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -1345,9 +1362,8 @@ mod tests { #[test] fn render_flake_names_each_agent_container_after_the_agent() { let out = render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -1383,9 +1399,8 @@ mod tests { // 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", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", Some("/nix/store/cccc-claude-code-2.1.220"), 8000, &[sample_spec("alice", 9001)], @@ -1409,9 +1424,8 @@ mod tests { // in `environment.systemPackages`, which is what makes the // unpinned case self-contained). let out = render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -1422,28 +1436,6 @@ mod tests { ); } - #[test] - fn render_flake_falls_back_to_follows_when_nixpkgs_flake_empty() { - // Empty nixpkgs_flake → legacy follows behaviour (backward compat - // for any code path that can't inject pkgs.path). - let out = render_flake( - "github:example/hyperhive", - "path:/nix/store/bbbb-hyperhive-docs-source", - "", - None, - 8000, - &[sample_spec("alice", 9001)], - ); - assert!( - out.contains("nixpkgs.follows = \"hyperhive/nixpkgs\""), - "expected fallback follows:\n{out}" - ); - assert!( - !out.contains("nixpkgs.url ="), - "no explicit url should be emitted in fallback mode:\n{out}" - ); - } - #[test] fn render_flake_emits_follows_for_agents_declaring_nixpkgs() { // Stub lookup: pretend `bitburner` + `dmatrix` declare `nixpkgs` @@ -1455,9 +1447,8 @@ mod tests { } }; let out = render_flake_with_lookup( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[ @@ -1486,9 +1477,8 @@ mod tests { #[test] fn render_flake_builds_every_agent_from_agent_base() { let out = render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001), sample_spec("ruth", 9002)], @@ -1513,9 +1503,8 @@ mod tests { #[test] fn render_flake_skips_canonical_follows_when_lookup_returns_empty() { let out = render_flake_with_lookup( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -1543,9 +1532,8 @@ mod tests { ..sample_spec("droid", 9002) }; let out = render_flake_with_lookup( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001), android], @@ -1596,9 +1584,8 @@ mod tests { std::env::set_var("HIVE_FORGE_URL", "http://forge.example.test"); } let out = render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -1639,9 +1626,8 @@ mod tests { std::env::set_var(VARS[1], domain); } render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -1836,9 +1822,8 @@ mod tests { std::env::set_var("HIVE_MATRIX_URL", "http://matrix.example.test"); } let out = render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -1870,9 +1855,8 @@ mod tests { // forge being reachable. The forge `agent-configs/` repos stay the // review surface, but they are not the flake's build input. let out = render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -1886,8 +1870,8 @@ mod tests { "expected the agent input to point at the local applied mirror ({want}):\n{out}" ); assert!( - !out.contains("git+http"), - "no forge git+http URL must remain in the rendered flake:\n{out}" + !out.contains("agent-alice.url = \"git+http"), + "no forge git+http URL must remain for the agent input:\n{out}" ); } @@ -1912,9 +1896,8 @@ mod tests { let render = || { render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -2002,9 +1985,8 @@ mod tests { // SAFETY: serialised by the guard above; restored before returning. let render = || { render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -2074,9 +2056,8 @@ mod tests { // SAFETY: serialised by the guard above; restored before returning. let render = || { render_flake( - "github:example/hyperhive", + "git+https://forge.example/hyperhive/hyperhive.git?rev=0123abcd", "path:/nix/store/bbbb-hyperhive-docs-source", - "path:/nix/store/aaaa-nixpkgs-source", None, 8000, &[sample_spec("alice", 9001)], @@ -2112,8 +2093,16 @@ mod tests { ..HiveEnv::default() }; let agents = [sample_spec("alice", 9001)]; - let small = render_hive_flake(&hive("4G"), &agents); - let large = render_hive_flake(&hive("16G"), &agents); + let small = render_hive_flake_at( + &hive("4G"), + "git+https://forge.example/h.git?rev=abc", + &agents, + ); + let large = render_hive_flake_at( + &hive("16G"), + "git+https://forge.example/h.git?rev=abc", + &agents, + ); assert_eq!( small, large, "the rendered flake must not depend on the memory cap" diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index 24e20cd4..15ddae87 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -97,7 +97,7 @@ let serveConfigJson = builtins.toJSON { hyperhive_flake = cfg.hyperhiveFlake; hyperhive_docs_flake = cfg.hyperhiveDocs; - nixpkgs_flake = cfg.nixpkgsFlake; + hyperhive_rev = cfg.hyperhiveRev; # The `claude-code` every agent runs, or null for "each agent keeps # the one out of its own nixpkgs". `builtins.toJSON` serialises a # derivation as its out path (and null as null), so the package goes diff --git a/nix/host-modules/hive-c0re/options.nix b/nix/host-modules/hive-c0re/options.nix index 6abf7a29..47749ae1 100644 --- a/nix/host-modules/hive-c0re/options.nix +++ b/nix/host-modules/hive-c0re/options.nix @@ -70,12 +70,24 @@ type = lib.types.str; defaultText = lib.literalMD "the hyperhive flake's own filtered source store path"; description = '' - URL of the hyperhive flake (no fragment). Inlined into each - per-agent `flake.nix` at `inputs.hyperhive.url`. The per-agent - flake then pulls `hyperhive.nixosConfigurations.agent-base` to - build the container. Wired by `nixosModules.default` to this - flake's own filtered source — only override if you want agents - tracking a different ref. + Store path of the hyperhive flake source (no fragment). hive-c0re + reads it to tell which revision is running, and the swarm builder + builds on it. The meta flake does not use it: that pins + `hyperhive` by forge URL and `hyperhiveRev`. Wired by + `nixosModules.default` to this flake's own filtered source. + ''; + }; + hyperhiveRev = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + defaultText = lib.literalMD "the hyperhive flake's own `self.rev`"; + description = '' + Git rev of the hyperhive checkout this host runs. hive-c0re pins + the generated meta flake's `hyperhive` input to + `git+/hyperhive/hyperhive.git?rev=`, and `nixpkgs` + follows that input. Wired by `nixosModules.default` from + `self.rev`, which a dirty tree does not have; with `null`, + hive-c0re refuses to render the meta flake. ''; }; hyperhiveDocs = lib.mkOption { @@ -105,20 +117,12 @@ default = "path:${pkgs.path}"; defaultText = lib.literalMD "`\"path:\${pkgs.path}\"`"; description = '' - Store-path URL for the `nixpkgs` input in the generated meta - flake. The meta flake declares this as a top-level input and - wires `inputs.hyperhive.inputs.nixpkgs.follows = "nixpkgs"` so - every agent container evaluates with this exact nixpkgs. + Store-path URL of the nixpkgs the swarm builder's check flake + builds on. The generated meta flake does not use it: its `nixpkgs` + follows `hyperhive/nixpkgs`. - Defaults to `"path:''${pkgs.path}"` — the store path of the - nixpkgs the host NixOS module was evaluated with. When the - operator sets `inputs.hyperhive.inputs.nixpkgs.follows = - "nixpkgs"` in their host flake, `pkgs.path` resolves to the - host's own nixpkgs, so agents transparently track the same - channel as the host. - - Override to pin agents to a specific nixpkgs version regardless - of the host's channel. + Defaults to `"path:''${pkgs.path}"`, the nixpkgs the host NixOS + module was evaluated with. ''; }; preBuildAgentTemplates = lib.mkOption { @@ -340,7 +344,7 @@ description = '' The `claude-code` build every agent on this hive runs, or `null` (the default) to leave each agent on the `claude-code` from its own - nixpkgs — i.e. whatever `c0re.nixpkgsFlake` resolves to. + nixpkgs — i.e. the nixpkgs pinned by hyperhive's own `flake.lock`. This is the one binary the whole hive is built around, and it moves fast enough that a release channel routinely trails diff --git a/nix/module-eval/builder.nix b/nix/module-eval/builder.nix index 7b7f9ef1..2f84f74f 100644 --- a/nix/module-eval/builder.nix +++ b/nix/module-eval/builder.nix @@ -135,8 +135,8 @@ let && lib.elem "swarm-bao-pki.service" c.after; } { - # The check builds on the sources a hive on this host would render into - # its meta flake, so a green check means the same base. + # The check builds on the sources this host was deployed with, so a green check means + # the same hyperhive, docs and nixpkgs base. name = "the builder builds on the hyperhive, docs and nixpkgs this host's hives use"; ok = env.SWARM_BUILDER_HYPERHIVE_FLAKE == withBuilder.services.hyperhive.c0re.hyperhiveFlake