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 source: None,
124 };
125 let cancel = tokio_util::sync::CancellationToken::new();
126 let decision = handler.approve(request, cancel).await.unwrap();
127 assert_eq!(decision, ApprovalDecision::AllowAlways);
128 }
129
130 #[tokio::test]
131 async fn test_deny_all_mode_denies() {
132 let handler = AutoApprovalHandler::new(ApprovalMode::DenyAll);
133 let request = ApprovalRequest {
134 title: "Read file".into(),
135 message: "Read /tmp/safe.txt".into(),
136 action_key: None,
137 risk_level: RiskLevel::Safe,
138 raw: None,
139 source: None,
140 };
141 let cancel = tokio_util::sync::CancellationToken::new();
142 let decision = handler.approve(request, cancel).await.unwrap();
143 assert_eq!(decision, ApprovalDecision::Deny);
144 }
145
146 #[tokio::test]
147 async fn test_deny_all_denies_even_safe_operations() {
148 let handler = AutoApprovalHandler::new(ApprovalMode::DenyAll);
149 let request = ApprovalRequest {
150 title: "Safe thing".into(),
151 message: "Perfectly safe".into(),
152 action_key: None,
153 risk_level: RiskLevel::Safe,
154 raw: None,
155 source: None,
156 };
157 let cancel = tokio_util::sync::CancellationToken::new();
158 let decision = handler.approve(request, cancel).await.unwrap();
159 assert_eq!(decision, ApprovalDecision::Deny);
160 }
161
162 #[tokio::test]
163 async fn queued_handler_roundtrips_decision() {
164 let (queue_tx, mut queue_rx) = tokio::sync::mpsc::unbounded_channel();
165 let handler = QueuedApprovalHandler::new(queue_tx);
166 let cancel = tokio_util::sync::CancellationToken::new();
167
168 let request = ApprovalRequest {
169 title: "write_file".to_string(),
170 message: "Write file: src/lib.rs".to_string(),
171 action_key: Some("write_file:src/lib.rs".to_string()),
172 risk_level: RiskLevel::Sensitive,
173 raw: None,
174 source: None,
175 };
176
177 let handle = {
178 let handler = handler.clone();
179 let request = request.clone();
180 let cancel = cancel.clone();
181 tokio::spawn(async move { handler.approve(request, cancel).await })
182 };
183
184 let item = queue_rx.recv().await.expect("request should be queued");
186 assert_eq!(item.request.title, "write_file");
187 item.decision_tx.send(ApprovalDecision::AllowAlways).expect("UI should be able to answer");
188
189 let decision = handle.await.expect("handler task").expect("approve");
190 assert_eq!(decision, ApprovalDecision::AllowAlways);
191 }
192
193 #[tokio::test]
194 async fn queued_handler_respects_cancel() {
195 let (queue_tx, mut queue_rx) = tokio::sync::mpsc::unbounded_channel();
196 let handler = QueuedApprovalHandler::new(queue_tx);
197 let cancel = tokio_util::sync::CancellationToken::new();
198
199 let request = ApprovalRequest {
200 title: "t".to_string(),
201 message: "m".to_string(),
202 action_key: None,
203 risk_level: RiskLevel::Safe,
204 raw: None,
205 source: None,
206 };
207
208 let handle = {
209 let handler = handler.clone();
210 let request = request.clone();
211 let cancel = cancel.clone();
212 tokio::spawn(async move { handler.approve(request, cancel).await })
213 };
214
215 let _item = queue_rx.recv().await.expect("request queued");
216 cancel.cancel();
217
218 let result = handle.await.expect("handler task");
219 assert!(matches!(result, Err(AgentError::Cancelled)));
220 }
221}