Skip to main content

phi_agent/cli/
approval.rs

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