phi_agent/cli/
approval.rs1use agent_base::{AgentError, AgentResult, ApprovalDecision, ApprovalHandler, ApprovalRequest};
12use async_trait::async_trait;
13use tokio::sync::{mpsc, oneshot};
14
15#[derive(Clone, Debug)]
17pub enum ApprovalMode {
18 Auto,
20 DenyAll,
22}
23
24pub struct AutoApprovalHandler {
29 mode: ApprovalMode,
30}
31
32impl AutoApprovalHandler {
33 pub fn new(mode: ApprovalMode) -> Self {
35 Self { mode }
36 }
37}
38
39#[async_trait]
40impl ApprovalHandler for AutoApprovalHandler {
41 async fn approve(
42 &self,
43 request: ApprovalRequest,
44 _cancel_token: tokio_util::sync::CancellationToken,
45 ) -> AgentResult<ApprovalDecision> {
46 match &self.mode {
47 ApprovalMode::Auto => {
48 tracing::info!(decision = "AllowAlways", tool = %request.title, "auto-approved");
49 Ok(ApprovalDecision::AllowAlways)
50 },
51 ApprovalMode::DenyAll => {
52 tracing::info!(decision = "Deny", tool = %request.title, "auto-denied");
53 Ok(ApprovalDecision::Deny)
54 },
55 }
56 }
57}
58
59#[derive(Debug)]
64pub struct ApprovalItem {
65 pub request: ApprovalRequest,
67 pub decision_tx: oneshot::Sender<ApprovalDecision>,
69}
70
71#[derive(Debug, Clone)]
80pub struct QueuedApprovalHandler {
81 queue_tx: mpsc::UnboundedSender<ApprovalItem>,
82}
83
84impl QueuedApprovalHandler {
85 pub fn new(queue_tx: mpsc::UnboundedSender<ApprovalItem>) -> Self {
88 Self { queue_tx }
89 }
90}
91
92#[async_trait]
93impl ApprovalHandler for QueuedApprovalHandler {
94 async fn approve(
95 &self,
96 request: ApprovalRequest,
97 cancel_token: tokio_util::sync::CancellationToken,
98 ) -> AgentResult<ApprovalDecision> {
99 let (decision_tx, decision_rx) = oneshot::channel();
100 let _ = self.queue_tx.send(ApprovalItem { request, decision_tx });
101
102 tokio::select! {
103 _ = cancel_token.cancelled() => Err(AgentError::Cancelled),
104 result = decision_rx => result.map_err(|_| AgentError::Cancelled),
105 }
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use agent_base::{ApprovalRequest, RiskLevel};
113
114 #[tokio::test]
115 async fn test_auto_mode_approves_all() {
116 let handler = AutoApprovalHandler::new(ApprovalMode::Auto);
117 let request = ApprovalRequest {
118 title: "Delete file".into(),
119 message: "This will delete /tmp/important.txt".into(),
120 action_key: None,
121 risk_level: RiskLevel::Destructive,
122 raw: None,
123 };
124 let cancel = tokio_util::sync::CancellationToken::new();
125 let decision = handler.approve(request, cancel).await.unwrap();
126 assert_eq!(decision, ApprovalDecision::AllowAlways);
127 }
128
129 #[tokio::test]
130 async fn test_deny_all_mode_denies() {
131 let handler = AutoApprovalHandler::new(ApprovalMode::DenyAll);
132 let request = ApprovalRequest {
133 title: "Read file".into(),
134 message: "Read /tmp/safe.txt".into(),
135 action_key: None,
136 risk_level: RiskLevel::Safe,
137 raw: None,
138 };
139 let cancel = tokio_util::sync::CancellationToken::new();
140 let decision = handler.approve(request, cancel).await.unwrap();
141 assert_eq!(decision, ApprovalDecision::Deny);
142 }
143
144 #[tokio::test]
145 async fn test_deny_all_denies_even_safe_operations() {
146 let handler = AutoApprovalHandler::new(ApprovalMode::DenyAll);
147 let request = ApprovalRequest {
148 title: "Safe thing".into(),
149 message: "Perfectly safe".into(),
150 action_key: None,
151 risk_level: RiskLevel::Safe,
152 raw: None,
153 };
154 let cancel = tokio_util::sync::CancellationToken::new();
155 let decision = handler.approve(request, cancel).await.unwrap();
156 assert_eq!(decision, ApprovalDecision::Deny);
157 }
158
159 #[tokio::test]
160 async fn queued_handler_roundtrips_decision() {
161 let (queue_tx, mut queue_rx) = tokio::sync::mpsc::unbounded_channel();
162 let handler = QueuedApprovalHandler::new(queue_tx);
163 let cancel = tokio_util::sync::CancellationToken::new();
164
165 let request = ApprovalRequest {
166 title: "write_file".to_string(),
167 message: "Write file: src/lib.rs".to_string(),
168 action_key: Some("write_file:src/lib.rs".to_string()),
169 risk_level: RiskLevel::Sensitive,
170 raw: None,
171 };
172
173 let handle = {
174 let handler = handler.clone();
175 let request = request.clone();
176 let cancel = cancel.clone();
177 tokio::spawn(async move { handler.approve(request, cancel).await })
178 };
179
180 let item = queue_rx.recv().await.expect("request should be queued");
182 assert_eq!(item.request.title, "write_file");
183 item.decision_tx.send(ApprovalDecision::AllowAlways).expect("UI should be able to answer");
184
185 let decision = handle.await.expect("handler task").expect("approve");
186 assert_eq!(decision, ApprovalDecision::AllowAlways);
187 }
188
189 #[tokio::test]
190 async fn queued_handler_respects_cancel() {
191 let (queue_tx, mut queue_rx) = tokio::sync::mpsc::unbounded_channel();
192 let handler = QueuedApprovalHandler::new(queue_tx);
193 let cancel = tokio_util::sync::CancellationToken::new();
194
195 let request = ApprovalRequest {
196 title: "t".to_string(),
197 message: "m".to_string(),
198 action_key: None,
199 risk_level: RiskLevel::Safe,
200 raw: None,
201 };
202
203 let handle = {
204 let handler = handler.clone();
205 let request = request.clone();
206 let cancel = cancel.clone();
207 tokio::spawn(async move { handler.approve(request, cancel).await })
208 };
209
210 let _item = queue_rx.recv().await.expect("request queued");
211 cancel.cancel();
212
213 let result = handle.await.expect("handler task");
214 assert!(matches!(result, Err(AgentError::Cancelled)));
215 }
216}