Skip to main content

runifold_core/
capability.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::CapabilityId;
7
8/// A capability category.
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
10#[non_exhaustive]
11pub enum CapabilityKind {
12    /// A language or multimodal model.
13    Model,
14    /// A callable tool.
15    Tool,
16    /// Another agent.
17    Agent,
18    /// A readable or writable resource.
19    Resource,
20    /// A renderable prompt contract.
21    Prompt,
22    /// A namespaced extension capability.
23    Extension(String),
24}
25
26/// The external-effect behavior of a capability.
27#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
28#[non_exhaustive]
29pub enum EffectClass {
30    /// No externally visible effect.
31    Pure,
32    /// Reads external state without modifying it.
33    ReadOnly,
34    /// Writes external state and is safe to repeat with the same key.
35    IdempotentWrite,
36    /// Writes external state and may not be safe to repeat.
37    NonIdempotentWrite,
38    /// May destroy or irreversibly mutate state.
39    Destructive,
40    /// Effect behavior is unknown.
41    Unknown,
42}
43
44/// A coarse capability risk classification.
45#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
46#[non_exhaustive]
47pub enum RiskLevel {
48    /// No known meaningful external risk.
49    Low,
50    /// Requires normal policy evaluation.
51    Medium,
52    /// Requires elevated scrutiny or approval.
53    High,
54    /// Should be denied unless explicitly approved.
55    Critical,
56}
57
58/// A versioned description of a grantable capability.
59#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
60pub struct CapabilityDescriptor {
61    /// Stable identity of this capability instance.
62    pub id: CapabilityId,
63    /// Human-readable name exposed to operators and possibly models.
64    pub name: String,
65    /// Semantic contract version.
66    pub version: String,
67    /// Capability category.
68    pub kind: CapabilityKind,
69    /// JSON Schema for invocation input.
70    pub input_schema: Value,
71    /// JSON Schema for invocation output.
72    pub output_schema: Value,
73    /// External-effect classification.
74    pub effect: EffectClass,
75    /// Risk classification.
76    pub risk: RiskLevel,
77    /// Namespaced extension metadata.
78    pub metadata: BTreeMap<String, Value>,
79}
80
81/// An explicit set of capabilities granted to a run.
82#[derive(Clone, Debug, Default)]
83pub struct CapabilitySet {
84    entries: BTreeMap<CapabilityId, CapabilityDescriptor>,
85}
86
87impl CapabilitySet {
88    /// Creates an empty capability set.
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// Grants a capability, replacing a descriptor with the same identity.
94    pub fn grant(&mut self, capability: CapabilityDescriptor) {
95        self.entries.insert(capability.id, capability);
96    }
97
98    /// Revokes a capability by identity.
99    pub fn revoke(&mut self, id: CapabilityId) -> Option<CapabilityDescriptor> {
100        self.entries.remove(&id)
101    }
102
103    /// Returns a granted capability.
104    pub fn get(&self, id: CapabilityId) -> Option<&CapabilityDescriptor> {
105        self.entries.get(&id)
106    }
107
108    /// Returns whether the set contains a capability.
109    pub fn contains(&self, id: CapabilityId) -> bool {
110        self.entries.contains_key(&id)
111    }
112
113    /// Returns whether every capability in this set is also granted by
114    /// `authority`.
115    pub fn is_subset_of(&self, authority: &Self) -> bool {
116        self.entries.keys().all(|id| authority.contains(*id))
117    }
118
119    /// Returns the first capability not granted by `authority`.
120    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    /// Iterates over granted capabilities.
127    pub fn iter(&self) -> impl Iterator<Item = &CapabilityDescriptor> {
128        self.entries.values()
129    }
130
131    /// Returns the number of granted capabilities.
132    pub fn len(&self) -> usize {
133        self.entries.len()
134    }
135
136    /// Returns whether no capabilities are granted.
137    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}