Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-queue-client/src/archetypes.rs
atlas 51035eac1d archetypes: prefill the create form from flake-declared agent archetypes
A flake declares agent archetypes in its `hyperhiveArchetypes` output,
`<name> = { module; meta; }`. The hyperhive flake ships `reviewer`
(default permissions without `execution`).

swarm-builder answers `$SWARM.builder.archetypes` with the `meta` of
every archetype its own hyperhive source declares. swarm-controller asks
once the queue connects and every 10 minutes, checks each archetype as
`POST /api/agents` checks its settings, and stores the result in the
`agent-archetypes` KV bucket when it changed. `GET /api/archetypes`
serves it, and the create form's archetype select prefills permissions,
resource caps and inference from it.

The agent created is an ordinary agent with its own config repo; the
archetype's module is not applied yet.

Refs #5386
2026-10-17 12:57:20 +02:00

117 lines
4.3 KiB
Rust

//! The agent archetype KV bucket: one key per archetype source flake, holding
//! the `meta` of every archetype that flake's `hyperhiveArchetypes` output
//! declares, as swarm-controller last checked it.
//!
//! **swarm-controller is the only writer and the only reader.** The swarm
//! builder evaluates the source and answers on
//! [`crate::builder::ARCHETYPES_SUBJECT`]; the controller checks each
//! archetype's settings the way `POST /api/agents` checks them and writes the
//! result here, so the create screen lists archetypes from a value that a
//! create would accept.
use std::collections::BTreeMap;
use crate::agent_inference::AgentInference;
use crate::agent_limits::AgentCaps;
/// The KV bucket archetype sources are published into, keyed by source name.
pub const BUCKET: &str = "agent-archetypes";
/// The key of the archetypes the hyperhive flake itself ships, the one source
/// there is so far.
pub const BUILTIN_SOURCE: &str = "hyperhive";
/// One source's archetypes: the value under `BUCKET`/`<source>`.
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SourceRecord {
/// The flake reference the builder evaluated.
pub source: String,
/// Every archetype whose `meta` passed the controller's checks, by name.
#[serde(default)]
pub archetypes: BTreeMap<String, ArchetypeMeta>,
/// Every archetype whose `meta` did not, by name, with the reason.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub refused: BTreeMap<String, String>,
}
/// One archetype's `meta`, as checked: the settings a create takes, in the
/// shapes the per-agent buckets store them in.
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ArchetypeMeta {
pub description: String,
/// Permission names, `snake_case`. Absent leaves the default set.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permissions: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limits: Option<AgentCaps>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inference: Option<AgentInference>,
}
/// Open the archetype bucket for writing, creating it if nothing has yet.
///
/// **Controller-side only.** `history: 1`: a reader wants the current value.
#[cfg(feature = "kv")]
pub async fn open_or_create(
client: &async_nats::Client,
) -> Result<async_nats::jetstream::kv::Store, crate::Error> {
let js = async_nats::jetstream::new(client.clone());
crate::kv::open_or_create(
&js,
BUCKET,
"agent-archetypes",
"Agent archetypes the swarm builder evaluated, one key per source flake".to_owned(),
)
.await
}
/// Open the archetype bucket for reading, or `None` when nothing has created
/// it yet, which reads the same as no archetypes.
///
/// # Errors
///
/// [`crate::Error::OpenBucket`] for every failure other than the bucket not
/// existing.
#[cfg(feature = "kv")]
pub async fn open(
client: &async_nats::Client,
) -> Result<Option<async_nats::jetstream::kv::Store>, crate::Error> {
let js = async_nats::jetstream::new(client.clone());
match js.get_key_value(BUCKET).await {
Ok(store) => Ok(Some(store)),
Err(e) if crate::wanted::is_bucket_not_found(&e) => Ok(None),
Err(source) => Err(crate::Error::OpenBucket {
bucket: BUCKET.to_owned(),
source,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
/// An unset setting is left out on the wire, and so is an empty refusal
/// list.
#[test]
fn unset_fields_are_absent() {
let mut archetypes = BTreeMap::new();
archetypes.insert(
"reviewer".to_owned(),
ArchetypeMeta {
description: "reads".to_owned(),
permissions: Some(vec!["messaging".to_owned()]),
..ArchetypeMeta::default()
},
);
let record = SourceRecord {
source: "/nix/store/h".to_owned(),
archetypes,
refused: BTreeMap::new(),
};
assert_eq!(
serde_json::to_string(&record).unwrap(),
r#"{"source":"/nix/store/h","archetypes":{"reviewer":{"description":"reads","permissions":["messaging"]}}}"#
);
}
}