Skip to main content

agent_core_runtime/permissions/
mod.rs

1//! Permission system for controlling agent access to resources.
2//!
3//! This module implements a grant-based permission system where permissions are
4//! represented as tuples of `(Target, Level)`:
5//!
6//! - **Target**: What resource is being accessed (path, domain, or command)
7//! - **Level**: What level of access is permitted (read, write, execute, admin)
8//!
9//! ## Permission Levels
10//!
11//! Permission levels form a hierarchy where higher levels imply lower levels:
12//!
13//! ```text
14//! Admin > Execute > Write > Read > None
15//! ```
16//!
17//! For example, a `Write` grant automatically allows `Read` operations.
18//!
19//! ## Target Types
20//!
21//! | Target | Controls | Examples |
22//! |--------|----------|----------|
23//! | `Path` | Files and directories | `/project/src`, `/home/user/.config` |
24//! | `Domain` | Network endpoints | `api.github.com`, `*.anthropic.com` |
25//! | `Command` | Shell commands | `git *`, `cargo build` |
26//!
27//! ## Batch Requests
28//!
29//! When multiple tools run in parallel, their permission requests can be
30//! batched together for a single UI prompt, avoiding deadlocks and reducing
31//! user friction.
32//!
33//! ## Example
34//!
35//! ```
36//! use agent_core_runtime::permissions::{Grant, GrantTarget, PermissionLevel, PermissionRequest};
37//!
38//! // Create a grant for writing to /project/src recursively
39//! let grant = Grant::write_path("/project/src", true);
40//!
41//! // Create a request to write a file
42//! let request = PermissionRequest::file_write("req-1", "/project/src/main.rs");
43//!
44//! // Check if the grant satisfies the request
45//! assert!(grant.satisfies(&request));
46//!
47//! // Write grant also satisfies read requests (level hierarchy)
48//! let read_request = PermissionRequest::file_read("req-2", "/project/src/lib.rs");
49//! assert!(grant.satisfies(&read_request));
50//! ```
51
52mod batch;
53mod grant;
54mod level;
55mod registry;
56mod target;
57mod tool_mapping;
58
59pub use batch::{
60    compute_suggested_grants, BatchAction, BatchPermissionRequest, BatchPermissionResponse,
61};
62pub use grant::{Grant, PermissionRequest};
63pub use level::PermissionLevel;
64pub use registry::{
65    generate_batch_id, PendingPermissionInfo, PermissionError, PermissionPanelResponse,
66    PermissionRegistry,
67};
68pub use target::GrantTarget;
69pub use tool_mapping::{get_tool_category, ToolCategory, ToolPermissions};
70
71#[cfg(test)]
72mod integration_tests {
73    use super::*;
74
75    #[test]
76    fn test_end_to_end_permission_check() {
77        // Simulate granting read access to a project
78        let grant = Grant::read_path("/project", true);
79
80        // Various read requests should be satisfied
81        let requests = vec![
82            PermissionRequest::file_read("1", "/project/src/main.rs"),
83            PermissionRequest::file_read("2", "/project/Cargo.toml"),
84            PermissionRequest::directory_read("3", "/project/src", true),
85        ];
86
87        for request in &requests {
88            assert!(
89                grant.satisfies(request),
90                "Grant should satisfy request: {}",
91                request.id
92            );
93        }
94
95        // Write request should NOT be satisfied
96        let write_request = PermissionRequest::file_write("4", "/project/src/main.rs");
97        assert!(
98            !grant.satisfies(&write_request),
99            "Read grant should not satisfy write request"
100        );
101
102        // Request outside the grant path should NOT be satisfied
103        let outside_request = PermissionRequest::file_read("5", "/other/file.rs");
104        assert!(
105            !grant.satisfies(&outside_request),
106            "Grant should not satisfy request outside path"
107        );
108    }
109
110    #[test]
111    fn test_level_hierarchy() {
112        // Admin grant should satisfy all levels
113        let admin_grant = Grant::admin_path("/project", true);
114
115        let requests = vec![
116            PermissionRequest::file_read("1", "/project/file.rs"),
117            PermissionRequest::file_write("2", "/project/file.rs"),
118            PermissionRequest::new(
119                "3",
120                GrantTarget::path("/project/file.rs", false),
121                PermissionLevel::Execute,
122                "Execute",
123            ),
124            PermissionRequest::new(
125                "4",
126                GrantTarget::path("/project/file.rs", false),
127                PermissionLevel::Admin,
128                "Admin",
129            ),
130        ];
131
132        for request in &requests {
133            assert!(
134                admin_grant.satisfies(request),
135                "Admin grant should satisfy {} level request",
136                request.required_level
137            );
138        }
139    }
140
141    #[test]
142    fn test_batch_permission_flow() {
143        // Simulate parallel tool execution requesting permissions
144        let requests = vec![
145            PermissionRequest::file_read("tool-1", "/project/src/main.rs"),
146            PermissionRequest::file_read("tool-2", "/project/src/lib.rs"),
147            PermissionRequest::file_read("tool-3", "/project/Cargo.toml"),
148        ];
149
150        // Create batch request
151        let batch = BatchPermissionRequest::new("batch-1", requests.clone());
152
153        // Verify suggestions were computed
154        assert!(!batch.suggested_grants.is_empty());
155
156        // Simulate user approving with a recursive grant
157        let approved_grant = Grant::read_path("/project", true);
158        let response = BatchPermissionResponse::all_granted("batch-1", vec![approved_grant]);
159
160        // All requests should now be satisfied
161        for request in &requests {
162            assert!(
163                response.is_granted(&request.id, request),
164                "Request {} should be granted",
165                request.id
166            );
167        }
168    }
169
170    #[test]
171    fn test_different_target_types_independent() {
172        // Path grant should not satisfy domain request
173        let path_grant = Grant::read_path("/project", true);
174        let domain_request =
175            PermissionRequest::network_access("1", "api.github.com", PermissionLevel::Read);
176        assert!(!path_grant.satisfies(&domain_request));
177
178        // Domain grant should not satisfy command request
179        let domain_grant = Grant::domain("*", PermissionLevel::Admin);
180        let cmd_request = PermissionRequest::command_execute("2", "git status");
181        assert!(!domain_grant.satisfies(&cmd_request));
182
183        // Command grant should not satisfy path request
184        let cmd_grant = Grant::command("*", PermissionLevel::Admin);
185        let path_request = PermissionRequest::file_read("3", "/project/file.rs");
186        assert!(!cmd_grant.satisfies(&path_request));
187    }
188
189    #[test]
190    fn test_serialization_roundtrip() {
191        let grant = Grant::write_path("/project/src", true);
192        let json = serde_json::to_string(&grant).unwrap();
193        let deserialized: Grant = serde_json::from_str(&json).unwrap();
194
195        assert_eq!(grant.level, deserialized.level);
196        assert_eq!(grant.target, deserialized.target);
197
198        let request = PermissionRequest::file_write("test", "/project/src/main.rs")
199            .with_reason("Testing")
200            .with_tool("write_file");
201        let json = serde_json::to_string(&request).unwrap();
202        let deserialized: PermissionRequest = serde_json::from_str(&json).unwrap();
203
204        assert_eq!(request.id, deserialized.id);
205        assert_eq!(request.reason, deserialized.reason);
206        assert_eq!(request.tool_name, deserialized.tool_name);
207    }
208}