Watch
0
0
Fork
You've already forked hyperhive
0

archetypes: build an archetype agent from its archetype's module on the hive

The hive reads the agent-archetype key when it provisions and rebuilds an
agent and keeps it in swarm-agent-archetypes.json. For an archetype agent
the meta flake has no agent-<name> input and mkAgent imports
hyperhive.hyperhiveArchetypes.<name>.module; provisioning, rebuilds, the
meta-input cascade, a deploy's rev and the startup migration skip the
config repos. The harness gets HIVE_ARCHETYPE and the system prompt says
the agent has no config repo. The create form sends the picked archetype.
Adds the android-dev built-in.

Refs #5386
This commit is contained in:
atlas 2026-10-17 08:49:03 +02:00 • committed by mara
commit 553ed8376c
24 changed files with 557 additions and 92 deletions

View file

@ -42,6 +42,9 @@ pub const CONTAINER_MANAGER_META_MOUNT: &str = "/meta";
pub struct AgentSpec {
pub name: String,
pub port: u16,
/// The archetype the agent is built from (`crate::archetypes`), `None`
/// for a custom agent built from its own config repo.
pub archetype: Option<swarm_queue_client::agent_archetype::ArchetypeRef>,
}
/// Stage topology.json if it exists: it is regenerated by `reconcile`
@ -786,6 +789,11 @@ where
// (config PRs land there) but are not the flake's build input. deploy +
// rebuild re-lock this input to `applied/<name>`'s current `main` head.
for spec in agents {
// An archetype agent has no config repo, so no input: `mkAgent`
// takes its module from the archetype.
if spec.archetype.is_some() {
continue;
}
let _ = writeln!(
out,
" agent-{name}.url = \"git+file://{applied}\";",
@ -811,17 +819,26 @@ where
out.push_str(" };\n outputs =\n { self, hyperhive, ... }@inputs:\n let\n");
let _ = writeln!(
out,
" dashboardPort = {dashboard_port};\n mkAgent = {{ name, port }}:"
" dashboardPort = {dashboard_port};\n mkAgent = {{ name, port, archetype ? null }}:"
);
out.push_str(
r#" let
base = hyperhive.nixosConfigurations.agent-base;
input = inputs."agent-${name}";
# Every flake an archetype can come from, by its source name.
archetypeSources = { inherit hyperhive; };
agentModule =
if archetype == null then
inputs."agent-${name}".nixosModules.default
else
(archetypeSources.${archetype.source}
or (throw "agent ${name}: archetype source ${archetype.source} is not an input of this hive's meta flake")
).hyperhiveArchetypes.${archetype.name}.module
or (throw "agent ${name}: this hive's ${archetype.source} input has no archetype ${archetype.name}; update that input");
service = "hive-agent";
in
base.extendModules {
modules = [
input.nixosModules.default
agentModule
{
"#,
);
@ -1008,6 +1025,10 @@ where
HYPERHIVE_HARNESS_DIR = "/agents/${name}/harness";
};
}
# Tells the harness the agent has no config repo of its own.
(if archetype == null then { } else {
systemd.services.${service}.environment.HIVE_ARCHETYPE = archetype.name;
})
];
};
in
@ -1016,9 +1037,17 @@ where
"#,
);
for spec in agents {
// `source` and `name` are identifiers (`archetypes::store_read`), so
// they need no escaping.
let archetype = spec.archetype.as_ref().map_or(String::new(), |a| {
format!(
" archetype = {{ source = \"{}\"; name = \"{}\"; }};",
a.source, a.name
)
});
let _ = writeln!(
out,
" {} = mkAgent {{ name = \"{}\"; port = {}; }};",
" {} = mkAgent {{ name = \"{}\"; port = {};{archetype} }};",
spec.name, spec.name, spec.port,
);
}
@ -1235,6 +1264,7 @@ mod tests {
AgentSpec {
name: name.to_owned(),
port,
archetype: None,
}
}
@ -1501,6 +1531,53 @@ mod tests {
);
}
/// An archetype agent has no `agent-<name>` input, and `mkAgent` is
/// handed its archetype; a custom agent beside it keeps its input.
#[test]
fn render_flake_builds_an_archetype_agent_from_its_archetype() {
let android = AgentSpec {
archetype: Some(swarm_queue_client::agent_archetype::ArchetypeRef {
source: "hyperhive".to_owned(),
name: "android-dev".to_owned(),
}),
..sample_spec("droid", 9002)
};
let out = render_flake_with_lookup(
"github:example/hyperhive",
"path:/nix/store/bbbb-hyperhive-docs-source",
"path:/nix/store/aaaa-nixpkgs-source",
None,
8000,
&[sample_spec("alice", 9001), android],
|_| vec!["nixpkgs"],
);
assert!(
!out.contains("agent-droid"),
"an archetype agent has no config input:\n{out}"
);
assert!(
out.contains(
"droid = mkAgent { name = \"droid\"; port = 9002; \
archetype = { source = \"hyperhive\"; name = \"android-dev\"; }; };"
),
"{out}"
);
assert!(out.contains("agent-alice.url = \"git+file://"), "{out}");
assert!(out.contains("agent-alice.inputs.nixpkgs.follows"), "{out}");
assert!(
out.contains("alice = mkAgent { name = \"alice\"; port = 9001; };"),
"{out}"
);
assert!(
out.contains("hyperhiveArchetypes.${archetype.name}.module"),
"{out}"
);
assert!(
out.contains("environment.HIVE_ARCHETYPE = archetype.name"),
"{out}"
);
}
#[test]
fn render_flake_forwards_env_into_global_environment() {
// Regression for the isolation breakage where forwarded vars