Skip to main content

agent_core_runtime/permissions/
grant.rs

1//! Grant structure combining target and permission level.
2//!
3//! A Grant is the fundamental unit of the permission system, representing
4//! the tuple `(Target, Level)` - what resource is being accessed and how.
5
6use super::{GrantTarget, PermissionLevel};
7use serde::{Deserialize, Serialize};
8use std::time::Instant;
9
10/// A permission grant combining a target and permission level.
11///
12/// Grants are the core building blocks of the permission system. Each grant
13/// specifies:
14/// - What can be accessed (the target)
15/// - What level of access is permitted (the level)
16/// - When the grant expires (optional)
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct Grant {
19    /// What this grant applies to (path, domain, or command).
20    pub target: GrantTarget,
21    /// The permission level granted.
22    pub level: PermissionLevel,
23    /// Optional expiration time. If `None`, grant lasts for the session.
24    #[serde(skip)]
25    pub expires: Option<Instant>,
26}
27
28impl Grant {
29    /// Creates a new grant with the given target and level.
30    ///
31    /// The grant has no expiration (session-scoped).
32    pub fn new(target: GrantTarget, level: PermissionLevel) -> Self {
33        Self {
34            target,
35            level,
36            expires: None,
37        }
38    }
39
40    /// Creates a new grant with an expiration time.
41    pub fn with_expiration(target: GrantTarget, level: PermissionLevel, expires: Instant) -> Self {
42        Self {
43            target,
44            level,
45            expires: Some(expires),
46        }
47    }
48
49    /// Creates a read grant for a path.
50    pub fn read_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
51        Self::new(GrantTarget::path(path, recursive), PermissionLevel::Read)
52    }
53
54    /// Creates a write grant for a path.
55    pub fn write_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
56        Self::new(GrantTarget::path(path, recursive), PermissionLevel::Write)
57    }
58
59    /// Creates an execute grant for a path.
60    pub fn execute_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
61        Self::new(GrantTarget::path(path, recursive), PermissionLevel::Execute)
62    }
63
64    /// Creates an admin grant for a path.
65    pub fn admin_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
66        Self::new(GrantTarget::path(path, recursive), PermissionLevel::Admin)
67    }
68
69    /// Creates a grant for a network domain.
70    pub fn domain(pattern: impl Into<String>, level: PermissionLevel) -> Self {
71        Self::new(GrantTarget::domain(pattern), level)
72    }
73
74    /// Creates a grant for a shell command pattern.
75    pub fn command(pattern: impl Into<String>, level: PermissionLevel) -> Self {
76        Self::new(GrantTarget::command(pattern), level)
77    }
78
79    /// Checks if this grant satisfies a permission request.
80    ///
81    /// A grant satisfies a request if:
82    /// 1. The grant's target covers the request's target
83    /// 2. The grant's level satisfies the request's required level
84    /// 3. The grant has not expired
85    ///
86    /// # Arguments
87    /// * `request` - The permission request to check against
88    ///
89    /// # Returns
90    /// `true` if this grant satisfies the request
91    pub fn satisfies(&self, request: &PermissionRequest) -> bool {
92        // Check expiration
93        if let Some(expires) = self.expires {
94            if Instant::now() >= expires {
95                return false;
96            }
97        }
98
99        // Check target coverage
100        if !self.target.covers(&request.target) {
101            return false;
102        }
103
104        // Check level hierarchy
105        self.level.satisfies(request.required_level)
106    }
107
108    /// Checks if this grant has expired.
109    pub fn is_expired(&self) -> bool {
110        self.expires.map_or(false, |e| Instant::now() >= e)
111    }
112
113    /// Returns a display-friendly description of this grant.
114    pub fn description(&self) -> String {
115        format!("[{}] {}", self.level, self.target)
116    }
117}
118
119impl std::fmt::Display for Grant {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        write!(f, "{}", self.description())
122    }
123}
124
125/// A request for permission to perform an operation.
126///
127/// Permission requests are generated by tools when they need access to
128/// resources. The request specifies:
129/// - What target is being accessed
130/// - What level of access is needed
131/// - A human-readable description of the operation
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct PermissionRequest {
134    /// Unique identifier for this request.
135    pub id: String,
136    /// The target being accessed.
137    pub target: GrantTarget,
138    /// The required permission level.
139    pub required_level: PermissionLevel,
140    /// Human-readable description of the operation.
141    pub description: String,
142    /// Optional reason explaining why this access is needed.
143    pub reason: Option<String>,
144    /// The tool that generated this request.
145    pub tool_name: Option<String>,
146}
147
148impl PermissionRequest {
149    /// Creates a new permission request.
150    pub fn new(
151        id: impl Into<String>,
152        target: GrantTarget,
153        required_level: PermissionLevel,
154        description: impl Into<String>,
155    ) -> Self {
156        Self {
157            id: id.into(),
158            target,
159            required_level,
160            description: description.into(),
161            reason: None,
162            tool_name: None,
163        }
164    }
165
166    /// Sets the reason for this request.
167    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
168        self.reason = Some(reason.into());
169        self
170    }
171
172    /// Sets the tool name for this request.
173    pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
174        self.tool_name = Some(tool_name.into());
175        self
176    }
177
178    /// Creates a file read request.
179    pub fn file_read(id: impl Into<String>, path: impl Into<std::path::PathBuf>) -> Self {
180        let path = path.into();
181        let description = format!("Read file: {}", path.display());
182        Self::new(
183            id,
184            GrantTarget::path(path, false),
185            PermissionLevel::Read,
186            description,
187        )
188    }
189
190    /// Creates a file write request.
191    pub fn file_write(id: impl Into<String>, path: impl Into<std::path::PathBuf>) -> Self {
192        let path = path.into();
193        let description = format!("Write file: {}", path.display());
194        Self::new(
195            id,
196            GrantTarget::path(path, false),
197            PermissionLevel::Write,
198            description,
199        )
200    }
201
202    /// Creates a directory read request.
203    pub fn directory_read(
204        id: impl Into<String>,
205        path: impl Into<std::path::PathBuf>,
206        recursive: bool,
207    ) -> Self {
208        let path = path.into();
209        let description = if recursive {
210            format!("Read directory (recursive): {}", path.display())
211        } else {
212            format!("Read directory: {}", path.display())
213        };
214        Self::new(
215            id,
216            GrantTarget::path(path, recursive),
217            PermissionLevel::Read,
218            description,
219        )
220    }
221
222    /// Creates a command execution request.
223    pub fn command_execute(id: impl Into<String>, command: impl Into<String>) -> Self {
224        let command = command.into();
225        let description = format!("Execute command: {}", command);
226        Self::new(
227            id,
228            GrantTarget::command(command),
229            PermissionLevel::Execute,
230            description,
231        )
232    }
233
234    /// Creates a network request.
235    pub fn network_access(
236        id: impl Into<String>,
237        domain: impl Into<String>,
238        level: PermissionLevel,
239    ) -> Self {
240        let domain = domain.into();
241        let description = format!("Access domain: {}", domain);
242        Self::new(id, GrantTarget::domain(domain), level, description)
243    }
244}
245
246impl std::fmt::Display for PermissionRequest {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        write!(f, "[{}] {}", self.required_level, self.description)
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    mod grant_tests {
257        use super::*;
258
259        #[test]
260        fn test_grant_satisfies_same_level() {
261            let grant = Grant::read_path("/project/src", true);
262            let request = PermissionRequest::file_read("1", "/project/src/main.rs");
263            assert!(grant.satisfies(&request));
264        }
265
266        #[test]
267        fn test_grant_satisfies_higher_level() {
268            let grant = Grant::write_path("/project/src", true);
269            let request = PermissionRequest::file_read("1", "/project/src/main.rs");
270            assert!(grant.satisfies(&request));
271        }
272
273        #[test]
274        fn test_grant_fails_lower_level() {
275            let grant = Grant::read_path("/project/src", true);
276            let request = PermissionRequest::file_write("1", "/project/src/main.rs");
277            assert!(!grant.satisfies(&request));
278        }
279
280        #[test]
281        fn test_grant_fails_wrong_path() {
282            let grant = Grant::write_path("/project/src", true);
283            let request = PermissionRequest::file_write("1", "/other/file.rs");
284            assert!(!grant.satisfies(&request));
285        }
286
287        #[test]
288        fn test_grant_fails_non_recursive() {
289            let grant = Grant::read_path("/project/src", false);
290            let request = PermissionRequest::file_read("1", "/project/src/utils/mod.rs");
291            assert!(!grant.satisfies(&request));
292        }
293
294        #[test]
295        fn test_admin_grant_satisfies_all_levels() {
296            let grant = Grant::admin_path("/project", true);
297
298            let read_request = PermissionRequest::file_read("1", "/project/src/main.rs");
299            let write_request = PermissionRequest::file_write("2", "/project/src/main.rs");
300
301            assert!(grant.satisfies(&read_request));
302            assert!(grant.satisfies(&write_request));
303        }
304
305        #[test]
306        fn test_domain_grant() {
307            let grant = Grant::domain("*.github.com", PermissionLevel::Read);
308            let request =
309                PermissionRequest::network_access("1", "api.github.com", PermissionLevel::Read);
310            assert!(grant.satisfies(&request));
311        }
312
313        #[test]
314        fn test_command_grant() {
315            let grant = Grant::command("git *", PermissionLevel::Execute);
316            let request = PermissionRequest::command_execute("1", "git status");
317            assert!(grant.satisfies(&request));
318        }
319
320        #[test]
321        fn test_expired_grant() {
322            use std::time::Duration;
323            let expired = Instant::now() - Duration::from_secs(1);
324            let grant =
325                Grant::with_expiration(GrantTarget::path("/project", true), PermissionLevel::Read, expired);
326
327            let request = PermissionRequest::file_read("1", "/project/file.rs");
328            assert!(!grant.satisfies(&request));
329        }
330
331        #[test]
332        fn test_grant_description() {
333            let grant = Grant::write_path("/project/src", true);
334            let desc = grant.description();
335            assert!(desc.contains("Write"));
336            assert!(desc.contains("/project/src"));
337        }
338    }
339
340    mod request_tests {
341        use super::*;
342
343        #[test]
344        fn test_file_read_request() {
345            let request = PermissionRequest::file_read("test-id", "/path/to/file.rs");
346            assert_eq!(request.id, "test-id");
347            assert_eq!(request.required_level, PermissionLevel::Read);
348            assert!(request.description.contains("Read file"));
349        }
350
351        #[test]
352        fn test_file_write_request() {
353            let request = PermissionRequest::file_write("test-id", "/path/to/file.rs");
354            assert_eq!(request.required_level, PermissionLevel::Write);
355            assert!(request.description.contains("Write file"));
356        }
357
358        #[test]
359        fn test_request_with_reason() {
360            let request = PermissionRequest::file_read("1", "/file.rs")
361                .with_reason("Need to analyze the code");
362            assert_eq!(request.reason, Some("Need to analyze the code".to_string()));
363        }
364
365        #[test]
366        fn test_request_with_tool() {
367            let request = PermissionRequest::file_read("1", "/file.rs").with_tool("read_file");
368            assert_eq!(request.tool_name, Some("read_file".to_string()));
369        }
370    }
371
372    mod serialization_tests {
373        use super::*;
374
375        #[test]
376        fn test_grant_serialization() {
377            let grant = Grant::write_path("/project/src", true);
378            let json = serde_json::to_string(&grant).unwrap();
379
380            let deserialized: Grant = serde_json::from_str(&json).unwrap();
381            assert_eq!(deserialized.target, grant.target);
382            assert_eq!(deserialized.level, grant.level);
383        }
384
385        #[test]
386        fn test_request_serialization() {
387            let request =
388                PermissionRequest::file_read("test-id", "/path/to/file.rs").with_reason("testing");
389            let json = serde_json::to_string(&request).unwrap();
390
391            let deserialized: PermissionRequest = serde_json::from_str(&json).unwrap();
392            assert_eq!(deserialized.id, request.id);
393            assert_eq!(deserialized.reason, request.reason);
394        }
395    }
396}