//! 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 //! `swarm_wire::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 swarm_wire::agent_inference::AgentInference; use swarm_wire::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`/``. #[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, /// Every archetype whose `meta` did not, by name, with the reason. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub refused: BTreeMap, } /// 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>, #[serde(default, skip_serializing_if = "Option::is_none")] pub limits: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub inference: Option, } /// 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 { 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, 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"]}}}"# ); } }