refactor(#1003): nixpkgs + nixpkgs-unstable as top-level meta inputs

Per mara's direction: both nixpkgs and nixpkgs-unstable are now
top-level meta flake inputs with explicit store-path URLs.  Hyperhive
follows them rather than the other way around:

  inputs.nixpkgs.url            = "path:${pkgs.path}";
  inputs.nixpkgs-unstable.url   = "path:${nixpkgs-unstable}";
  inputs.hyperhive.url          = "...";
  inputs.hyperhive.inputs.nixpkgs.follows            = "nixpkgs";
  inputs.hyperhive.inputs.nixpkgs-unstable.follows   = "nixpkgs-unstable";

New NixOS host options (auto-set at build time, overridable):
  services.hyperhive.c0re.nixpkgsFlake
    default: "path:${pkgs.path}" — host's evaluated nixpkgs.
  services.hyperhive.c0re.nixpkgsUnstableFlake
    default: "path:${nixpkgs-unstable}" from hyperhive's flake.nix —
    the channel that carries claude-code.  Operators can override to
    track a different unstable snapshot.

Legacy fallback (both args empty) preserved for backward compat.
Two new Rust tests cover the full-URL and fallback paths.
This commit is contained in:
atlas 2026-06-01 22:44:21 +02:00
commit db50da570a
10 changed files with 124 additions and 28 deletions

View file

@ -225,6 +225,7 @@ pub async fn run_approval_spawn(
&approval.agent,
&coord.hyperhive_flake,
&coord.nixpkgs_flake,
&coord.nixpkgs_unstable_flake,
&agent_dir,
&proposed_dir,
&applied_dir,
@ -545,6 +546,7 @@ async fn run_apply_commit(
if let Err(e) = crate::meta::sync_agents(
&coord.hyperhive_flake,
&coord.nixpkgs_flake,
&coord.nixpkgs_unstable_flake,
coord.dashboard_port,
&coord.operator_pronouns,
&coord.context_window_tokens,
@ -716,6 +718,7 @@ async fn sync_meta_after_lifecycle(coord: &Coordinator) -> Result<()> {
crate::meta::sync_agents(
&coord.hyperhive_flake,
&coord.nixpkgs_flake,
&coord.nixpkgs_unstable_flake,
coord.dashboard_port,
&coord.operator_pronouns,
&coord.context_window_tokens,

View file

@ -89,6 +89,7 @@ pub async fn rebuild_agent(
name,
&coord.hyperhive_flake,
&coord.nixpkgs_flake,
&coord.nixpkgs_unstable_flake,
&agent_dir,
&applied_dir,
&claude_dir,
@ -192,6 +193,7 @@ pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> {
MANAGER_NAME,
&coord.hyperhive_flake,
&coord.nixpkgs_flake,
&coord.nixpkgs_unstable_flake,
&runtime,
&proposed,
&applied,

View file

@ -59,6 +59,11 @@ pub struct Coordinator {
/// "nixpkgs"` is set in the host flake). Empty string = legacy
/// `follows = "hyperhive/nixpkgs"` behaviour.
pub nixpkgs_flake: String,
/// Store-path URL for `nixpkgs-unstable` to wire as a top-level meta
/// flake input. Hyperhive's `inputs.nixpkgs-unstable` then follows it.
/// Set via `--nixpkgs-unstable-flake` from `hive-c0re.nix`. Empty string
/// falls back to the legacy `follows = "hyperhive/nixpkgs-unstable"`.
pub nixpkgs_unstable_flake: 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.
@ -220,6 +225,7 @@ impl Coordinator {
db_path: &Path,
hyperhive_flake: String,
nixpkgs_flake: String,
nixpkgs_unstable_flake: String,
dashboard_port: u16,
operator_pronouns: String,
context_window_tokens: std::collections::HashMap<String, u64>,
@ -252,6 +258,7 @@ impl Coordinator {
build_logs,
hyperhive_flake,
nixpkgs_flake,
nixpkgs_unstable_flake,
dashboard_port,
operator_pronouns,
context_window_tokens,

View file

@ -186,6 +186,7 @@ pub async fn spawn(
name: &str,
hyperhive_flake: &str,
nixpkgs_flake: &str,
nixpkgs_unstable_flake: &str,
agent_dir: &Path,
proposed_dir: &Path,
applied_dir: &Path,
@ -213,6 +214,7 @@ pub async fn spawn(
crate::meta::sync_agents(
hyperhive_flake,
nixpkgs_flake,
nixpkgs_unstable_flake,
dashboard_port,
operator_pronouns,
context_window_tokens,
@ -352,6 +354,7 @@ pub async fn rebuild(
name: &str,
hyperhive_flake: &str,
nixpkgs_flake: &str,
nixpkgs_unstable_flake: &str,
agent_dir: &Path,
applied_dir: &Path,
claude_dir: &Path,
@ -370,6 +373,7 @@ pub async fn rebuild(
crate::meta::sync_agents(
hyperhive_flake,
nixpkgs_flake,
nixpkgs_unstable_flake,
dashboard_port,
operator_pronouns,
context_window_tokens,

View file

@ -45,6 +45,14 @@ enum Cmd {
/// `follows = "hyperhive/nixpkgs"` fallback.
#[arg(long, default_value = "")]
nixpkgs_flake: String,
/// Store-path URL of the nixpkgs-unstable to wire into the meta
/// flake as `inputs.nixpkgs-unstable.url`. Hyperhive's
/// `inputs.nixpkgs-unstable` then follows this top-level input.
/// Set by the NixOS module; defaults to the hyperhive flake's own
/// nixpkgs-unstable store path. Empty = legacy
/// `follows = "hyperhive/nixpkgs-unstable"` fallback.
#[arg(long, default_value = "")]
nixpkgs_unstable_flake: String,
/// Path to the sqlite message store.
#[arg(long, default_value = "/var/lib/hyperhive/broker.sqlite")]
db: PathBuf,
@ -128,6 +136,7 @@ async fn main() -> Result<()> {
Cmd::Serve {
hyperhive_flake,
nixpkgs_flake,
nixpkgs_unstable_flake,
db,
dashboard_port,
operator_pronouns,
@ -136,6 +145,7 @@ async fn main() -> Result<()> {
cmd_serve(
hyperhive_flake,
nixpkgs_flake,
nixpkgs_unstable_flake,
db,
dashboard_port,
operator_pronouns,
@ -184,6 +194,7 @@ async fn main() -> Result<()> {
async fn cmd_serve(
hyperhive_flake: String,
nixpkgs_flake: String,
nixpkgs_unstable_flake: String,
db: std::path::PathBuf,
dashboard_port: u16,
operator_pronouns: String,
@ -196,6 +207,7 @@ async fn cmd_serve(
&db,
hyperhive_flake,
nixpkgs_flake,
nixpkgs_unstable_flake,
dashboard_port,
operator_pronouns,
cwt,

View file

@ -53,6 +53,7 @@ pub fn meta_dir() -> PathBuf {
pub async fn sync_agents(
hyperhive_flake: &str,
nixpkgs_flake: &str,
nixpkgs_unstable_flake: &str,
dashboard_port: u16,
operator_pronouns: &str,
context_window_tokens: &std::collections::HashMap<String, u64>,
@ -65,6 +66,7 @@ pub async fn sync_agents(
let new_flake = render_flake(
hyperhive_flake,
nixpkgs_flake,
nixpkgs_unstable_flake,
dashboard_port,
operator_pronouns,
context_window_tokens,
@ -303,6 +305,7 @@ pub async fn lock_update_hyperhive() -> Result<()> {
fn render_flake(
hyperhive_flake: &str,
nixpkgs_flake: &str,
nixpkgs_unstable_flake: &str,
dashboard_port: u16,
operator_pronouns: &str,
context_window_tokens: &std::collections::HashMap<String, u64>,
@ -311,6 +314,7 @@ fn render_flake(
render_flake_with_lookup(
hyperhive_flake,
nixpkgs_flake,
nixpkgs_unstable_flake,
dashboard_port,
operator_pronouns,
context_window_tokens,
@ -397,6 +401,7 @@ fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
fn render_flake_with_lookup<F>(
hyperhive_flake: &str,
nixpkgs_flake: &str,
nixpkgs_unstable_flake: &str,
dashboard_port: u16,
operator_pronouns: &str,
context_window_tokens: &std::collections::HashMap<String, u64>,
@ -409,31 +414,35 @@ where
use std::fmt::Write as _;
let mut out = String::new();
out.push_str("{\n description = \"hyperhive deployed agents\";\n inputs = {\n");
// `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.
// `nixpkgs` + `nixpkgs-unstable` are top-level meta inputs with explicit
// store-path URLs. `hyperhive` then follows them via
// `hyperhive.inputs.*.follows`. This cascades through to every agent
// because `agent-<n>.inputs.nixpkgs.follows = "nixpkgs"` resolves to
// the same top-level node.
//
// `nixpkgs-unstable` still follows hyperhive (claude-code lives there;
// no same-channel requirement from the host side).
// Why explicit `path:` URLs 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` / nixpkgs-unstable path directly at nix-module
// evaluation time is the only reliable way to honour the host's channel
// choice.
//
// `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}\";");
// Fallback (both flake args empty): legacy `follows` wiring — used when
// hive-c0re is not built with this option wired up.
if nixpkgs_flake.is_empty() {
// Fallback: legacy behaviour when nixpkgs_flake not injected.
// Legacy path: meta defers to hyperhive's own lock.
let _ = writeln!(out, " hyperhive.url = \"{hyperhive_flake}\";");
out.push_str(" nixpkgs.follows = \"hyperhive/nixpkgs\";\n");
out.push_str(" nixpkgs-unstable.follows = \"hyperhive/nixpkgs-unstable\";\n");
} else {
let _ = writeln!(out, " nixpkgs.url = \"{nixpkgs_flake}\";");
let _ = writeln!(out, " nixpkgs-unstable.url = \"{nixpkgs_unstable_flake}\";");
let _ = writeln!(out, " hyperhive.url = \"{hyperhive_flake}\";");
out.push_str(" hyperhive.inputs.nixpkgs.follows = \"nixpkgs\";\n");
out.push_str(" hyperhive.inputs.nixpkgs-unstable.follows = \"nixpkgs-unstable\";\n");
}
out.push_str(" nixpkgs-unstable.follows = \"hyperhive/nixpkgs-unstable\";\n");
for spec in agents {
let _ = writeln!(
out,
@ -699,27 +708,33 @@ mod tests {
let out = render_flake(
"github:example/hyperhive",
"path:/nix/store/aaaa-nixpkgs-source",
"path:/nix/store/bbbb-nixpkgs-unstable-source",
8000,
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
);
// 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).
// Both nixpkgs + nixpkgs-unstable are top-level inputs with
// explicit URLs; hyperhive follows them.
assert!(
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}"
out.contains("nixpkgs-unstable.url = \"path:/nix/store/bbbb-nixpkgs-unstable-source\""),
"expected explicit nixpkgs-unstable.url:\n{out}"
);
// nixpkgs-unstable still follows hyperhive (claude-code lives there).
assert!(
out.contains("nixpkgs-unstable.follows = \"hyperhive/nixpkgs-unstable\""),
"missing nixpkgs-unstable follows:\n{out}"
out.contains("hyperhive.inputs.nixpkgs.follows = \"nixpkgs\""),
"expected hyperhive.inputs.nixpkgs.follows:\n{out}"
);
assert!(
out.contains("hyperhive.inputs.nixpkgs-unstable.follows = \"nixpkgs-unstable\""),
"expected hyperhive.inputs.nixpkgs-unstable.follows:\n{out}"
);
assert!(
!out.contains("nixpkgs.follows = \"hyperhive"),
"old-style follows must not appear when flake args are set:\n{out}"
);
}
@ -730,6 +745,7 @@ mod tests {
let out = render_flake(
"github:example/hyperhive",
"",
"",
8000,
"she/her",
&std::collections::HashMap::new(),
@ -739,6 +755,10 @@ mod tests {
out.contains("nixpkgs.follows = \"hyperhive/nixpkgs\""),
"expected fallback follows:\n{out}"
);
assert!(
out.contains("nixpkgs-unstable.follows = \"hyperhive/nixpkgs-unstable\""),
"expected fallback unstable follows:\n{out}"
);
assert!(
!out.contains("nixpkgs.url ="),
"no explicit url should be emitted in fallback mode:\n{out}"
@ -759,6 +779,7 @@ mod tests {
let out = render_flake_with_lookup(
"github:example/hyperhive",
"path:/nix/store/aaaa-nixpkgs-source",
"path:/nix/store/bbbb-nixpkgs-unstable-source",
8000,
"she/her",
&std::collections::HashMap::new(),
@ -793,6 +814,7 @@ mod tests {
let out = render_flake_with_lookup(
"github:example/hyperhive",
"path:/nix/store/aaaa-nixpkgs-source",
"path:/nix/store/bbbb-nixpkgs-unstable-source",
8000,
"she/her",
&std::collections::HashMap::new(),

View file

@ -79,6 +79,7 @@ pub async fn run(coord: &Arc<Coordinator>) -> Result<()> {
if let Err(e) = meta::sync_agents(
&coord.hyperhive_flake,
&coord.nixpkgs_flake,
&coord.nixpkgs_unstable_flake,
coord.dashboard_port,
&coord.operator_pronouns,
&coord.context_window_tokens,

View file

@ -88,6 +88,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
name,
&coord.hyperhive_flake,
&coord.nixpkgs_flake,
&coord.nixpkgs_unstable_flake,
&agent_dir,
&proposed_dir,
&applied_dir,
@ -153,6 +154,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
name,
&coord.hyperhive_flake,
&coord.nixpkgs_flake,
&coord.nixpkgs_unstable_flake,
&agent_dir,
&applied_dir,
&claude_dir,