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