feat(#1003): inject pkgs.path into meta flake as explicit nixpkgs.url
meta flake was using `nixpkgs.follows = "hyperhive/nixpkgs"` but
`hyperhive` is a store-path input, so nix resolves hyperhive's own
pinned lock rather than the host's follows-substituted version.
When an operator sets `inputs.hyperhive.inputs.nixpkgs.follows =
"nixpkgs"` in their host flake, the meta flake was silently ignoring
it and using hyperhive's pinned nixpkgs instead.
Fix: hive-c0re.nix injects `--nixpkgs-flake path:${pkgs.path}` into
the daemon's ExecStart. `pkgs` IS the host's nixpkgs when follows is
set; otherwise it's hyperhive's own pin — so the meta flake gets the
right nixpkgs in both cases. render_flake emits `nixpkgs.url = "..."`
(explicit) when nixpkgs_flake is non-empty, falling back to the old
`follows` form when empty for backward compat.
This commit is contained in:
parent
6b6289c191
commit
fe5a41288d
9 changed files with 97 additions and 37 deletions
|
|
@ -52,6 +52,7 @@ pub fn meta_dir() -> PathBuf {
|
|||
#[allow(dead_code, clippy::implicit_hasher)] // first caller lands in a later commit
|
||||
pub async fn sync_agents(
|
||||
hyperhive_flake: &str,
|
||||
nixpkgs_flake: &str,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
|
|
@ -63,6 +64,7 @@ pub async fn sync_agents(
|
|||
|
||||
let new_flake = render_flake(
|
||||
hyperhive_flake,
|
||||
nixpkgs_flake,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
|
|
@ -300,6 +302,7 @@ pub async fn lock_update_hyperhive() -> Result<()> {
|
|||
|
||||
fn render_flake(
|
||||
hyperhive_flake: &str,
|
||||
nixpkgs_flake: &str,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
|
|
@ -307,6 +310,7 @@ fn render_flake(
|
|||
) -> String {
|
||||
render_flake_with_lookup(
|
||||
hyperhive_flake,
|
||||
nixpkgs_flake,
|
||||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
|
|
@ -392,6 +396,7 @@ fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
|
|||
)]
|
||||
fn render_flake_with_lookup<F>(
|
||||
hyperhive_flake: &str,
|
||||
nixpkgs_flake: &str,
|
||||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
|
|
@ -404,27 +409,30 @@ where
|
|||
use std::fmt::Write as _;
|
||||
let mut out = String::new();
|
||||
out.push_str("{\n description = \"hyperhive deployed agents\";\n inputs = {\n");
|
||||
// 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.<X>.inputs.nixpkgs.follows = "nixpkgs"` directive
|
||||
// resolves transitively to hyperhive's pin. One channel decision
|
||||
// in the whole tree, no second source to drift.
|
||||
// `hyperhive` is the single channel-pin authority. `nixpkgs` is wired
|
||||
// to the exact nixpkgs store path hive-c0re was evaluated with — which
|
||||
// is the host's nixpkgs when the operator sets
|
||||
// `inputs.hyperhive.inputs.nixpkgs.follows = "nixpkgs"` in their host
|
||||
// flake, or hyperhive's own pin otherwise. Using an explicit `path:`
|
||||
// URL instead of `follows = "hyperhive/nixpkgs"` is essential here:
|
||||
// meta points to hyperhive's store path, so nix would otherwise
|
||||
// resolve hyperhive's own pinned lock rather than the host-substituted
|
||||
// version that `follows` produced.
|
||||
//
|
||||
// All nixpkgs follow the one hyperhive was deployed with —
|
||||
// anything else would drift.
|
||||
// `nixpkgs-unstable` still follows hyperhive (claude-code lives there;
|
||||
// no same-channel requirement from the host side).
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// `nixpkgs` is still a single canonical name in the meta tree,
|
||||
// it just resolves through hyperhive instead of being its own
|
||||
// root input.
|
||||
// `nixpkgs` is the single canonical name in the meta tree — every
|
||||
// agent that declares it in its own `flake.nix` gets a
|
||||
// `agent-<n>.inputs.nixpkgs.follows = "nixpkgs"` directive that
|
||||
// collapses all per-agent nixpkgs nodes into one.
|
||||
let _ = writeln!(out, " hyperhive.url = \"{hyperhive_flake}\";");
|
||||
out.push_str(" nixpkgs.follows = \"hyperhive/nixpkgs\";\n");
|
||||
if nixpkgs_flake.is_empty() {
|
||||
// Fallback: legacy behaviour when nixpkgs_flake not injected.
|
||||
out.push_str(" nixpkgs.follows = \"hyperhive/nixpkgs\";\n");
|
||||
} else {
|
||||
let _ = writeln!(out, " nixpkgs.url = \"{nixpkgs_flake}\";");
|
||||
}
|
||||
out.push_str(" nixpkgs-unstable.follows = \"hyperhive/nixpkgs-unstable\";\n");
|
||||
for spec in agents {
|
||||
let _ = writeln!(
|
||||
|
|
@ -687,40 +695,55 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn render_flake_aliases_nixpkgs_to_hyperhive() {
|
||||
fn render_flake_uses_explicit_nixpkgs_url_when_provided() {
|
||||
let out = render_flake(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
// Meta's `nixpkgs` + `nixpkgs-unstable` are aliases for
|
||||
// hyperhive's sub-inputs. Single channel-pin authority:
|
||||
// hyperhive's own flake.nix. All nixpkgs follow the one
|
||||
// hyperhive was deployed with.
|
||||
// Explicit nixpkgs_flake → meta uses `nixpkgs.url`, NOT follows.
|
||||
// This is the path taken when hive-c0re.nix injects `pkgs.path`:
|
||||
// the URL is the exact nixpkgs evaluated with the host's nixpkgs
|
||||
// (which IS the host's version when `follows` is set).
|
||||
assert!(
|
||||
out.contains("nixpkgs.follows = \"hyperhive/nixpkgs\""),
|
||||
"missing nixpkgs follows alias:\n{out}"
|
||||
out.contains("nixpkgs.url = \"path:/nix/store/aaaa-nixpkgs-source\""),
|
||||
"expected explicit nixpkgs.url:\n{out}"
|
||||
);
|
||||
assert!(
|
||||
!out.contains("nixpkgs.follows"),
|
||||
"follows must not appear when nixpkgs_flake is set:\n{out}"
|
||||
);
|
||||
// nixpkgs-unstable still follows hyperhive (claude-code lives there).
|
||||
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 — would
|
||||
// drift away from hyperhive's pin.
|
||||
assert!(
|
||||
!out.contains("nixpkgs.url ="),
|
||||
"no literal `nixpkgs.url` should be emitted (hyperhive owns the pin):\n{out}"
|
||||
"missing nixpkgs-unstable follows:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
// `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_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",
|
||||
"",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
&[sample_spec("alice", false, 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() {
|
||||
|
|
@ -735,6 +758,7 @@ mod tests {
|
|||
};
|
||||
let out = render_flake_with_lookup(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
@ -768,6 +792,7 @@ mod tests {
|
|||
fn render_flake_skips_canonical_follows_when_lookup_returns_empty() {
|
||||
let out = render_flake_with_lookup(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue