Skip to main content

oxicode_sdk/security/capability/
types.rs

1//! seL4-style capability types — CSpace, ResourceRef, Rights.
2//!
3//! Provides a capability-space abstraction where each agent holds a set of
4//! typed capability tokens (rights over named resources). Inspired by
5//! seL4's capability model, simplified for SDK use.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11/// Access rights for a capability.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum Rights {
14    /// Read access.
15    Read,
16    /// Write access.
17    Write,
18    /// Execute access (e.g., run a tool or command).
19    Execute,
20    /// Grant (delegate) this capability to another agent.
21    Grant,
22}
23
24/// Reference to a protected resource.
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub enum ResourceRef {
27    /// Kernel-service domain (e.g., a tool name or subsystem).
28    KernelDomain {
29        /// The kernel-service domain identifier (e.g. a tool or subsystem name).
30        domain: String,
31    },
32    /// Filesystem path pattern.
33    Path {
34        /// Glob pattern matching filesystem paths.
35        pattern: String,
36    },
37    /// Network endpoint pattern.
38    Network {
39        /// Glob pattern matching network endpoints (hosts/domains).
40        pattern: String,
41    },
42    /// Arbitrary named resource.
43    Named {
44        /// Free-form resource name.
45        name: String,
46    },
47}
48
49/// A single capability entry: rights over a resource, optionally with metadata.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct CapabilityEntry {
52    /// The resource this capability covers.
53    pub resource: ResourceRef,
54    /// Granted rights.
55    pub rights: Vec<Rights>,
56    /// Optional human-readable label.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub label: Option<String>,
59}
60
61impl CapabilityEntry {
62    /// Create a new entry.
63    pub fn new(resource: ResourceRef, rights: Vec<Rights>) -> Self {
64        Self {
65            resource,
66            rights,
67            label: None,
68        }
69    }
70
71    /// Check if this entry grants the specified right.
72    pub fn has_right(&self, right: Rights) -> bool {
73        self.rights.contains(&right)
74    }
75}
76
77/// Capability Space — an agent's collection of capability tokens.
78///
79/// Inspired by seL4's CSpace. Each agent has exactly one CSpace that
80/// defines what resources it can access and with which rights.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct CSpace {
83    /// The agent that owns this CSpace.
84    pub agent_id: Uuid,
85    /// Human-readable name (for display).
86    #[serde(default)]
87    pub name: String,
88    /// Capability entries keyed by a stable index.
89    entries: HashMap<u32, CapabilityEntry>,
90    /// Next free index.
91    next_index: u32,
92}
93
94impl CSpace {
95    /// Create an empty CSpace for an agent.
96    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    /// Create a CSpace with a human-readable name.
106    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    /// Insert a capability entry, returning its index.
116    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    /// Remove a capability by index.
124    pub fn remove(&mut self, index: u32) -> Option<CapabilityEntry> {
125        self.entries.remove(&index)
126    }
127
128    /// Check if this CSpace grants `right` over `resource`.
129    ///
130    /// Checks for an exact or more-permissive match. For `ResourceRef::Path`
131    /// and `ResourceRef::Network`, a wildcard pattern (`*`) matches anything.
132    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    /// Iterate over all entries.
139    pub fn iter(&self) -> impl Iterator<Item = (&u32, &CapabilityEntry)> {
140        self.entries.iter()
141    }
142
143    /// Number of capability entries.
144    pub fn len(&self) -> usize {
145        self.entries.len()
146    }
147
148    /// Whether the CSpace is empty.
149    pub fn is_empty(&self) -> bool {
150        self.entries.is_empty()
151    }
152}
153
154/// Check if a granted resource reference satisfies a required one.
155fn resource_matches(granted: &ResourceRef, required: &ResourceRef) -> bool {
156    match (granted, required) {
157        // Exact match
158        _ if granted == required => true,
159
160        // KernelDomain: wildcard matches any domain
161        (ResourceRef::KernelDomain { domain: g }, ResourceRef::KernelDomain { domain: _r }) => {
162            g == "*"
163        }
164
165        // Path: wildcard matches any path
166        (ResourceRef::Path { pattern: g }, ResourceRef::Path { pattern: _r }) => {
167            if g == "*" || g == "/**" {
168                return true;
169            }
170            // Simple prefix/suffix matching
171            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        // Network: wildcard matches any endpoint
179        (ResourceRef::Network { pattern: g }, ResourceRef::Network { pattern: _r }) => g == "*",
180
181        // Named: wildcard matches any name
182        (ResourceRef::Named { name: g }, ResourceRef::Named { name: _r }) => g == "*",
183
184        _ => false,
185    }
186}
187
188/// Builder for constructing a CSpace with standard templates.
189#[derive(Debug)]
190pub struct CSpaceBuilder {
191    agent_id: Uuid,
192    name: String,
193    entries: Vec<CapabilityEntry>,
194}
195
196impl CSpaceBuilder {
197    /// Create a builder for the given agent.
198    pub fn new(agent_id: Uuid) -> Self {
199        Self {
200            agent_id,
201            name: String::new(),
202            entries: Vec::new(),
203        }
204    }
205
206    /// Set a human-readable name.
207    pub fn name(mut self, name: &str) -> Self {
208        self.name = name.to_string();
209        self
210    }
211
212    /// Grant rights over a resource.
213    pub fn grant(mut self, resource: ResourceRef, rights: Vec<Rights>) -> Self {
214        self.entries.push(CapabilityEntry::new(resource, rights));
215        self
216    }
217
218    /// Grant all rights over all resources (admin/superuser).
219    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    /// Standard template: read/write/execute on workspace tools.
239    pub fn standard(self) -> Self {
240        // Essential tools
241        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    /// Worker template: standard + subagent + network.
298    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    /// Build the CSpace.
315    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}