car-registry 0.55.0

File-based agent registry + lifecycle supervisor for Common Agent Runtime.
Documentation
//! Validation for daemon-method scopes attached to supervised-agent tokens.
//!
//! The inventory is generated from `docs/capabilities.json`. Only methods whose
//! derived caller role is `agent`, `operator`, or `owner` are eligible. A scope
//! never grants the authority represented by those roles; it can only remove
//! methods from what the session's existing authority would otherwise permit.
//! The generator also names the three incoming agent notifications; explicit
//! grants do not bypass their existing routing and producer ownership checks.

use serde::Deserialize;
use std::collections::BTreeSet;
use std::sync::OnceLock;

const INVENTORY_JSON: &str = include_str!("agent_usable_daemon_methods.json");

#[derive(Deserialize)]
struct Inventory {
    methods: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
    "invalid method_allowlist entries (not agent-usable daemon methods): {}",
    .0.join(", ")
)]
pub struct InvalidMethodAllowlist(pub Vec<String>);

fn inventory() -> &'static BTreeSet<String> {
    static METHODS: OnceLock<BTreeSet<String>> = OnceLock::new();
    METHODS.get_or_init(|| {
        let parsed: Inventory = serde_json::from_str(INVENTORY_JSON)
            .expect("generated agent-usable daemon method inventory must be valid JSON");
        parsed.methods.into_iter().collect()
    })
}

/// Validate and canonicalize an optional supervised-agent method scope.
///
/// `None` preserves the historical unrestricted token. `Some([])` is a valid
/// deny-all scope. Present lists are sorted and deduplicated before they are
/// persisted so equivalent registrations have one on-disk representation.
pub fn normalize_method_allowlist(
    allowlist: &mut Option<Vec<String>>,
) -> Result<(), InvalidMethodAllowlist> {
    let Some(methods) = allowlist.as_mut() else {
        return Ok(());
    };

    let known = inventory();
    let mut invalid: Vec<String> = methods
        .iter()
        .filter(|method| !known.contains(method.as_str()))
        .cloned()
        .collect();
    invalid.sort();
    invalid.dedup();
    if !invalid.is_empty() {
        return Err(InvalidMethodAllowlist(invalid));
    }

    methods.sort();
    methods.dedup();
    Ok(())
}

pub fn is_agent_usable_daemon_method(method: &str) -> bool {
    inventory().contains(method)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn inventory_accepts_non_host_roles_and_rejects_host_methods() {
        assert!(is_agent_usable_daemon_method("infer"));
        assert!(is_agent_usable_daemon_method("mail.messages"));
        assert!(is_agent_usable_daemon_method("runs.get_trace"));
        assert!(!is_agent_usable_daemon_method("agents.upsert"));
        let mut notifications = Some(vec![
            "agent.chat.event".to_string(),
            "browser.producer.frame".to_string(),
            "browser.producer.presentation".to_string(),
        ]);
        normalize_method_allowlist(&mut notifications).unwrap();
        assert!(!is_agent_usable_daemon_method("agents.chat.event"));
    }

    #[test]
    fn normalization_preserves_none_and_empty_but_canonicalizes_entries() {
        let mut absent = None;
        normalize_method_allowlist(&mut absent).unwrap();
        assert_eq!(absent, None);

        let mut empty = Some(Vec::new());
        normalize_method_allowlist(&mut empty).unwrap();
        assert_eq!(empty, Some(Vec::new()));

        let mut methods = Some(vec![
            "mail.messages".to_string(),
            "infer".to_string(),
            "mail.messages".to_string(),
        ]);
        normalize_method_allowlist(&mut methods).unwrap();
        assert_eq!(
            methods,
            Some(vec!["infer".to_string(), "mail.messages".to_string()])
        );
    }

    #[test]
    fn invalid_entries_are_named_together() {
        let mut methods = Some(vec![
            "mail.send.typo".to_string(),
            "agents.upsert".to_string(),
        ]);
        let error = normalize_method_allowlist(&mut methods).unwrap_err();
        assert_eq!(
            error.0,
            vec!["agents.upsert".to_string(), "mail.send.typo".to_string()]
        );
    }
}