use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct CapabilitySet {
granted: HashMap<String, HashSet<String>>,
}
impl CapabilitySet {
pub fn from_grants(granted: HashMap<String, HashSet<String>>) -> Arc<Self> {
Arc::new(Self { granted })
}
pub fn empty() -> Arc<Self> {
Arc::new(Self::default())
}
pub fn check(&self, microapp_id: &str, capability: &str) -> bool {
self.granted
.get(microapp_id)
.is_some_and(|set| set.contains(capability))
}
pub fn granted_for(&self, microapp_id: &str) -> Option<&HashSet<String>> {
self.granted.get(microapp_id)
}
}
#[derive(Debug, Default)]
pub struct CapabilityBootReport {
pub errors: Vec<CapabilityBootError>,
pub warns: Vec<CapabilityBootWarn>,
pub grants: HashMap<String, HashSet<String>>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum CapabilityBootError {
RequiredNotGranted {
microapp_id: String,
missing: Vec<String>,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum CapabilityBootWarn {
OptionalNotGranted {
microapp_id: String,
missing: Vec<String>,
},
OrphanGrant {
microapp_id: String,
orphan: Vec<String>,
},
}
pub fn validate_capabilities_at_boot(
declarations: &[(String, AdminCapabilityDecl)],
grants: &HashMap<String, Vec<String>>,
) -> CapabilityBootReport {
let mut report = CapabilityBootReport::default();
for (microapp_id, decl) in declarations {
let granted: HashSet<String> = grants
.get(microapp_id)
.cloned()
.unwrap_or_default()
.into_iter()
.collect();
let required: HashSet<String> = decl.required.iter().cloned().collect();
let optional: HashSet<String> = decl.optional.iter().cloned().collect();
let declared: HashSet<String> = required.union(&optional).cloned().collect();
let missing_required: Vec<String> = required.difference(&granted).cloned().collect();
if !missing_required.is_empty() {
let mut sorted = missing_required;
sorted.sort();
report.errors.push(CapabilityBootError::RequiredNotGranted {
microapp_id: microapp_id.clone(),
missing: sorted,
});
}
let missing_optional: Vec<String> = optional.difference(&granted).cloned().collect();
if !missing_optional.is_empty() {
let mut sorted = missing_optional;
sorted.sort();
report.warns.push(CapabilityBootWarn::OptionalNotGranted {
microapp_id: microapp_id.clone(),
missing: sorted,
});
}
let orphan: Vec<String> = granted.difference(&declared).cloned().collect();
if !orphan.is_empty() {
let mut sorted = orphan;
sorted.sort();
report.warns.push(CapabilityBootWarn::OrphanGrant {
microapp_id: microapp_id.clone(),
orphan: sorted,
});
}
report.grants.insert(microapp_id.clone(), granted);
}
report
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct AdminCapabilityDecl {
pub required: Vec<String>,
pub optional: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn decl(required: &[&str], optional: &[&str]) -> AdminCapabilityDecl {
AdminCapabilityDecl {
required: required.iter().map(|s| s.to_string()).collect(),
optional: optional.iter().map(|s| s.to_string()).collect(),
}
}
fn grant(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn required_missing_returns_boot_error() {
let decls = vec![(
"agent-creator".into(),
decl(&["agents_crud", "credentials_crud"], &[]),
)];
let mut grants_map = HashMap::new();
grants_map.insert("agent-creator".into(), grant(&["agents_crud"]));
let report = validate_capabilities_at_boot(&decls, &grants_map);
assert_eq!(report.errors.len(), 1);
match &report.errors[0] {
CapabilityBootError::RequiredNotGranted {
microapp_id,
missing,
} => {
assert_eq!(microapp_id, "agent-creator");
assert_eq!(missing, &vec!["credentials_crud".to_string()]);
}
}
}
#[test]
fn optional_missing_returns_warn_not_error() {
let decls = vec![(
"agent-creator".into(),
decl(&["agents_crud"], &["llm_keys_crud"]),
)];
let mut grants_map = HashMap::new();
grants_map.insert("agent-creator".into(), grant(&["agents_crud"]));
let report = validate_capabilities_at_boot(&decls, &grants_map);
assert!(report.errors.is_empty());
assert_eq!(report.warns.len(), 1);
match &report.warns[0] {
CapabilityBootWarn::OptionalNotGranted { missing, .. } => {
assert_eq!(missing, &vec!["llm_keys_crud".to_string()]);
}
other => panic!("expected OptionalNotGranted, got {other:?}"),
}
}
#[test]
fn orphan_grant_returns_warn() {
let decls = vec![("agent-creator".into(), decl(&["agents_crud"], &[]))];
let mut grants_map = HashMap::new();
grants_map.insert(
"agent-creator".into(),
grant(&["agents_crud", "future_capability"]),
);
let report = validate_capabilities_at_boot(&decls, &grants_map);
assert!(report.errors.is_empty());
assert_eq!(report.warns.len(), 1);
match &report.warns[0] {
CapabilityBootWarn::OrphanGrant { orphan, .. } => {
assert_eq!(orphan, &vec!["future_capability".to_string()]);
}
other => panic!("expected OrphanGrant, got {other:?}"),
}
}
#[test]
fn all_satisfied_no_errors_no_warns() {
let decls = vec![(
"agent-creator".into(),
decl(&["agents_crud"], &["llm_keys_crud"]),
)];
let mut grants_map = HashMap::new();
grants_map.insert(
"agent-creator".into(),
grant(&["agents_crud", "llm_keys_crud"]),
);
let report = validate_capabilities_at_boot(&decls, &grants_map);
assert!(report.errors.is_empty());
assert!(report.warns.is_empty());
}
#[test]
fn capability_set_check_lookup() {
let mut grants = HashMap::new();
grants.insert(
"agent-creator".to_string(),
HashSet::from(["agents_crud".to_string()]),
);
let set = CapabilitySet::from_grants(grants);
assert!(set.check("agent-creator", "agents_crud"));
assert!(!set.check("agent-creator", "credentials_crud"));
assert!(!set.check("unknown-app", "agents_crud"));
}
#[test]
fn capability_set_empty_denies_everything() {
let set = CapabilitySet::empty();
assert!(!set.check("any-app", "any_capability"));
}
}