Skip to main content

orcs_auth/
capability.rs

1//! Capability-based permission model.
2//!
3//! Defines the logical capabilities that control *what* operations
4//! a context can perform.
5//!
6//! # Three-Layer Permission Model
7//!
8//! ```text
9//! Effective Permission = Capability(What) ∩ SandboxPolicy(Where) ∩ Session(Who+When)
10//! ```
11//!
12//! - **Capability** controls *what* operations are allowed (logical permissions).
13//! - **SandboxPolicy** controls *where* operations are allowed (resource boundary).
14//! - **Session** controls *who* is acting and *when* (privilege level).
15//!
16//! All layers must permit an operation for it to succeed. Deny wins.
17//!
18//! # Inheritance
19//!
20//! Capabilities are inherited from parent to child with narrowing only:
21//!
22//! ```text
23//! Component (ALL)
24//! ├── Child-A (READ | WRITE)       ← subset of parent
25//! └── SubAgent-B (READ)            ← subset of parent
26//!     └── Child-B-1 (READ)         ← inherited, cannot exceed parent
27//! ```
28//!
29//! A child can never hold a capability that its parent does not hold.
30//!
31//! # Example
32//!
33//! ```
34//! use orcs_auth::Capability;
35//!
36//! // Full access (default)
37//! let all = Capability::ALL;
38//! assert!(all.contains(Capability::READ));
39//! assert!(all.contains(Capability::EXECUTE));
40//!
41//! // Read-only agent
42//! let read_only = Capability::READ;
43//! assert!(read_only.contains(Capability::READ));
44//! assert!(!read_only.contains(Capability::WRITE));
45//!
46//! // Inheritance: child = parent ∩ requested
47//! let parent = Capability::READ | Capability::WRITE;
48//! let requested = Capability::READ | Capability::EXECUTE;
49//! let effective = parent & requested;
50//! assert_eq!(effective, Capability::READ);
51//! ```
52
53use bitflags::bitflags;
54use serde::{Deserialize, Serialize};
55
56bitflags! {
57    /// Logical capabilities that a context can grant to child entities.
58    ///
59    /// Each capability gates a set of operations. Operations require
60    /// the corresponding capability to be present in the context.
61    ///
62    /// | Capability | Operations |
63    /// |------------|------------|
64    /// | [`READ`](Self::READ) | `orcs.read`, `orcs.grep`, `orcs.glob` |
65    /// | [`WRITE`](Self::WRITE) | `orcs.write`, `orcs.mkdir` |
66    /// | [`DELETE`](Self::DELETE) | `orcs.remove`, `orcs.mv` |
67    /// | [`EXECUTE`](Self::EXECUTE) | `orcs.exec` |
68    /// | [`SPAWN`](Self::SPAWN) | `orcs.spawn_child`, `orcs.spawn_runner` |
69    /// | [`LLM`](Self::LLM) | `orcs.llm` |
70    /// | [`HTTP`](Self::HTTP) | `orcs.http` |
71    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
72    pub struct Capability: u16 {
73        /// Read files: `orcs.read`, `orcs.grep`, `orcs.glob`
74        const READ    = 0b0000_0001;
75        /// Write files: `orcs.write`, `orcs.mkdir`
76        const WRITE   = 0b0000_0010;
77        /// Delete/move files: `orcs.remove`, `orcs.mv`
78        const DELETE  = 0b0000_0100;
79        /// Execute commands: `orcs.exec`
80        const EXECUTE = 0b0000_1000;
81        /// Spawn children/runners: `orcs.spawn_child`, `orcs.spawn_runner`
82        const SPAWN   = 0b0001_0000;
83        /// Call LLM: `orcs.llm`
84        const LLM     = 0b0010_0000;
85        /// HTTP requests: `orcs.http`
86        const HTTP    = 0b0100_0000;
87    }
88}
89
90impl Capability {
91    /// All file operations: READ | WRITE | DELETE.
92    pub const FILE_ALL: Self = Self::READ.union(Self::WRITE).union(Self::DELETE);
93
94    /// All capabilities.
95    pub const ALL: Self = Self::FILE_ALL
96        .union(Self::EXECUTE)
97        .union(Self::SPAWN)
98        .union(Self::LLM)
99        .union(Self::HTTP);
100
101    /// Computes the effective capabilities for a child.
102    ///
103    /// Returns the intersection of parent and requested capabilities.
104    /// A child can never exceed its parent's capabilities.
105    ///
106    /// # Arguments
107    ///
108    /// * `parent` - Parent's capabilities
109    /// * `requested` - Requested capabilities for the child
110    ///
111    /// # Returns
112    ///
113    /// `parent & requested` — the effective capability set.
114    ///
115    /// # Example
116    ///
117    /// ```
118    /// use orcs_auth::Capability;
119    ///
120    /// let parent = Capability::READ | Capability::WRITE;
121    /// let requested = Capability::READ | Capability::EXECUTE;
122    /// let effective = Capability::inherit(parent, requested);
123    /// assert_eq!(effective, Capability::READ);
124    /// ```
125    #[must_use]
126    pub fn inherit(parent: Self, requested: Self) -> Self {
127        parent & requested
128    }
129
130    /// Returns a human-readable list of capability names.
131    ///
132    /// # Example
133    ///
134    /// ```
135    /// use orcs_auth::Capability;
136    ///
137    /// let caps = Capability::READ | Capability::WRITE;
138    /// let names = caps.names();
139    /// assert!(names.contains(&"READ"));
140    /// assert!(names.contains(&"WRITE"));
141    /// ```
142    #[must_use]
143    pub fn names(self) -> Vec<&'static str> {
144        let mut names = Vec::new();
145        if self.contains(Self::READ) {
146            names.push("READ");
147        }
148        if self.contains(Self::WRITE) {
149            names.push("WRITE");
150        }
151        if self.contains(Self::DELETE) {
152            names.push("DELETE");
153        }
154        if self.contains(Self::EXECUTE) {
155            names.push("EXECUTE");
156        }
157        if self.contains(Self::SPAWN) {
158            names.push("SPAWN");
159        }
160        if self.contains(Self::LLM) {
161            names.push("LLM");
162        }
163        if self.contains(Self::HTTP) {
164            names.push("HTTP");
165        }
166        names
167    }
168
169    /// Parses a capability name string (case-insensitive).
170    ///
171    /// Unlike `Flags::from_name` (exact match), this accepts
172    /// lowercase input and aliases like "EXEC" for "EXECUTE".
173    ///
174    /// # Example
175    ///
176    /// ```
177    /// use orcs_auth::Capability;
178    ///
179    /// assert_eq!(Capability::parse("read"), Some(Capability::READ));
180    /// assert_eq!(Capability::parse("EXECUTE"), Some(Capability::EXECUTE));
181    /// assert_eq!(Capability::parse("exec"), Some(Capability::EXECUTE));
182    /// assert_eq!(Capability::parse("http"), Some(Capability::HTTP));
183    /// assert_eq!(Capability::parse("unknown"), None);
184    /// ```
185    #[must_use]
186    pub fn parse(name: &str) -> Option<Self> {
187        match name.to_uppercase().as_str() {
188            "READ" => Some(Self::READ),
189            "WRITE" => Some(Self::WRITE),
190            "DELETE" => Some(Self::DELETE),
191            "EXECUTE" | "EXEC" => Some(Self::EXECUTE),
192            "SPAWN" => Some(Self::SPAWN),
193            "LLM" => Some(Self::LLM),
194            "HTTP" => Some(Self::HTTP),
195            "ALL" => Some(Self::ALL),
196            "FILE_ALL" => Some(Self::FILE_ALL),
197            _ => None,
198        }
199    }
200
201    /// Parses a list of capability names into a combined set.
202    ///
203    /// Returns the combined capabilities and a list of unknown names.
204    /// Callers should decide how to handle unknown names (error, warn, etc.)
205    ///
206    /// # Example
207    ///
208    /// ```
209    /// use orcs_auth::Capability;
210    ///
211    /// let (caps, unknown) = Capability::parse_list(&["READ", "WRITE"]);
212    /// assert_eq!(caps, Capability::READ | Capability::WRITE);
213    /// assert!(unknown.is_empty());
214    ///
215    /// let (caps, unknown) = Capability::parse_list(&["READ", "bad"]);
216    /// assert_eq!(caps, Capability::READ);
217    /// assert_eq!(unknown, vec!["bad"]);
218    /// ```
219    #[must_use]
220    pub fn parse_list<'a>(names: &[&'a str]) -> (Self, Vec<&'a str>) {
221        let mut caps = Self::empty();
222        let mut unknown = Vec::new();
223        for name in names {
224            match Self::parse(name) {
225                Some(c) => caps |= c,
226                None => unknown.push(*name),
227            }
228        }
229        (caps, unknown)
230    }
231}
232
233impl std::fmt::Display for Capability {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        let names = self.names();
236        if names.is_empty() {
237            write!(f, "(none)")
238        } else {
239            write!(f, "{}", names.join(" | "))
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn all_contains_every_capability() {
250        assert!(Capability::ALL.contains(Capability::READ));
251        assert!(Capability::ALL.contains(Capability::WRITE));
252        assert!(Capability::ALL.contains(Capability::DELETE));
253        assert!(Capability::ALL.contains(Capability::EXECUTE));
254        assert!(Capability::ALL.contains(Capability::SPAWN));
255        assert!(Capability::ALL.contains(Capability::LLM));
256        assert!(Capability::ALL.contains(Capability::HTTP));
257    }
258
259    #[test]
260    fn file_all_contains_file_ops() {
261        assert!(Capability::FILE_ALL.contains(Capability::READ));
262        assert!(Capability::FILE_ALL.contains(Capability::WRITE));
263        assert!(Capability::FILE_ALL.contains(Capability::DELETE));
264        assert!(!Capability::FILE_ALL.contains(Capability::EXECUTE));
265        assert!(!Capability::FILE_ALL.contains(Capability::SPAWN));
266        assert!(!Capability::FILE_ALL.contains(Capability::LLM));
267        assert!(!Capability::FILE_ALL.contains(Capability::HTTP));
268    }
269
270    #[test]
271    fn inherit_narrows_capabilities() {
272        let parent = Capability::READ | Capability::WRITE | Capability::EXECUTE;
273        let requested = Capability::READ | Capability::DELETE;
274        let effective = Capability::inherit(parent, requested);
275
276        assert_eq!(effective, Capability::READ);
277    }
278
279    #[test]
280    fn inherit_cannot_exceed_parent() {
281        let parent = Capability::READ;
282        let requested = Capability::ALL;
283        let effective = Capability::inherit(parent, requested);
284
285        assert_eq!(effective, Capability::READ);
286    }
287
288    #[test]
289    fn inherit_all_from_all() {
290        let effective = Capability::inherit(Capability::ALL, Capability::ALL);
291        assert_eq!(effective, Capability::ALL);
292    }
293
294    #[test]
295    fn empty_capability() {
296        let empty = Capability::empty();
297        assert!(!empty.contains(Capability::READ));
298        assert_eq!(empty.names(), Vec::<&str>::new());
299        assert_eq!(empty.to_string(), "(none)");
300    }
301
302    #[test]
303    fn names_returns_set_capabilities() {
304        let caps = Capability::READ | Capability::EXECUTE;
305        let names = caps.names();
306        assert_eq!(names, vec!["READ", "EXECUTE"]);
307    }
308
309    #[test]
310    fn parse_case_insensitive() {
311        assert_eq!(Capability::parse("read"), Some(Capability::READ));
312        assert_eq!(Capability::parse("READ"), Some(Capability::READ));
313        assert_eq!(Capability::parse("Read"), Some(Capability::READ));
314        assert_eq!(Capability::parse("exec"), Some(Capability::EXECUTE));
315        assert_eq!(Capability::parse("EXECUTE"), Some(Capability::EXECUTE));
316    }
317
318    #[test]
319    fn parse_unknown_returns_none() {
320        assert_eq!(Capability::parse("NETWORK"), None);
321        assert_eq!(Capability::parse(""), None);
322    }
323
324    #[test]
325    fn parse_list_combines() {
326        let (caps, unknown) = Capability::parse_list(&["READ", "WRITE"]);
327        assert_eq!(caps, Capability::READ | Capability::WRITE);
328        assert!(unknown.is_empty());
329    }
330
331    #[test]
332    fn parse_list_reports_unknown() {
333        let (caps, unknown) = Capability::parse_list(&["READ", "bad", "WRITE", "nope"]);
334        assert_eq!(caps, Capability::READ | Capability::WRITE);
335        assert_eq!(unknown, vec!["bad", "nope"]);
336    }
337
338    #[test]
339    fn display_formatting() {
340        assert_eq!(Capability::READ.to_string(), "READ");
341        assert_eq!(
342            (Capability::READ | Capability::WRITE).to_string(),
343            "READ | WRITE"
344        );
345        assert_eq!(Capability::empty().to_string(), "(none)");
346    }
347
348    #[test]
349    fn serde_roundtrip() {
350        let caps = Capability::READ | Capability::EXECUTE;
351        let json = serde_json::to_string(&caps).expect("serialize");
352        let parsed: Capability = serde_json::from_str(&json).expect("deserialize");
353        assert_eq!(parsed, caps);
354    }
355
356    #[test]
357    fn bitwise_operations() {
358        let a = Capability::READ | Capability::WRITE;
359        let b = Capability::WRITE | Capability::DELETE;
360
361        // Union
362        assert_eq!(
363            a | b,
364            Capability::READ | Capability::WRITE | Capability::DELETE
365        );
366        // Intersection
367        assert_eq!(a & b, Capability::WRITE);
368        // Difference
369        assert_eq!(a - b, Capability::READ);
370    }
371}