meta: emit nixpkgs follows for agents that declare it (closes #355)
This commit is contained in:
parent
8e5112aa27
commit
4d7c767eb0
1 changed files with 139 additions and 0 deletions
|
|
@ -239,6 +239,68 @@ fn render_flake(
|
||||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||||
agents: &[AgentSpec],
|
agents: &[AgentSpec],
|
||||||
) -> String {
|
) -> String {
|
||||||
|
render_flake_with_lookup(
|
||||||
|
hyperhive_flake,
|
||||||
|
dashboard_port,
|
||||||
|
operator_pronouns,
|
||||||
|
context_window_tokens,
|
||||||
|
agents,
|
||||||
|
agent_canonical_inputs,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Canonical inputs meta knows how to dedup. An agent that declares one
|
||||||
|
/// of these as a top-level input in its own `flake.nix` will get a
|
||||||
|
/// `follows = "<name>"` line emitted in meta — collapsing the
|
||||||
|
/// otherwise-separate-but-identical `nixpkgs_N` nodes into a single
|
||||||
|
/// meta-level reference (#355).
|
||||||
|
const CANONICAL_INPUTS: &[&str] = &["nixpkgs", "nixpkgs-unstable"];
|
||||||
|
|
||||||
|
/// Read an agent's applied `flake.lock` and return the subset of
|
||||||
|
/// `CANONICAL_INPUTS` it declares as direct (root-level) inputs.
|
||||||
|
/// Returns an empty vec when the lock is missing or unparsable —
|
||||||
|
/// safe degradation, the worst case is no dedup for that agent.
|
||||||
|
fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
|
||||||
|
let path = std::path::PathBuf::from(format!("{APPLIED_ROOT}/{name}/flake.lock"));
|
||||||
|
let Ok(raw) = std::fs::read_to_string(&path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some(nodes) = json.get("nodes").and_then(|v| v.as_object()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some(root_name) = json.get("root").and_then(|v| v.as_str()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some(root_inputs) = nodes
|
||||||
|
.get(root_name)
|
||||||
|
.and_then(|n| n.get("inputs"))
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
CANONICAL_INPUTS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|canon| root_inputs.contains_key(*canon))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inner render helper accepting a lookup fn so tests can stub the
|
||||||
|
/// agent flake-lock introspection.
|
||||||
|
fn render_flake_with_lookup<F>(
|
||||||
|
hyperhive_flake: &str,
|
||||||
|
dashboard_port: u16,
|
||||||
|
operator_pronouns: &str,
|
||||||
|
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||||
|
agents: &[AgentSpec],
|
||||||
|
lookup: F,
|
||||||
|
) -> String
|
||||||
|
where
|
||||||
|
F: Fn(&str) -> Vec<&'static str>,
|
||||||
|
{
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str("{\n description = \"hyperhive deployed agents\";\n inputs = {\n");
|
out.push_str("{\n description = \"hyperhive deployed agents\";\n inputs = {\n");
|
||||||
|
|
@ -263,6 +325,21 @@ fn render_flake(
|
||||||
" agent-{}.url = \"git+file://{APPLIED_ROOT}/{}\";",
|
" agent-{}.url = \"git+file://{APPLIED_ROOT}/{}\";",
|
||||||
spec.name, spec.name,
|
spec.name, spec.name,
|
||||||
);
|
);
|
||||||
|
// For each canonical input the agent declares in its own
|
||||||
|
// `flake.nix` (detected by reading its applied `flake.lock`),
|
||||||
|
// emit `inputs.agent-<name>.inputs.<canon>.follows = "<canon>"`.
|
||||||
|
// Collapses three otherwise-separate-but-identical nixpkgs
|
||||||
|
// nodes (root + agent-bitburner's + agent-dmatrix's) into one
|
||||||
|
// (closes #355). Skipped silently for agents that don't
|
||||||
|
// declare the input — emitting follows on a non-existent
|
||||||
|
// input would error at `nix flake lock` time.
|
||||||
|
for canon in lookup(&spec.name) {
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
" agent-{}.inputs.{canon}.follows = \"{canon}\";",
|
||||||
|
spec.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out.push_str(" };\n outputs =\n { self, hyperhive, ... }@inputs:\n let\n");
|
out.push_str(" };\n outputs =\n { self, hyperhive, ... }@inputs:\n let\n");
|
||||||
// Free-text operator string — escape backslash + double-quote so a
|
// Free-text operator string — escape backslash + double-quote so a
|
||||||
|
|
@ -387,6 +464,68 @@ mod tests {
|
||||||
assert!(out.contains("hyperhive.inputs.nixpkgs.follows = \"nixpkgs\""));
|
assert!(out.contains("hyperhive.inputs.nixpkgs.follows = \"nixpkgs\""));
|
||||||
assert!(out.contains("hyperhive.inputs.nixpkgs-unstable.follows = \"nixpkgs-unstable\""));
|
assert!(out.contains("hyperhive.inputs.nixpkgs-unstable.follows = \"nixpkgs-unstable\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_flake_emits_follows_for_agents_declaring_nixpkgs() {
|
||||||
|
// Stub lookup: pretend `bitburner` declares `nixpkgs` at its
|
||||||
|
// root, while `argus` has no canonical inputs at all.
|
||||||
|
let lookup = |name: &str| -> Vec<&'static str> {
|
||||||
|
match name {
|
||||||
|
"bitburner" => vec!["nixpkgs"],
|
||||||
|
"dmatrix" => vec!["nixpkgs", "nixpkgs-unstable"],
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let out = render_flake_with_lookup(
|
||||||
|
"github:example/hyperhive",
|
||||||
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
|
&[
|
||||||
|
sample_spec("argus", false, 9001),
|
||||||
|
sample_spec("bitburner", false, 9002),
|
||||||
|
sample_spec("dmatrix", false, 9003),
|
||||||
|
],
|
||||||
|
lookup,
|
||||||
|
);
|
||||||
|
// bitburner declares nixpkgs → follows emitted.
|
||||||
|
assert!(
|
||||||
|
out.contains("agent-bitburner.inputs.nixpkgs.follows = \"nixpkgs\""),
|
||||||
|
"missing bitburner nixpkgs follows:\n{out}"
|
||||||
|
);
|
||||||
|
// dmatrix declares both → both follows emitted.
|
||||||
|
assert!(out.contains("agent-dmatrix.inputs.nixpkgs.follows = \"nixpkgs\""));
|
||||||
|
assert!(
|
||||||
|
out.contains("agent-dmatrix.inputs.nixpkgs-unstable.follows = \"nixpkgs-unstable\"")
|
||||||
|
);
|
||||||
|
// argus declares neither → no follows emitted for it. Asserting
|
||||||
|
// ABSENCE is the important bit: emitting a follows on a
|
||||||
|
// non-existent input errors at `nix flake lock` time.
|
||||||
|
assert!(
|
||||||
|
!out.contains("agent-argus.inputs.nixpkgs"),
|
||||||
|
"argus shouldn't have nixpkgs follows:\n{out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_flake_skips_canonical_follows_when_lookup_returns_empty() {
|
||||||
|
let out = render_flake_with_lookup(
|
||||||
|
"github:example/hyperhive",
|
||||||
|
8000,
|
||||||
|
"she/her",
|
||||||
|
&std::collections::HashMap::new(),
|
||||||
|
&[sample_spec("alice", false, 9001)],
|
||||||
|
|_| Vec::new(),
|
||||||
|
);
|
||||||
|
// No agent-side follows when the lookup reports nothing
|
||||||
|
// declared — protects agents whose flake.lock can't be read
|
||||||
|
// (missing / unparsable) from being broken by a follows on a
|
||||||
|
// non-existent input.
|
||||||
|
assert!(
|
||||||
|
!out.contains("agent-alice.inputs."),
|
||||||
|
"alice shouldn't have any inputs follows:\n{out}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn git_is_clean(dir: &Path) -> Result<bool> {
|
async fn git_is_clean(dir: &Path) -> Result<bool> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue