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

@ -0,0 +1,143 @@
//! The archetype each of this hive's archetype agents is built from, as last
//! read from the swarm's `agent-archetype` bucket
//! ([`swarm_queue_client::agent_archetype`]). Stored at
//! `/var/lib/hyperhive/swarm-agent-archetypes.json`, outside the meta repo:
//! the meta render runs at boot, possibly before the queue answers, so it
//! reads this file rather than the bucket.
//!
//! Format: agent name → [`ArchetypeRef`]. An agent with an entry has no config
//! repo: the meta flake builds it from the archetype's module, and the paths
//! that seed, relock or advance `applied/<name>` skip it. An agent with no
//! entry is a custom agent.
//!
//! [`refresh`] reads one agent's key; provisioning and every rebuild's meta
//! sync call it before they render the meta flake.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use swarm_queue_client::agent_archetype::{self, ArchetypeRef, Read};
const FILE: &str = "swarm-agent-archetypes.json";
pub type Stored = BTreeMap<String, ArchetypeRef>;
#[must_use]
pub fn path() -> PathBuf {
crate::paths::state_root().join(FILE)
}
/// Read the stored archetypes. An absent file is the empty map: every agent
/// custom. A file that exists but can't be read or parsed is an error.
pub fn read_at(path: &Path) -> std::io::Result<Stored> {
super::read_map(path)
}
/// `agent`'s archetype per the file at [`path`], `None` for a custom agent.
///
/// # Errors
///
/// As [`read_at`].
pub fn of(agent: &str) -> std::io::Result<Option<ArchetypeRef>> {
Ok(read_at(&path())?.remove(agent))
}
/// Read `agent`'s key and store what it holds. No queue, or a bucket that
/// does not answer, leaves the stored entry as it is.
///
/// # Errors
///
/// The queue is configured but not connected, the read failed, the value
/// names a source or archetype that is not an identifier, or the file could
/// not be read or written.
pub async fn refresh(agent: &str) -> Result<()> {
let Some(client) = crate::swarm_queue::client().await else {
return Ok(());
};
// An unconnected client does not fail a JetStream request, it hangs on it.
swarm_queue_client::ensure_connected(&client)?;
let read = agent_archetype::read(&client, agent)
.await
.map_err(|e| anyhow::anyhow!(e))
.with_context(|| format!("reading the swarm's archetype for {agent}"))?;
store_read(&path(), agent, read)
}
/// Store `read`, the value of `agent`'s key, in the file at `path`. Written
/// only when it changes the entry.
fn store_read(path: &Path, agent: &str, read: Read) -> Result<()> {
let value = match read {
Read::NoBucket => {
tracing::debug!(%agent, "archetype: the agent-archetype bucket does not answer");
return Ok(());
}
Read::Current(value) => value,
};
// Both are rendered into the meta flake as nix strings.
if let Some(ArchetypeRef { source, name }) = &value {
for part in [source, name] {
hive_types::Ident::parse(part)
.map_err(|why| anyhow::anyhow!("archetype {part:?} of {agent}: {why}"))?;
}
}
let mut stored = read_at(path).context("reading the stored archetypes")?;
let changed = match value {
Some(value) => stored.insert(agent.to_owned(), value.clone()) != Some(value),
None => stored.remove(agent).is_some(),
};
if changed {
super::write_map(path, &stored).context("storing the archetypes")?;
tracing::info!(%agent, archetype = ?stored.get(agent), "archetype: stored the swarm's archetype");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn reviewer() -> ArchetypeRef {
ArchetypeRef {
source: "hyperhive".to_owned(),
name: "reviewer".to_owned(),
}
}
#[test]
fn a_read_sets_or_clears_the_entry_and_no_bucket_keeps_it() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join(FILE);
assert!(read_at(&path).expect("missing is empty").is_empty());
store_read(&path, "atlas", Read::Current(Some(reviewer()))).expect("store");
store_read(&path, "iris", Read::Current(None)).expect("store");
assert_eq!(
read_at(&path).expect("read"),
Stored::from([("atlas".to_owned(), reviewer())])
);
store_read(&path, "atlas", Read::NoBucket).expect("store");
assert_eq!(
read_at(&path).expect("read").get("atlas"),
Some(&reviewer())
);
store_read(&path, "atlas", Read::Current(None)).expect("store");
assert!(read_at(&path).expect("read").is_empty());
}
/// The value is rendered into nix, so a name that is not an identifier is
/// refused and nothing is stored.
#[test]
fn a_name_that_is_not_an_identifier_is_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join(FILE);
let bad = ArchetypeRef {
source: "hyperhive".to_owned(),
name: "x\"; y".to_owned(),
};
store_read(&path, "atlas", Read::Current(Some(bad))).expect_err("refused");
assert!(read_at(&path).expect("read").is_empty());
}
}

View file

@ -1,13 +1,14 @@
//! Per-agent configuration registries: resource limits, topology (JSON files
//! under `/var/lib/hyperhive/meta/`), the permissions the swarm grants
//! (outside the meta repo) and the shared wire-protocol size limits. Each
//! submodule is re-exported at the crate root, so `crate::topology::…` etc.
//! keep working unchanged.
//! under `/var/lib/hyperhive/meta/`), the permissions and archetypes the
//! swarm sets (outside the meta repo) and the shared wire-protocol size
//! limits. Each submodule is re-exported at the crate root, so
//! `crate::topology::…` etc. keep working unchanged.
//!
//! Note the two similarly-named modules: [`limits`] caps inline
//! *message body* sizes on the sockets, while [`resource_limits`] holds
//! per-agent CPU/memory caps for the container drop-in.
pub mod archetypes;
pub mod limits;
pub mod permissions;
pub mod resource_limits;