diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index 6fe93fcf..9f313525 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -333,27 +333,30 @@ where use std::fmt::Write as _; let mut out = String::new(); out.push_str("{\n description = \"hyperhive deployed agents\";\n inputs = {\n"); - // Pin canonical nixpkgs revisions at the meta level so every input - // that pulls a nixpkgs sub-input can `follows = "nixpkgs"` and - // collapse to one shared node (closes #317). hyperhive's own - // flake.nix picks `nixos-26.05`; we mirror that here so meta and - // hyperhive don't diverge into two stable channels by default. - // Operator can override these at the meta layer to slide every - // dependent agent onto a different channel in one move. + // hyperhive's own flake.nix is the single channel-pin authority. + // meta declares `nixpkgs` + `nixpkgs-unstable` as aliases for + // hyperhive's sub-inputs via `follows`, so every agent-level + // `inputs..inputs.nixpkgs.follows = "nixpkgs"` directive + // resolves transitively to hyperhive's pin. One channel decision + // in the whole tree, no second source to drift. // - // Keeping the URL concrete (rather than `follows = "hyperhive/nixpkgs"`) - // is intentional per mara on #619: a configuring agent needs the - // url present in the rendered meta flake so it can eval against - // it — `follows` requires resolving through hyperhive, which - // breaks standalone eval workflows the agent + manager rely on. - out.push_str(" nixpkgs.url = \"github:NixOS/nixpkgs/nixos-26.05\";\n"); - out.push_str(" nixpkgs-unstable.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n"); + // Per mara via triage on #619: "all nixpkgs follow the one + // hyperhive was deployed with — if not, that's what we should + // fix." This is the fix (flip from the concrete-url shape + // #619 originally shipped with). + // + // Operators who want to slide the whole swarm onto a different + // channel do it at the host level via + // `inputs.hyperhive.inputs.nixpkgs.follows = "nixpkgs"`, which + // makes hyperhive's nixpkgs = the host's nixpkgs and cascades + // through to every agent. + // + // closes #317 invariant still satisfied — `nixpkgs` is still a + // single canonical name in the meta tree, it just resolves + // through hyperhive instead of being its own root input. let _ = writeln!(out, " hyperhive.url = \"{hyperhive_flake}\";"); - // Collapse hyperhive's own `nixpkgs` + `nixpkgs-unstable` inputs - // into meta's. Without this, hyperhive's flake.nix declarations - // become independent `nixpkgs_N` nodes in meta/flake.lock. - out.push_str(" hyperhive.inputs.nixpkgs.follows = \"nixpkgs\";\n"); - out.push_str(" hyperhive.inputs.nixpkgs-unstable.follows = \"nixpkgs-unstable\";\n"); + out.push_str(" nixpkgs.follows = \"hyperhive/nixpkgs\";\n"); + out.push_str(" nixpkgs-unstable.follows = \"hyperhive/nixpkgs-unstable\";\n"); for spec in agents { let _ = writeln!( out, @@ -569,7 +572,7 @@ mod tests { } #[test] - fn render_flake_declares_canonical_nixpkgs() { + fn render_flake_aliases_nixpkgs_to_hyperhive() { let out = render_flake( "github:example/hyperhive", 8000, @@ -577,31 +580,32 @@ mod tests { &std::collections::HashMap::new(), &[sample_spec("alice", false, 9001)], ); - // Top-level nixpkgs inputs pinned by meta — every nested - // nixpkgs input can follow these instead of resolving its own - // (closes #317). Concrete URL (not `follows = "hyperhive/nixpkgs"`) - // per mara on #619 — a configuring agent needs the URL to - // resolve when evaluating meta standalone. - assert!(out.contains("nixpkgs.url = \"github:NixOS/nixpkgs/nixos-26.05\"")); - assert!(out.contains("nixpkgs-unstable.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\"")); + // Meta's `nixpkgs` + `nixpkgs-unstable` are aliases for + // hyperhive's sub-inputs. Single channel-pin authority: + // hyperhive's own flake.nix. Per mara via triage on #619: + // "all nixpkgs follow the one hyperhive was deployed with." + assert!( + out.contains("nixpkgs.follows = \"hyperhive/nixpkgs\""), + "missing nixpkgs follows alias:\n{out}" + ); + assert!( + out.contains("nixpkgs-unstable.follows = \"hyperhive/nixpkgs-unstable\""), + "missing nixpkgs-unstable follows alias:\n{out}" + ); + // And conversely: no literal channel ref baked into meta. If + // this fails, someone reintroduced a hardcoded ref — see + // #619 follow-up rationale for why that drifts. + assert!( + !out.contains("nixpkgs.url ="), + "no literal `nixpkgs.url` should be emitted (hyperhive owns the pin):\n{out}" + ); } - #[test] - fn render_flake_collapses_hyperhive_nixpkgs_via_follows() { - let out = render_flake( - "github:example/hyperhive", - 8000, - "she/her", - &std::collections::HashMap::new(), - &[], - ); - // hyperhive's own `nixpkgs` + `nixpkgs-unstable` declarations - // get redirected at meta's. Without these, meta/flake.lock - // ends up with separate `nixpkgs_N` nodes for hyperhive's - // copy (the pre-#317 status quo). - assert!(out.contains("hyperhive.inputs.nixpkgs.follows = \"nixpkgs\"")); - assert!(out.contains("hyperhive.inputs.nixpkgs-unstable.follows = \"nixpkgs-unstable\"")); - } + // `render_flake_collapses_hyperhive_nixpkgs_via_follows` dropped: + // with meta's `nixpkgs.follows = "hyperhive/nixpkgs"`, there's no + // separate meta-level nixpkgs to collapse hyperhive's into. The + // redirect goes the other way now (the alias test above covers + // the new invariant). #[test] fn render_flake_emits_follows_for_agents_declaring_nixpkgs() {