diff --git a/docs/swarm/archetypes.md b/docs/swarm/archetypes.md index 201c1e6e..bcd05393 100644 --- a/docs/swarm/archetypes.md +++ b/docs/swarm/archetypes.md @@ -7,8 +7,8 @@ resource caps and inference fields from it, and you can still change each field before you create the agent. An agent created from an archetype has no `agent-configs/` repo of its -own. Its hive builds it from the archetype's module, so every agent of that -archetype runs the same configuration. To change what it runs, change the +own. The swarm's `internal/meta` flake builds it from the archetype's module, +so every agent of that archetype runs the same configuration. To change what it runs, change the archetype. Picking "none" creates a custom agent, with its own config repo. ## The built-in archetypes @@ -41,11 +41,13 @@ hyperhiveArchetypes.reviewer = { - Every archetype needs a `description`. `permissions`, `limits` and `inference` are optional and take the shapes `POST /api/agents` takes. The swarm ignores other keys in `meta`. -- `module` is a NixOS module for the agent's container. A hive imports it - into each agent created from the archetype. +- `module` is a NixOS module for the agent's container. The meta flake + imports it into each agent created from the archetype. Today the swarm reads one flake: the hyperhive source the -[builder](builder.md)'s host runs, so only the built-ins appear. +[builder](builder.md)'s host runs, so only the built-ins appear. The meta +flake has one archetype source too, its own `hyperhive` input, and refuses +an archetype from any other source name. ## How the list stays current @@ -110,23 +112,31 @@ Deploy `swarm-nats-auth` before the hives: every provision reads the agent's key, and a hive whose queue connection predates the grant gets a refused read, so its `Provision` nodes fail until it reconnects. -The hive reads the agent's key when it provisions the agent and on every -rebuild, and keeps the last value read in -`/var/lib/hyperhive/swarm-agent-archetypes.json`, so the meta flake renders -without the queue. When the read fails, a rebuild keeps the value -stored there. For an archetype agent the hive: +swarm-controller renders `internal/meta`'s `flake.nix` with the +`hive-meta-flake` crate, and reads each agent's archetype from the +`agent-archetype` bucket to do so. For an archetype agent the render: -- seeds no `proposed` or `applied` repo, and gives `internal/meta` no - `agent-` input. `mkAgent` imports - `hyperhive.hyperhiveArchetypes..module` in place of the agent's own - flake, and sets `HIVE_ARCHETYPE` in the harness environment; +- adds no `agent-` input and reads no config repo, and lists the agent + as `mkAgent { …; archetype = { source = "hyperhive"; name = ""; }; }`; +- makes `mkAgent` import `hyperhive.hyperhiveArchetypes..module` in + place of the agent's own flake, and set `HIVE_ARCHETYPE` in the harness + environment. + +The hive builds from `internal/meta` alone. It reads the agent's key when it +provisions the agent and on every rebuild, and keeps the last value read in +`/var/lib/hyperhive/swarm-agent-archetypes.json`. It takes the head of +`internal/meta` only when that `flake.nix` lists the agent at the archetype +it has now. When the read fails, a rebuild keeps the stored value. For an +archetype agent the hive also: + +- seeds no `proposed` or `applied` repo; - ignores the `rev` of a swarm deploy request; - mounts an empty `/agents//config/`. The agent's system prompt says it has no config repo and names its archetype. -The hive takes the module from its own `hyperhive` input. When that input -predates the archetype, the agent's build fails with -`this hive's hyperhive input has no archetype `. Update the hive's -`hyperhive` input to fix it. +The module comes from the `hyperhive` input that `internal/meta` pins, not +from the hive's own. When that pin predates the archetype, the agent's build +fails with `this hive's hyperhive input has no archetype ; update that +input`. Update the `hyperhive` pin in `internal/meta` to fix it. diff --git a/flake.nix b/flake.nix index dc795ad2..2bd6eb1f 100644 --- a/flake.nix +++ b/flake.nix @@ -194,8 +194,9 @@ nixosConfigurations = let # These two configs are what hive-c0re extends per agent - # (meta.rs's `mkAgent` does `.extendModules { … }`, so - # the built base and every container stay on one version). + # (the `mkAgent` in hive-meta-flake's render does + # `.extendModules { … }`, so the built base and every + # container stay on one version). # Nothing deployment-specific may be set here: it would be # inherited by every container on the hive and collide with # the per-agent values hive-c0re renders. The agent modules diff --git a/hive-meta-flake/src/lib.rs b/hive-meta-flake/src/lib.rs index 1f6344dd..e48f08ed 100644 --- a/hive-meta-flake/src/lib.rs +++ b/hive-meta-flake/src/lib.rs @@ -617,6 +617,55 @@ mod tests { assert!(!builds_agent_at(&flake, "ghost", Some(&reviewer()), None)); } + /// The text of `mkAgent`'s `let` binding `name`: from `name =` to the + /// next binding at the same indent (`;` ends it at line end). + fn binding<'a>(flake: &'a str, name: &str) -> &'a str { + let from = flake + .find(&format!("\n {name} =\n")) + .or_else(|| flake.find(&format!("\n {name} = "))) + .unwrap_or_else(|| panic!("no `{name}` binding in mkAgent")); + let rest = &flake[from + 1..]; + let end = rest + .find("\n service =") + .unwrap_or_else(|| panic!("`{name}` is the last binding")); + &rest[..end] + } + + #[test] + fn an_archetype_agent_s_module_is_the_archetype_source_s_module() { + let flake = rendered(); + let module = binding(&flake, "agentModule"); + assert!( + module.contains("archetypeSources.${archetype.source}"), + "the archetype branch picks its source by name: {module}" + ); + assert!( + module.contains(".hyperhiveArchetypes.${archetype.name}.module"), + "the archetype branch imports the archetype's module: {module}" + ); + // The source it picks from must offer the hyperhive input. + assert!( + binding(&flake, "archetypeSources").contains("inherit hyperhive;"), + "the `hyperhive` archetype source" + ); + // The module reaches the agent's system. + let modules = &flake[flake.find("modules = [").expect("a module list")..]; + assert!( + modules + .trim_start_matches("modules = [") + .trim_start() + .starts_with("agentModule"), + "agentModule heads the extendModules list" + ); + // The entry names the source and archetype the import reads. + assert!( + flake.contains("rev = mkAgent { name = \"rev\"; port = ") + && flake + .contains(" archetype = { source = \"hyperhive\"; name = \"reviewer\"; }; };"), + "the archetype agent's entry" + ); + } + #[test] fn an_agent_whose_name_ends_another_s_is_not_that_agent() { let flake = rendered();