1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum Rights {
14 Read,
16 Write,
18 Execute,
20 Grant,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub enum ResourceRef {
27 KernelDomain {
29 domain: String,
31 },
32 Path {
34 pattern: String,
36 },
37 Network {
39 pattern: String,
41 },
42 Named {
44 name: String,
46 },
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct CapabilityEntry {
52 pub resource: ResourceRef,
54 pub rights: Vec<Rights>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub label: Option<String>,
59}
60
61impl CapabilityEntry {
62 pub fn new(resource: ResourceRef, rights: Vec<Rights>) -> Self {
64 Self {
65 resource,
66 rights,
67 label: None,
68 }
69 }
70
71 pub fn has_right(&self, right: Rights) -> bool {
73 self.rights.contains(&right)
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct CSpace {
83 pub agent_id: Uuid,
85 #[serde(default)]
87 pub name: String,
88 entries: HashMap<u32, CapabilityEntry>,
90 next_index: u32,
92}
93
94impl CSpace {
95 pub fn new(agent_id: Uuid) -> Self {
97 Self {
98 agent_id,
99 name: String::new(),
100 entries: HashMap::new(),
101 next_index: 1,
102 }
103 }
104
105 pub fn with_name(agent_id: Uuid, name: &str) -> Self {
107 Self {
108 agent_id,
109 name: name.to_string(),
110 entries: HashMap::new(),
111 next_index: 1,
112 }
113 }
114
115 pub fn insert(&mut self, entry: CapabilityEntry) -> u32 {
117 let idx = self.next_index;
118 self.next_index += 1;
119 self.entries.insert(idx, entry);
120 idx
121 }
122
123 pub fn remove(&mut self, index: u32) -> Option<CapabilityEntry> {
125 self.entries.remove(&index)
126 }
127
128 pub fn can(&self, resource: &ResourceRef, right: Rights) -> bool {
133 self.entries
134 .values()
135 .any(|entry| entry.has_right(right) && resource_matches(&entry.resource, resource))
136 }
137
138 pub fn iter(&self) -> impl Iterator<Item = (&u32, &CapabilityEntry)> {
140 self.entries.iter()
141 }
142
143 pub fn len(&self) -> usize {
145 self.entries.len()
146 }
147
148 pub fn is_empty(&self) -> bool {
150 self.entries.is_empty()
151 }
152}
153
154fn resource_matches(granted: &ResourceRef, required: &ResourceRef) -> bool {
156 match (granted, required) {
157 _ if granted == required => true,
159
160 (ResourceRef::KernelDomain { domain: g }, ResourceRef::KernelDomain { domain: _r }) => {
162 g == "*"
163 }
164
165 (ResourceRef::Path { pattern: g }, ResourceRef::Path { pattern: _r }) => {
167 if g == "*" || g == "/**" {
168 return true;
169 }
170 if let Some(prefix) = g.strip_suffix("/**") {
172 return _r.starts_with(prefix)
173 || _r.starts_with(&format!("{}/", prefix.trim_end_matches('/')));
174 }
175 false
176 }
177
178 (ResourceRef::Network { pattern: g }, ResourceRef::Network { pattern: _r }) => g == "*",
180
181 (ResourceRef::Named { name: g }, ResourceRef::Named { name: _r }) => g == "*",
183
184 _ => false,
185 }
186}
187
188#[derive(Debug)]
190pub struct CSpaceBuilder {
191 agent_id: Uuid,
192 name: String,
193 entries: Vec<CapabilityEntry>,
194}
195
196impl CSpaceBuilder {
197 pub fn new(agent_id: Uuid) -> Self {
199 Self {
200 agent_id,
201 name: String::new(),
202 entries: Vec::new(),
203 }
204 }
205
206 pub fn name(mut self, name: &str) -> Self {
208 self.name = name.to_string();
209 self
210 }
211
212 pub fn grant(mut self, resource: ResourceRef, rights: Vec<Rights>) -> Self {
214 self.entries.push(CapabilityEntry::new(resource, rights));
215 self
216 }
217
218 pub fn all_access(self) -> Self {
220 self.grant(
221 ResourceRef::KernelDomain { domain: "*".into() },
222 vec![Rights::Read, Rights::Write, Rights::Execute, Rights::Grant],
223 )
224 .grant(
225 ResourceRef::Path {
226 pattern: "/**".into(),
227 },
228 vec![Rights::Read, Rights::Write, Rights::Execute],
229 )
230 .grant(
231 ResourceRef::Network {
232 pattern: "*".into(),
233 },
234 vec![Rights::Read, Rights::Write, Rights::Execute],
235 )
236 }
237
238 pub fn standard(self) -> Self {
240 self.grant(
242 ResourceRef::KernelDomain {
243 domain: "read".into(),
244 },
245 vec![Rights::Read, Rights::Execute],
246 )
247 .grant(
248 ResourceRef::KernelDomain {
249 domain: "write".into(),
250 },
251 vec![Rights::Read, Rights::Write, Rights::Execute],
252 )
253 .grant(
254 ResourceRef::KernelDomain {
255 domain: "edit".into(),
256 },
257 vec![Rights::Read, Rights::Write, Rights::Execute],
258 )
259 .grant(
260 ResourceRef::KernelDomain {
261 domain: "bash".into(),
262 },
263 vec![Rights::Read, Rights::Write, Rights::Execute],
264 )
265 .grant(
266 ResourceRef::KernelDomain {
267 domain: "grep".into(),
268 },
269 vec![Rights::Read, Rights::Execute],
270 )
271 .grant(
272 ResourceRef::KernelDomain {
273 domain: "find".into(),
274 },
275 vec![Rights::Read, Rights::Execute],
276 )
277 .grant(
278 ResourceRef::KernelDomain {
279 domain: "ls".into(),
280 },
281 vec![Rights::Read, Rights::Execute],
282 )
283 .grant(
284 ResourceRef::KernelDomain {
285 domain: "memory".into(),
286 },
287 vec![Rights::Read, Rights::Write],
288 )
289 .grant(
290 ResourceRef::Path {
291 pattern: "/workspace/**".into(),
292 },
293 vec![Rights::Read, Rights::Write, Rights::Execute],
294 )
295 }
296
297 pub fn worker(self) -> Self {
299 self.standard()
300 .grant(
301 ResourceRef::KernelDomain {
302 domain: "subagent".into(),
303 },
304 vec![Rights::Execute],
305 )
306 .grant(
307 ResourceRef::Network {
308 pattern: "*".into(),
309 },
310 vec![Rights::Read, Rights::Write],
311 )
312 }
313
314 pub fn build(self) -> CSpace {
316 let mut cspace = CSpace::with_name(self.agent_id, &self.name);
317 for entry in self.entries {
318 cspace.insert(entry);
319 }
320 cspace
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_cspace_insert_and_check() {
330 let id = Uuid::new_v4();
331 let mut cs = CSpace::new(id);
332 cs.insert(CapabilityEntry::new(
333 ResourceRef::KernelDomain {
334 domain: "read".into(),
335 },
336 vec![Rights::Read, Rights::Execute],
337 ));
338
339 assert!(cs.can(
340 &ResourceRef::KernelDomain {
341 domain: "read".into()
342 },
343 Rights::Execute
344 ));
345 assert!(!cs.can(
346 &ResourceRef::KernelDomain {
347 domain: "write".into()
348 },
349 Rights::Execute
350 ));
351 }
352
353 #[test]
354 fn test_cspace_wildcard() {
355 let id = Uuid::new_v4();
356 let mut cs = CSpace::new(id);
357 cs.insert(CapabilityEntry::new(
358 ResourceRef::KernelDomain { domain: "*".into() },
359 vec![Rights::Read, Rights::Write, Rights::Execute],
360 ));
361
362 assert!(cs.can(
363 &ResourceRef::KernelDomain {
364 domain: "anything".into()
365 },
366 Rights::Execute
367 ));
368 }
369
370 #[test]
371 fn test_cspace_path_glob() {
372 let id = Uuid::new_v4();
373 let mut cs = CSpace::new(id);
374 cs.insert(CapabilityEntry::new(
375 ResourceRef::Path {
376 pattern: "/workspace/**".into(),
377 },
378 vec![Rights::Read, Rights::Write],
379 ));
380
381 assert!(cs.can(
382 &ResourceRef::Path {
383 pattern: "/workspace/src/main.rs".into()
384 },
385 Rights::Read
386 ));
387 assert!(!cs.can(
388 &ResourceRef::Path {
389 pattern: "/etc/passwd".into()
390 },
391 Rights::Read
392 ));
393 }
394
395 #[test]
396 fn test_builder_standard() {
397 let id = Uuid::new_v4();
398 let cs = CSpaceBuilder::new(id).standard().build();
399 assert!(cs.can(
400 &ResourceRef::KernelDomain {
401 domain: "read".into()
402 },
403 Rights::Execute
404 ));
405 assert!(cs.can(
406 &ResourceRef::KernelDomain {
407 domain: "memory".into()
408 },
409 Rights::Read
410 ));
411 }
412
413 #[test]
414 fn test_builder_all_access() {
415 let id = Uuid::new_v4();
416 let cs = CSpaceBuilder::new(id).all_access().build();
417 assert!(cs.can(
418 &ResourceRef::KernelDomain {
419 domain: "anything".into()
420 },
421 Rights::Grant
422 ));
423 assert!(cs.can(
424 &ResourceRef::Path {
425 pattern: "/any/path".into()
426 },
427 Rights::Write
428 ));
429 }
430
431 #[test]
432 fn test_builder_worker() {
433 let id = Uuid::new_v4();
434 let cs = CSpaceBuilder::new(id).worker().build();
435 assert!(cs.can(
436 &ResourceRef::KernelDomain {
437 domain: "subagent".into()
438 },
439 Rights::Execute
440 ));
441 assert!(cs.can(
442 &ResourceRef::Network {
443 pattern: "example.com".into()
444 },
445 Rights::Read
446 ));
447 }
448
449 #[test]
450 fn test_remove_capability() {
451 let id = Uuid::new_v4();
452 let mut cs = CSpace::new(id);
453 let idx = cs.insert(CapabilityEntry::new(
454 ResourceRef::KernelDomain {
455 domain: "read".into(),
456 },
457 vec![Rights::Read],
458 ));
459 assert!(cs.remove(idx).is_some());
460 assert!(!cs.can(
461 &ResourceRef::KernelDomain {
462 domain: "read".into()
463 },
464 Rights::Read
465 ));
466 }
467}