Skip to main content

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}