agent_core_runtime/agent/interface/policy.rs
1//! Permission Policy - Automatic handling of permission requests
2//!
3//! The [`PermissionPolicy`] trait allows automatic approval, denial, or
4//! filtering of permission requests before they reach the user.
5
6use crate::permissions::{Grant, PermissionRequest};
7
8/// Decision from a permission policy.
9#[derive(Debug, Clone)]
10pub enum PolicyDecision {
11 /// Allow the request immediately.
12 Allow,
13
14 /// Allow and grant permission for similar future requests.
15 ///
16 /// The grant is stored and used to auto-approve matching requests.
17 AllowWithGrant(Grant),
18
19 /// Deny the request immediately.
20 Deny {
21 /// Optional reason for denial (shown to LLM).
22 reason: Option<String>,
23 },
24
25 /// Defer to the consumer (show prompt to user).
26 ///
27 /// This is the default for interactive frontends.
28 AskUser,
29}
30
31impl PolicyDecision {
32 /// Create an Allow decision.
33 pub fn allow() -> Self {
34 Self::Allow
35 }
36
37 /// Create an AllowWithGrant decision.
38 pub fn allow_with_grant(grant: Grant) -> Self {
39 Self::AllowWithGrant(grant)
40 }
41
42 /// Create a Deny decision with a reason.
43 pub fn deny(reason: impl Into<String>) -> Self {
44 Self::Deny {
45 reason: Some(reason.into()),
46 }
47 }
48
49 /// Create a Deny decision without a reason.
50 pub fn deny_silent() -> Self {
51 Self::Deny { reason: None }
52 }
53
54 /// Create an AskUser decision.
55 pub fn ask_user() -> Self {
56 Self::AskUser
57 }
58}
59
60/// Policy for handling permission requests.
61///
62/// Implement this trait to automatically approve, deny, or filter
63/// permission requests before they reach the user. Useful for:
64///
65/// - **Headless servers**: Auto-approve everything in trusted environments
66/// - **Allowlists**: Only prompt for paths/commands outside a safe list
67/// - **Audit logging**: Log all permission requests regardless of decision
68/// - **Rate limiting**: Deny requests that exceed usage thresholds
69///
70/// # Example: Custom Allowlist Policy
71///
72/// ```ignore
73/// use agent_core_runtime::agent::interface::{PermissionPolicy, PolicyDecision};
74/// use agent_core_runtime::permissions::{PermissionRequest, GrantTarget};
75///
76/// struct AllowlistPolicy {
77/// allowed_paths: Vec<String>,
78/// }
79///
80/// impl PermissionPolicy for AllowlistPolicy {
81/// fn decide(&self, request: &PermissionRequest) -> PolicyDecision {
82/// match &request.target {
83/// GrantTarget::Path { path, .. } => {
84/// let path_str = path.to_string_lossy();
85/// if self.allowed_paths.iter().any(|p| path_str.starts_with(p)) {
86/// PolicyDecision::Allow
87/// } else {
88/// PolicyDecision::AskUser
89/// }
90/// }
91/// _ => PolicyDecision::AskUser,
92/// }
93/// }
94/// }
95/// ```
96pub trait PermissionPolicy: Send + Sync + 'static {
97 /// Decide how to handle a permission request.
98 ///
99 /// Called before the request is sent to the consumer. Return:
100 /// - `Allow` or `AllowWithGrant` to approve immediately
101 /// - `Deny` to reject immediately
102 /// - `AskUser` to forward to the consumer for user decision
103 fn decide(&self, request: &PermissionRequest) -> PolicyDecision;
104
105 /// Whether this policy supports interactive user questions.
106 ///
107 /// Returns `false` for headless/auto-approve policies, causing
108 /// `UserInteractionRequired` events to be auto-cancelled (the tool
109 /// receives "User declined to answer").
110 ///
111 /// Returns `true` (default) for interactive policies where a user
112 /// can answer questions.
113 fn supports_interaction(&self) -> bool {
114 true
115 }
116}
117
118/// Auto-approve all permission requests.
119///
120/// Use this policy in trusted environments where all tool operations
121/// should be allowed without user interaction.
122///
123/// # Warning
124///
125/// This grants full access to file system, commands, network, etc.
126/// Only use in controlled environments where you trust the LLM's actions.
127///
128/// # Example
129///
130/// ```ignore
131/// use agent_core_runtime::agent::interface::AutoApprovePolicy;
132///
133/// let policy = AutoApprovePolicy;
134/// // All permission requests will be automatically approved
135/// ```
136#[derive(Debug, Clone, Copy, Default)]
137pub struct AutoApprovePolicy;
138
139impl AutoApprovePolicy {
140 /// Create a new auto-approve policy.
141 pub fn new() -> Self {
142 Self
143 }
144}
145
146impl PermissionPolicy for AutoApprovePolicy {
147 fn decide(&self, _request: &PermissionRequest) -> PolicyDecision {
148 PolicyDecision::Allow
149 }
150
151 fn supports_interaction(&self) -> bool {
152 false // Headless - no user to answer questions
153 }
154}
155
156/// Deny all permission requests.
157///
158/// Use this for read-only or sandboxed environments where no
159/// tool operations should be allowed.
160///
161/// # Example
162///
163/// ```ignore
164/// use agent_core_runtime::agent::interface::DenyAllPolicy;
165///
166/// let policy = DenyAllPolicy::new();
167/// // All permission requests will be denied
168/// ```
169#[derive(Debug, Clone, Default)]
170pub struct DenyAllPolicy {
171 reason: Option<String>,
172}
173
174impl DenyAllPolicy {
175 /// Create a new deny-all policy with a default message.
176 pub fn new() -> Self {
177 Self {
178 reason: Some("Permission denied by policy".to_string()),
179 }
180 }
181
182 /// Create a deny-all policy with a custom reason.
183 pub fn with_reason(reason: impl Into<String>) -> Self {
184 Self {
185 reason: Some(reason.into()),
186 }
187 }
188}
189
190impl PermissionPolicy for DenyAllPolicy {
191 fn decide(&self, _request: &PermissionRequest) -> PolicyDecision {
192 PolicyDecision::Deny {
193 reason: self.reason.clone(),
194 }
195 }
196}
197
198/// Interactive policy - always ask the user.
199///
200/// This is the default policy used by the TUI. All permission
201/// requests are forwarded to the consumer for user decision.
202///
203/// # Example
204///
205/// ```ignore
206/// use agent_core_runtime::agent::interface::InteractivePolicy;
207///
208/// let policy = InteractivePolicy;
209/// // All permission requests will prompt the user
210/// ```
211#[derive(Debug, Clone, Copy, Default)]
212pub struct InteractivePolicy;
213
214impl InteractivePolicy {
215 /// Create a new interactive policy.
216 pub fn new() -> Self {
217 Self
218 }
219}
220
221impl PermissionPolicy for InteractivePolicy {
222 fn decide(&self, _request: &PermissionRequest) -> PolicyDecision {
223 PolicyDecision::AskUser
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::permissions::{GrantTarget, PermissionLevel};
231 use std::path::PathBuf;
232
233 fn make_request(target: GrantTarget, level: PermissionLevel) -> PermissionRequest {
234 PermissionRequest {
235 id: "test-id".to_string(),
236 target,
237 required_level: level,
238 description: "Test operation".to_string(),
239 reason: None,
240 tool_name: Some("test_tool".to_string()),
241 }
242 }
243
244 #[test]
245 fn test_auto_approve_policy() {
246 let policy = AutoApprovePolicy::new();
247 let request = make_request(
248 GrantTarget::Path {
249 path: PathBuf::from("/etc/passwd"),
250 recursive: false,
251 },
252 PermissionLevel::Read,
253 );
254
255 match policy.decide(&request) {
256 PolicyDecision::Allow => {}
257 other => panic!("Expected Allow, got {:?}", other),
258 }
259 }
260
261 #[test]
262 fn test_deny_all_policy() {
263 let policy = DenyAllPolicy::new();
264 let request = make_request(
265 GrantTarget::Path {
266 path: PathBuf::from("/tmp/test"),
267 recursive: false,
268 },
269 PermissionLevel::Write,
270 );
271
272 match policy.decide(&request) {
273 PolicyDecision::Deny { reason } => {
274 assert!(reason.is_some());
275 }
276 other => panic!("Expected Deny, got {:?}", other),
277 }
278 }
279
280 #[test]
281 fn test_deny_all_policy_custom_reason() {
282 let policy = DenyAllPolicy::with_reason("Sandbox mode");
283 let request = make_request(
284 GrantTarget::Command {
285 pattern: "rm".to_string(),
286 },
287 PermissionLevel::Execute,
288 );
289
290 match policy.decide(&request) {
291 PolicyDecision::Deny { reason } => {
292 assert_eq!(reason, Some("Sandbox mode".to_string()));
293 }
294 other => panic!("Expected Deny, got {:?}", other),
295 }
296 }
297
298 #[test]
299 fn test_interactive_policy() {
300 let policy = InteractivePolicy::new();
301 let request = make_request(
302 GrantTarget::Domain {
303 pattern: "api.example.com".to_string(),
304 },
305 PermissionLevel::Read,
306 );
307
308 match policy.decide(&request) {
309 PolicyDecision::AskUser => {}
310 other => panic!("Expected AskUser, got {:?}", other),
311 }
312 }
313
314 #[test]
315 fn test_policy_decision_constructors() {
316 let allow = PolicyDecision::allow();
317 assert!(matches!(allow, PolicyDecision::Allow));
318
319 let deny = PolicyDecision::deny("test reason");
320 assert!(matches!(deny, PolicyDecision::Deny { reason: Some(_) }));
321
322 let deny_silent = PolicyDecision::deny_silent();
323 assert!(matches!(deny_silent, PolicyDecision::Deny { reason: None }));
324
325 let ask = PolicyDecision::ask_user();
326 assert!(matches!(ask, PolicyDecision::AskUser));
327 }
328}