Skip to main content

phi_agent/cli/
approval.rs

1//! Approval strategies for tool calls.
2//!
3//! Provides [`AutoApprovalHandler`] which implements a pure-strategy
4//! approval model — automatically approve all calls or deny all calls
5//! — suitable for CLI, CI, and headless environments.
6
7use agent_base::{AgentResult, ApprovalDecision, ApprovalHandler, ApprovalRequest};
8use async_trait::async_trait;
9
10/// Approval strategy.
11#[derive(Clone, Debug)]
12pub enum ApprovalMode {
13    /// Automatically approve all tool calls without user interaction.
14    Auto,
15    /// Reject all tool calls that require approval.
16    DenyAll,
17}
18
19/// I/O-free approval handler — pure strategy, suitable for all consumers.
20///
21/// Use [`ApprovalMode::Auto`] for automated/CI scenarios, or
22/// [`ApprovalMode::DenyAll`] for read-only / preview modes.
23pub struct AutoApprovalHandler {
24    mode: ApprovalMode,
25}
26
27impl AutoApprovalHandler {
28    /// Create a new handler with the given approval strategy.
29    pub fn new(mode: ApprovalMode) -> Self {
30        Self { mode }
31    }
32}
33
34#[async_trait]
35impl ApprovalHandler for AutoApprovalHandler {
36    async fn approve(
37        &self,
38        request: ApprovalRequest,
39        _cancel_token: tokio_util::sync::CancellationToken,
40    ) -> AgentResult<ApprovalDecision> {
41        match &self.mode {
42            ApprovalMode::Auto => {
43                tracing::info!(decision = "AllowAlways", tool = %request.title, "auto-approved");
44                Ok(ApprovalDecision::AllowAlways)
45            },
46            ApprovalMode::DenyAll => {
47                tracing::info!(decision = "Deny", tool = %request.title, "auto-denied");
48                Ok(ApprovalDecision::Deny)
49            },
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use agent_base::{ApprovalRequest, RiskLevel};
58
59    #[tokio::test]
60    async fn test_auto_mode_approves_all() {
61        let handler = AutoApprovalHandler::new(ApprovalMode::Auto);
62        let request = ApprovalRequest {
63            title: "Delete file".into(),
64            message: "This will delete /tmp/important.txt".into(),
65            action_key: None,
66            risk_level: RiskLevel::Destructive,
67            raw: None,
68        };
69        let cancel = tokio_util::sync::CancellationToken::new();
70        let decision = handler.approve(request, cancel).await.unwrap();
71        assert_eq!(decision, ApprovalDecision::AllowAlways);
72    }
73
74    #[tokio::test]
75    async fn test_deny_all_mode_denies() {
76        let handler = AutoApprovalHandler::new(ApprovalMode::DenyAll);
77        let request = ApprovalRequest {
78            title: "Read file".into(),
79            message: "Read /tmp/safe.txt".into(),
80            action_key: None,
81            risk_level: RiskLevel::Safe,
82            raw: None,
83        };
84        let cancel = tokio_util::sync::CancellationToken::new();
85        let decision = handler.approve(request, cancel).await.unwrap();
86        assert_eq!(decision, ApprovalDecision::Deny);
87    }
88
89    #[tokio::test]
90    async fn test_deny_all_denies_even_safe_operations() {
91        let handler = AutoApprovalHandler::new(ApprovalMode::DenyAll);
92        let request = ApprovalRequest {
93            title: "Safe thing".into(),
94            message: "Perfectly safe".into(),
95            action_key: None,
96            risk_level: RiskLevel::Safe,
97            raw: None,
98        };
99        let cancel = tokio_util::sync::CancellationToken::new();
100        let decision = handler.approve(request, cancel).await.unwrap();
101        assert_eq!(decision, ApprovalDecision::Deny);
102    }
103}