1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case", tag = "type", content = "scope")]
11pub enum Capability {
12 FileRead(String),
14 FileWrite(String),
16 ShellExec(String),
18 Network(String),
20 Memory,
22 KnowledgeGraph,
24 BrowserControl,
26 AgentSpawn,
28 AgentMessage,
30 Schedule,
32 EventPublish,
34 SourceControl,
36 Container,
38 DataManipulation,
40 CodeAnalysis,
42 Archive,
44 Template,
46 Crypto,
48}
49
50impl std::fmt::Display for Capability {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::FileRead(g) => write!(f, "file_read({})", g),
54 Self::FileWrite(g) => write!(f, "file_write({})", g),
55 Self::ShellExec(p) => write!(f, "shell_exec({})", p),
56 Self::Network(h) => write!(f, "network({})", h),
57 Self::Memory => write!(f, "memory"),
58 Self::KnowledgeGraph => write!(f, "knowledge_graph"),
59 Self::BrowserControl => write!(f, "browser_control"),
60 Self::AgentSpawn => write!(f, "agent_spawn"),
61 Self::AgentMessage => write!(f, "agent_message"),
62 Self::Schedule => write!(f, "schedule"),
63 Self::EventPublish => write!(f, "event_publish"),
64 Self::SourceControl => write!(f, "source_control"),
65 Self::Container => write!(f, "container"),
66 Self::DataManipulation => write!(f, "data_manipulation"),
67 Self::CodeAnalysis => write!(f, "code_analysis"),
68 Self::Archive => write!(f, "archive"),
69 Self::Template => write!(f, "template"),
70 Self::Crypto => write!(f, "crypto"),
71 }
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct CapabilityGrant {
78 pub id: Uuid,
80 pub capability: Capability,
82 pub granted_by: String,
84 pub granted_at: DateTime<Utc>,
86 pub expires_at: Option<DateTime<Utc>>,
88}
89
90pub fn capability_matches(granted: &Capability, required: &Capability) -> bool {
95 match (granted, required) {
96 (Capability::FileRead(granted_glob), Capability::FileRead(required_path)) => {
97 glob_matches(granted_glob, required_path)
98 }
99 (Capability::FileWrite(granted_glob), Capability::FileWrite(required_path)) => {
100 glob_matches(granted_glob, required_path)
101 }
102 (Capability::ShellExec(granted_pat), Capability::ShellExec(required_cmd)) => {
103 pattern_matches(granted_pat, required_cmd)
104 }
105 (Capability::Network(granted_host), Capability::Network(required_host)) => {
106 host_matches(granted_host, required_host)
107 }
108 (Capability::Memory, Capability::Memory) => true,
109 (Capability::KnowledgeGraph, Capability::KnowledgeGraph) => true,
110 (Capability::BrowserControl, Capability::BrowserControl) => true,
111 (Capability::AgentSpawn, Capability::AgentSpawn) => true,
112 (Capability::AgentMessage, Capability::AgentMessage) => true,
113 (Capability::Schedule, Capability::Schedule) => true,
114 (Capability::EventPublish, Capability::EventPublish) => true,
115 (Capability::SourceControl, Capability::SourceControl) => true,
116 (Capability::Container, Capability::Container) => true,
117 (Capability::DataManipulation, Capability::DataManipulation) => true,
118 (Capability::CodeAnalysis, Capability::CodeAnalysis) => true,
119 (Capability::Archive, Capability::Archive) => true,
120 (Capability::Template, Capability::Template) => true,
121 (Capability::Crypto, Capability::Crypto) => true,
122 _ => false,
123 }
124}
125
126fn glob_matches(pattern: &str, path: &str) -> bool {
128 if pattern == "**" || pattern == "**/*" {
129 return true;
130 }
131 glob::Pattern::new(pattern)
132 .map(|p| p.matches(path))
133 .unwrap_or(false)
134}
135
136fn pattern_matches(pattern: &str, command: &str) -> bool {
138 if pattern == "*" {
139 return true;
140 }
141 glob::Pattern::new(pattern)
142 .map(|p| p.matches(command))
143 .unwrap_or(false)
144}
145
146fn host_matches(pattern: &str, host: &str) -> bool {
148 if pattern == "*" {
149 return true;
150 }
151 if let Some(suffix) = pattern.strip_prefix("*.") {
152 return host == suffix || host.ends_with(&format!(".{}", suffix));
153 }
154 pattern == host
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_glob_file_read() {
163 let granted = Capability::FileRead("src/**/*.rs".to_string());
164 let required = Capability::FileRead("src/main.rs".to_string());
165 assert!(capability_matches(&granted, &required));
166 }
167
168 #[test]
169 fn test_wildcard_grants_all() {
170 let granted = Capability::FileRead("**".to_string());
171 let required = Capability::FileRead("anything/at/all.txt".to_string());
172 assert!(capability_matches(&granted, &required));
173 }
174
175 #[test]
176 fn test_glob_no_match() {
177 let granted = Capability::FileRead("src/**/*.rs".to_string());
178 let required = Capability::FileRead("tests/data.json".to_string());
179 assert!(!capability_matches(&granted, &required));
180 }
181
182 #[test]
183 fn test_variant_mismatch() {
184 let granted = Capability::FileRead("**".to_string());
185 let required = Capability::FileWrite("foo.txt".to_string());
186 assert!(!capability_matches(&granted, &required));
187 }
188
189 #[test]
190 fn test_scopeless_capabilities() {
191 assert!(capability_matches(&Capability::Memory, &Capability::Memory));
192 assert!(!capability_matches(
193 &Capability::Memory,
194 &Capability::Schedule
195 ));
196 }
197
198 #[test]
199 fn test_wildcard_host() {
200 let granted = Capability::Network("*.example.com".to_string());
201 let required = Capability::Network("api.example.com".to_string());
202 assert!(capability_matches(&granted, &required));
203 }
204
205 #[test]
206 fn test_exact_host() {
207 let granted = Capability::Network("api.example.com".to_string());
208 let required = Capability::Network("api.example.com".to_string());
209 assert!(capability_matches(&granted, &required));
210
211 let other = Capability::Network("other.example.com".to_string());
212 assert!(!capability_matches(&granted, &other));
213 }
214
215 #[test]
216 fn test_capability_display_all_scoped() {
217 assert_eq!(
218 Capability::FileRead("src/*.rs".to_string()).to_string(),
219 "file_read(src/*.rs)"
220 );
221 assert_eq!(
222 Capability::FileWrite("out/**".to_string()).to_string(),
223 "file_write(out/**)"
224 );
225 assert_eq!(
226 Capability::ShellExec("ls*".to_string()).to_string(),
227 "shell_exec(ls*)"
228 );
229 assert_eq!(
230 Capability::Network("*.example.com".to_string()).to_string(),
231 "network(*.example.com)"
232 );
233 }
234
235 #[test]
236 fn test_capability_display_all_scopeless() {
237 assert_eq!(Capability::Memory.to_string(), "memory");
238 assert_eq!(Capability::KnowledgeGraph.to_string(), "knowledge_graph");
239 assert_eq!(Capability::BrowserControl.to_string(), "browser_control");
240 assert_eq!(Capability::AgentSpawn.to_string(), "agent_spawn");
241 assert_eq!(Capability::AgentMessage.to_string(), "agent_message");
242 assert_eq!(Capability::Schedule.to_string(), "schedule");
243 assert_eq!(Capability::EventPublish.to_string(), "event_publish");
244 assert_eq!(Capability::SourceControl.to_string(), "source_control");
245 assert_eq!(Capability::Container.to_string(), "container");
246 assert_eq!(
247 Capability::DataManipulation.to_string(),
248 "data_manipulation"
249 );
250 assert_eq!(Capability::CodeAnalysis.to_string(), "code_analysis");
251 assert_eq!(Capability::Archive.to_string(), "archive");
252 assert_eq!(Capability::Template.to_string(), "template");
253 assert_eq!(Capability::Crypto.to_string(), "crypto");
254 }
255
256 #[test]
257 fn test_all_scopeless_capability_matches() {
258 let scopeless = vec![
259 Capability::Memory,
260 Capability::KnowledgeGraph,
261 Capability::BrowserControl,
262 Capability::AgentSpawn,
263 Capability::AgentMessage,
264 Capability::Schedule,
265 Capability::EventPublish,
266 Capability::SourceControl,
267 Capability::Container,
268 Capability::DataManipulation,
269 Capability::CodeAnalysis,
270 Capability::Archive,
271 Capability::Template,
272 Capability::Crypto,
273 ];
274 for cap in &scopeless {
275 assert!(capability_matches(cap, cap), "{} should match itself", cap);
276 }
277 assert!(!capability_matches(
279 &Capability::Memory,
280 &Capability::Schedule
281 ));
282 assert!(!capability_matches(
283 &Capability::Archive,
284 &Capability::Template
285 ));
286 }
287
288 #[test]
289 fn test_capability_serde_roundtrip() {
290 let caps = vec![
291 Capability::FileRead("**/*.rs".to_string()),
292 Capability::FileWrite("out/**".to_string()),
293 Capability::ShellExec("*".to_string()),
294 Capability::Network("*.api.com".to_string()),
295 Capability::Memory,
296 Capability::BrowserControl,
297 Capability::Crypto,
298 ];
299 for cap in &caps {
300 let json = serde_json::to_string(cap).expect("serialize");
301 let deser: Capability = serde_json::from_str(&json).expect("deserialize");
302 assert_eq!(&deser, cap);
303 }
304 }
305
306 #[test]
307 fn test_glob_matches_star_star_slash_star() {
308 let granted = Capability::FileRead("**/*".to_string());
309 let required = Capability::FileRead("deep/nested/file.txt".to_string());
310 assert!(capability_matches(&granted, &required));
311 }
312
313 #[test]
314 fn test_shell_wildcard_grants_all() {
315 let granted = Capability::ShellExec("*".to_string());
316 let required = Capability::ShellExec("rm -rf /".to_string());
317 assert!(capability_matches(&granted, &required));
318 }
319
320 #[test]
321 fn test_network_wildcard_grants_all() {
322 let granted = Capability::Network("*".to_string());
323 let required = Capability::Network("any.host.com".to_string());
324 assert!(capability_matches(&granted, &required));
325 }
326
327 #[test]
328 fn test_subdomain_wildcard_host() {
329 let granted = Capability::Network("*.example.com".to_string());
330 assert!(capability_matches(
332 &granted,
333 &Capability::Network("example.com".to_string())
334 ));
335 assert!(capability_matches(
337 &granted,
338 &Capability::Network("deep.sub.example.com".to_string())
339 ));
340 }
341
342 #[test]
343 fn test_capability_grant_construction() {
344 let grant = CapabilityGrant {
345 id: Uuid::new_v4(),
346 capability: Capability::Memory,
347 granted_by: "admin".to_string(),
348 granted_at: chrono::Utc::now(),
349 expires_at: None,
350 };
351 assert_eq!(grant.granted_by, "admin");
352 assert!(grant.expires_at.is_none());
353 }
354
355 #[test]
356 fn test_capability_grant_with_expiry() {
357 let grant = CapabilityGrant {
358 id: Uuid::new_v4(),
359 capability: Capability::Network("*".to_string()),
360 granted_by: "system".to_string(),
361 granted_at: chrono::Utc::now(),
362 expires_at: Some(chrono::Utc::now()),
363 };
364 assert!(grant.expires_at.is_some());
365 }
366}