Watch
0
0
Fork
You've already forked hyperhive
0

controller: turn an archetype agent into a custom one

This commit is contained in:
atlas 2026-11-01 12:02:38 +01:00 • committed by mara
commit 0737798d78

View file

@ -43,12 +43,12 @@ pub fn pinned_git_url(forge_git_url: &str, rev: Option<&str>) -> Result<String>
Ok(format!("git+{forge_git_url}?rev={rev}"))
}
/// 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
/// Canonical inputs meta knows how to dedup. An agent whose `flake.lock` or
/// `flake.nix` declares one of these as a top-level input gets a
/// `follows = "<name>"` line emitted in meta — collapsing the
/// otherwise-separate-but-identical `nixpkgs_N` nodes into a single
/// meta-level reference.
pub const CANONICAL_INPUTS: &[&str] = &["nixpkgs"];
/// otherwise-separate-but-identical `nixpkgs_N` and `hyperhive_N` nodes into
/// a single meta-level reference.
pub const CANONICAL_INPUTS: &[&str] = &["nixpkgs", "hyperhive"];
/// Env vars the meta flake sets in every agent container, from the
/// `(name, value)` pairs [`render_flake`] is given. A caller passes only the
@ -143,6 +143,32 @@ pub fn push_forwarded_var_options(out: &mut String, vars: &[(&'static str, Strin
}
}
/// The subset of `CANONICAL_INPUTS` that an agent config declares, in its
/// `flake.lock` text `lock` or its `flake.nix` text `flake_nix`. A repo that
/// was only just seeded has no lock yet, so its `flake.nix` counts too: a line
/// starting `<input>.url` or `inputs.<input>.url`.
///
/// A `hyperhive` node in a lock does not count: it can be a leftover from an
/// input the config dropped from its `flake.nix`, and a `follows` for an input
/// the flake does not declare makes nix warn.
#[must_use]
pub fn canonical_inputs_declared(lock: Option<&str>, flake_nix: Option<&str>) -> Vec<&'static str> {
let locked = lock.map(canonical_inputs_in_lock).unwrap_or_default();
CANONICAL_INPUTS
.iter()
.copied()
.filter(|canon| {
(*canon != "hyperhive" && locked.contains(canon))
|| flake_nix.is_some_and(|text| {
text.lines().map(str::trim_start).any(|l| {
l.starts_with(&format!("{canon}.url"))
|| l.starts_with(&format!("inputs.{canon}.url"))
})
})
})
.collect()
}
/// The subset of `CANONICAL_INPUTS` that the `flake.lock` text `raw` declares
/// as direct (root-level) inputs. Empty when the lock is unparsable — safe
/// degradation, the worst case is no dedup for that agent.
@ -460,17 +486,26 @@ pub fn is_rev(rev: &str) -> bool {
rev.len() == 40 && rev.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
}
/// The URL of `input` in a flake [`render_flake`] rendered: the text of its
/// `<input>.url = "<url>";` line. `None` when the flake has no such input.
#[must_use]
pub fn input_url<'a>(flake_nix: &'a str, input: &str) -> Option<&'a str> {
let prefix = format!(" {input}.url = \"");
Some(
flake_nix
.lines()
.find_map(|l| l.strip_prefix(prefix.as_str()))?
.split_once('"')?
.0,
)
}
/// The commit `input` is pinned to in a flake [`render_flake`] rendered:
/// the `<rev>` of its `<input>.url = "git+<url>?rev=<rev>";` line. `None` when
/// the flake has no such input, or its rev is not a commit id.
#[must_use]
pub fn pinned_rev<'a>(flake_nix: &'a str, input: &str) -> Option<&'a str> {
let prefix = format!(" {input}.url = \"");
let url = flake_nix
.lines()
.find_map(|l| l.strip_prefix(prefix.as_str()))?
.split_once('"')?
.0;
let url = input_url(flake_nix, input)?;
let (_, rev) = url.split_once("?rev=")?;
let rev = rev.split('&').next()?;
is_rev(rev).then_some(rev)
@ -695,4 +730,51 @@ mod tests {
assert!(!builds_agent_at(&flake, "gus", None, Some(ARGUS)));
assert!(!builds_agent_at(&flake, "v", Some(&reviewer()), None));
}
/// An agent that declares `hyperhive` follows meta's own pin, so the
/// lock holds one hyperhive; one that declares nothing gets no line (the
/// control that the line is the declared input's, not every agent's).
#[test]
fn a_declared_hyperhive_input_follows_meta_s_own() {
let url = |repo: &str, rev: &str| {
pinned_git_url(&format!("https://f.t/{repo}.git"), Some(rev)).unwrap()
};
let agents = |canonical_inputs: Vec<&'static str>| {
render_flake(
&url("hyperhive/hyperhive", HYPERHIVE),
&[spec(
"argus",
None,
Some(AgentPin {
url: url("agent-configs/argus", ARGUS),
canonical_inputs,
}),
)],
&[],
false,
)
.unwrap()
};
let line = "agent-argus.inputs.hyperhive.follows = \"hyperhive\";";
assert!(agents(vec!["hyperhive"]).contains(line));
assert!(!agents(vec![]).contains(line));
}
/// A seeded repo has only a `flake.nix`; its input counts, from either
/// spelling, and a lock-declared input still does.
#[test]
fn canonical_inputs_are_read_from_the_flake_text_and_the_lock() {
let flake = "{\n inputs = {\n hyperhive.url = \"git+x\";\n };\n}\n";
assert_eq!(canonical_inputs_declared(None, Some(flake)), ["hyperhive"]);
let dotted = "{\n inputs.nixpkgs.url = \"x\";\n}\n";
assert_eq!(canonical_inputs_declared(None, Some(dotted)), ["nixpkgs"]);
let lock = r#"{"root":"root","nodes":{"root":{"inputs":{"nixpkgs":"n"}}}}"#;
assert_eq!(canonical_inputs_declared(Some(lock), None), ["nixpkgs"]);
assert!(canonical_inputs_declared(None, Some("{ inputs = { }; }")).is_empty());
// A leftover `hyperhive` node in a lock declares nothing.
let stale = r#"{"root":"root","nodes":{"root":{"inputs":{"hyperhive":"hyperhive"}}}}"#;
assert!(canonical_inputs_declared(Some(stale), Some("{ inputs = { }; }")).is_empty());
// Another archetype source is not canonical: meta does not follow it.
let other = "{\n inputs = {\n extra.url = \"git+x\";\n };\n}\n";
assert!(canonical_inputs_declared(None, Some(other)).is_empty());
}
}