runifold_core/
capability.rs1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::CapabilityId;
7
8#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
10#[non_exhaustive]
11pub enum CapabilityKind {
12 Model,
14 Tool,
16 Agent,
18 Resource,
20 Prompt,
22 Extension(String),
24}
25
26#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
28#[non_exhaustive]
29pub enum EffectClass {
30 Pure,
32 ReadOnly,
34 IdempotentWrite,
36 NonIdempotentWrite,
38 Destructive,
40 Unknown,
42}
43
44#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
46#[non_exhaustive]
47pub enum RiskLevel {
48 Low,
50 Medium,
52 High,
54 Critical,
56}
57
58#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
60pub struct CapabilityDescriptor {
61 pub id: CapabilityId,
63 pub name: String,
65 pub version: String,
67 pub kind: CapabilityKind,
69 pub input_schema: Value,
71 pub output_schema: Value,
73 pub effect: EffectClass,
75 pub risk: RiskLevel,
77 pub metadata: BTreeMap<String, Value>,
79}
80
81#[derive(Clone, Debug, Default)]
83pub struct CapabilitySet {
84 entries: BTreeMap<CapabilityId, CapabilityDescriptor>,
85}
86
87impl CapabilitySet {
88 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn grant(&mut self, capability: CapabilityDescriptor) {
95 self.entries.insert(capability.id, capability);
96 }
97
98 pub fn revoke(&mut self, id: CapabilityId) -> Option<CapabilityDescriptor> {
100 self.entries.remove(&id)
101 }
102
103 pub fn get(&self, id: CapabilityId) -> Option<&CapabilityDescriptor> {
105 self.entries.get(&id)
106 }
107
108 pub fn contains(&self, id: CapabilityId) -> bool {
110 self.entries.contains_key(&id)
111 }
112
113 pub fn is_subset_of(&self, authority: &Self) -> bool {
116 self.entries.keys().all(|id| authority.contains(*id))
117 }
118
119 pub fn first_missing_from(&self, authority: &Self) -> Option<&CapabilityDescriptor> {
121 self.entries
122 .values()
123 .find(|capability| !authority.contains(capability.id))
124 }
125
126 pub fn iter(&self) -> impl Iterator<Item = &CapabilityDescriptor> {
128 self.entries.values()
129 }
130
131 pub fn len(&self) -> usize {
133 self.entries.len()
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.entries.is_empty()
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use std::collections::BTreeMap;
145
146 use serde_json::json;
147
148 use super::{
149 CapabilityDescriptor, CapabilityId, CapabilityKind, CapabilitySet, EffectClass, RiskLevel,
150 };
151
152 fn capability(name: &str) -> CapabilityDescriptor {
153 CapabilityDescriptor {
154 id: CapabilityId::new(),
155 name: name.into(),
156 version: "1".into(),
157 kind: CapabilityKind::Tool,
158 input_schema: json!({}),
159 output_schema: json!({}),
160 effect: EffectClass::Pure,
161 risk: RiskLevel::Low,
162 metadata: BTreeMap::new(),
163 }
164 }
165
166 #[test]
167 fn subset_checks_use_stable_capability_identity() {
168 let granted = capability("granted");
169 let missing = capability("missing");
170 let mut authority = CapabilitySet::new();
171 authority.grant(granted.clone());
172 let mut requested = CapabilitySet::new();
173 requested.grant(granted);
174
175 assert!(requested.is_subset_of(&authority));
176
177 requested.grant(missing.clone());
178
179 assert!(!requested.is_subset_of(&authority));
180 assert_eq!(
181 requested.first_missing_from(&authority).map(|item| item.id),
182 Some(missing.id)
183 );
184 }
185}