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()
})
}
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()]
);
}
}