use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgentDef {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_steps: Option<u32>,
#[serde(default)]
pub deny_write: bool,
#[serde(default)]
pub deny_net: bool,
}
impl AgentDef {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
pub fn with_role(mut self, role: impl Into<String>) -> Self {
self.role = Some(role.into());
self
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn with_max_steps(mut self, steps: u32) -> Self {
self.max_steps = Some(steps);
self
}
pub fn deny_write(mut self) -> Self {
self.deny_write = true;
self
}
pub fn deny_net(mut self) -> Self {
self.deny_net = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Agents {
defs: BTreeMap<String, AgentDef>,
}
impl Agents {
pub fn new() -> Self {
Self::default()
}
pub fn with(mut self, def: AgentDef) -> Self {
self.defs.insert(def.name.clone(), def);
self
}
pub fn get(&self, name: &str) -> Option<&AgentDef> {
self.defs.get(name)
}
pub fn names(&self) -> Vec<&str> {
self.defs.keys().map(String::as_str).collect()
}
pub fn len(&self) -> usize {
self.defs.len()
}
pub fn is_empty(&self) -> bool {
self.defs.is_empty()
}
pub fn catalog(&self) -> String {
let mut out = String::new();
for def in self.defs.values() {
match def.role.as_deref() {
Some(role) => out.push_str(&format!("- {}: {}\n", def.name, role.trim())),
None => out.push_str(&format!("- {}\n", def.name)),
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_definition_narrows_nothing() {
let d = AgentDef::new("worker");
assert!(!d.deny_write && !d.deny_net);
assert!(d.role.is_none() && d.model.is_none() && d.max_steps.is_none());
}
#[test]
fn registering_the_same_name_twice_replaces_rather_than_shadows() {
let roster = Agents::new()
.with(AgentDef::new("w").with_model("a"))
.with(AgentDef::new("w").with_model("b"));
assert_eq!(roster.len(), 1);
assert_eq!(roster.get("w").unwrap().model.as_deref(), Some("b"));
}
#[test]
fn the_catalog_lists_names_in_sorted_order() {
let roster = Agents::new()
.with(AgentDef::new("zeta").with_role("last"))
.with(AgentDef::new("alpha").with_role("first"));
let catalog = roster.catalog();
assert!(catalog.find("alpha").unwrap() < catalog.find("zeta").unwrap());
}
#[test]
fn a_definition_has_no_way_to_express_a_grant() {
let json = serde_json::to_string(&AgentDef::new("x").deny_write()).unwrap();
assert!(json.contains("deny_write"));
assert!(!json.contains("allow"));
}
#[test]
fn an_unknown_key_in_a_definition_is_rejected() {
let err = serde_json::from_str::<AgentDef>(r#"{"name":"x","deny_writes":true}"#);
assert!(err.is_err(), "a misspelled narrowing must not be ignored");
}
}