the rendered meta flake pins hyperhive by forge URL and follows its nixpkgs
This commit is contained in:
parent
6ea13c02e3
commit
bb13909d8e
8 changed files with 135 additions and 153 deletions
|
|
@ -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+<forge>/hyperhive/hyperhive.git?rev=<rev>`.
|
||||
/// Errors when the host has no rev, rather than rendering an unpinned URL.
|
||||
fn hyperhive_input_url(hive: &HiveEnv) -> Result<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<F>(
|
||||
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-<n>.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-<n>.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/<n>` 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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue