agent_core_runtime/permissions/
grant.rs1use super::{GrantTarget, PermissionLevel};
7use serde::{Deserialize, Serialize};
8use std::time::Instant;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct Grant {
19 pub target: GrantTarget,
21 pub level: PermissionLevel,
23 #[serde(skip)]
25 pub expires: Option<Instant>,
26}
27
28impl Grant {
29 pub fn new(target: GrantTarget, level: PermissionLevel) -> Self {
33 Self {
34 target,
35 level,
36 expires: None,
37 }
38 }
39
40 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 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 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 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 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 pub fn domain(pattern: impl Into<String>, level: PermissionLevel) -> Self {
71 Self::new(GrantTarget::domain(pattern), level)
72 }
73
74 pub fn command(pattern: impl Into<String>, level: PermissionLevel) -> Self {
76 Self::new(GrantTarget::command(pattern), level)
77 }
78
79 pub fn satisfies(&self, request: &PermissionRequest) -> bool {
92 if let Some(expires) = self.expires {
94 if Instant::now() >= expires {
95 return false;
96 }
97 }
98
99 if !self.target.covers(&request.target) {
101 return false;
102 }
103
104 self.level.satisfies(request.required_level)
106 }
107
108 pub fn is_expired(&self) -> bool {
110 self.expires.map_or(false, |e| Instant::now() >= e)
111 }
112
113 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct PermissionRequest {
134 pub id: String,
136 pub target: GrantTarget,
138 pub required_level: PermissionLevel,
140 pub description: String,
142 pub reason: Option<String>,
144 pub tool_name: Option<String>,
146}
147
148impl PermissionRequest {
149 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 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
168 self.reason = Some(reason.into());
169 self
170 }
171
172 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 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 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 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 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 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}