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}