1use crate::agent::AgentEvent;
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15use tokio::sync::{broadcast, oneshot, RwLock};
16
17pub use crate::queue::SessionLane;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
22pub enum TimeoutAction {
23 #[default]
25 Reject,
26 AutoApprove,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ConfirmationPolicy {
37 pub enabled: bool,
39
40 pub default_timeout_ms: u64,
42
43 pub timeout_action: TimeoutAction,
45
46 pub yolo_lanes: HashSet<SessionLane>,
50}
51
52impl Default for ConfirmationPolicy {
53 fn default() -> Self {
54 Self {
55 enabled: false, default_timeout_ms: 30_000, timeout_action: TimeoutAction::Reject,
58 yolo_lanes: HashSet::new(), }
60 }
61}
62
63impl ConfirmationPolicy {
64 pub fn enabled() -> Self {
66 Self {
67 enabled: true,
68 ..Default::default()
69 }
70 }
71
72 pub fn with_yolo_lanes(mut self, lanes: impl IntoIterator<Item = SessionLane>) -> Self {
74 self.yolo_lanes = lanes.into_iter().collect();
75 self
76 }
77
78 pub fn with_timeout(mut self, timeout_ms: u64, action: TimeoutAction) -> Self {
80 self.default_timeout_ms = timeout_ms;
81 self.timeout_action = action;
82 self
83 }
84
85 pub fn is_yolo(&self, tool_name: &str) -> bool {
90 if !self.enabled {
91 return true; }
93 let lane = SessionLane::from_tool_name(tool_name);
94 self.yolo_lanes.contains(&lane)
95 }
96
97 pub fn requires_confirmation(&self, tool_name: &str) -> bool {
102 !self.is_yolo(tool_name)
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ConfirmationResponse {
109 pub approved: bool,
111 pub reason: Option<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PendingConfirmationInfo {
118 pub tool_id: String,
119 pub tool_name: String,
120 pub args: serde_json::Value,
121 pub remaining_ms: u64,
122}
123
124#[async_trait::async_trait]
129pub trait ConfirmationProvider: Send + Sync {
130 async fn requires_confirmation(&self, tool_name: &str) -> bool;
132
133 async fn request_confirmation(
137 &self,
138 tool_id: &str,
139 tool_name: &str,
140 args: &serde_json::Value,
141 ) -> oneshot::Receiver<ConfirmationResponse>;
142
143 async fn confirm(
148 &self,
149 tool_id: &str,
150 approved: bool,
151 reason: Option<String>,
152 ) -> Result<bool, String>;
153
154 async fn policy(&self) -> ConfirmationPolicy;
156
157 async fn set_policy(&self, policy: ConfirmationPolicy);
159
160 async fn check_timeouts(&self) -> usize;
162
163 async fn cancel_all(&self) -> usize;
165
166 async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
168 Vec::new()
169 }
170}
171
172pub struct PendingConfirmation {
174 pub tool_id: String,
176 pub tool_name: String,
178 pub args: serde_json::Value,
180 pub created_at: Instant,
182 pub timeout_ms: u64,
184 response_tx: oneshot::Sender<ConfirmationResponse>,
186}
187
188impl PendingConfirmation {
189 pub fn is_timed_out(&self) -> bool {
191 self.created_at.elapsed() > Duration::from_millis(self.timeout_ms)
192 }
193
194 pub fn remaining_ms(&self) -> u64 {
196 let elapsed = self.created_at.elapsed().as_millis() as u64;
197 self.timeout_ms.saturating_sub(elapsed)
198 }
199}
200
201pub struct ConfirmationManager {
203 policy: RwLock<ConfirmationPolicy>,
205 pending: Arc<RwLock<HashMap<String, PendingConfirmation>>>,
207 event_tx: broadcast::Sender<AgentEvent>,
209}
210
211impl ConfirmationManager {
212 pub fn new(policy: ConfirmationPolicy, event_tx: broadcast::Sender<AgentEvent>) -> Self {
214 Self {
215 policy: RwLock::new(policy),
216 pending: Arc::new(RwLock::new(HashMap::new())),
217 event_tx,
218 }
219 }
220
221 pub async fn policy(&self) -> ConfirmationPolicy {
223 self.policy.read().await.clone()
224 }
225
226 pub async fn set_policy(&self, policy: ConfirmationPolicy) {
228 *self.policy.write().await = policy;
229 }
230
231 pub async fn requires_confirmation(&self, tool_name: &str) -> bool {
233 self.policy.read().await.requires_confirmation(tool_name)
234 }
235
236 pub async fn request_confirmation(
241 &self,
242 tool_id: &str,
243 tool_name: &str,
244 args: &serde_json::Value,
245 ) -> oneshot::Receiver<ConfirmationResponse> {
246 let (tx, rx) = oneshot::channel();
247
248 let policy = self.policy.read().await;
249 let timeout_ms = policy.default_timeout_ms;
250 drop(policy);
251
252 let pending = PendingConfirmation {
253 tool_id: tool_id.to_string(),
254 tool_name: tool_name.to_string(),
255 args: args.clone(),
256 created_at: Instant::now(),
257 timeout_ms,
258 response_tx: tx,
259 };
260
261 {
263 let mut pending_map = self.pending.write().await;
264 pending_map.insert(tool_id.to_string(), pending);
265 }
266
267 let _ = self.event_tx.send(AgentEvent::ConfirmationRequired {
269 tool_id: tool_id.to_string(),
270 tool_name: tool_name.to_string(),
271 args: args.clone(),
272 timeout_ms,
273 });
274
275 rx
276 }
277
278 pub async fn confirm(
283 &self,
284 tool_id: &str,
285 approved: bool,
286 reason: Option<String>,
287 ) -> Result<bool, String> {
288 let pending = {
289 let mut pending_map = self.pending.write().await;
290 pending_map.remove(tool_id)
291 };
292
293 if let Some(confirmation) = pending {
294 let _ = self.event_tx.send(AgentEvent::ConfirmationReceived {
296 tool_id: tool_id.to_string(),
297 approved,
298 reason: reason.clone(),
299 });
300
301 let response = ConfirmationResponse { approved, reason };
303 let _ = confirmation.response_tx.send(response);
304
305 Ok(true)
306 } else {
307 Ok(false)
308 }
309 }
310
311 pub async fn check_timeouts(&self) -> usize {
315 let policy = self.policy.read().await;
316 let timeout_action = policy.timeout_action;
317 drop(policy);
318
319 let mut timed_out = Vec::new();
320
321 {
323 let pending_map = self.pending.read().await;
324 for (tool_id, pending) in pending_map.iter() {
325 if pending.is_timed_out() {
326 timed_out.push(tool_id.clone());
327 }
328 }
329 }
330
331 for tool_id in &timed_out {
333 let pending = {
334 let mut pending_map = self.pending.write().await;
335 pending_map.remove(tool_id)
336 };
337
338 if let Some(confirmation) = pending {
339 let (approved, action_taken) = match timeout_action {
340 TimeoutAction::Reject => (false, "rejected"),
341 TimeoutAction::AutoApprove => (true, "auto_approved"),
342 };
343
344 let _ = self.event_tx.send(AgentEvent::ConfirmationTimeout {
346 tool_id: tool_id.clone(),
347 action_taken: action_taken.to_string(),
348 });
349
350 let response = ConfirmationResponse {
352 approved,
353 reason: Some(format!("Confirmation timed out, action: {}", action_taken)),
354 };
355 let _ = confirmation.response_tx.send(response);
356 }
357 }
358
359 timed_out.len()
360 }
361
362 pub async fn pending_count(&self) -> usize {
364 self.pending.read().await.len()
365 }
366
367 pub async fn pending_confirmations(&self) -> Vec<(String, String, u64)> {
369 let pending_map = self.pending.read().await;
370 pending_map
371 .values()
372 .map(|p| (p.tool_id.clone(), p.tool_name.clone(), p.remaining_ms()))
373 .collect()
374 }
375
376 pub async fn pending_confirmation_details(&self) -> Vec<PendingConfirmationInfo> {
378 let pending_map = self.pending.read().await;
379 pending_map
380 .values()
381 .map(|p| PendingConfirmationInfo {
382 tool_id: p.tool_id.clone(),
383 tool_name: p.tool_name.clone(),
384 args: p.args.clone(),
385 remaining_ms: p.remaining_ms(),
386 })
387 .collect()
388 }
389
390 pub async fn cancel(&self, tool_id: &str) -> bool {
392 let pending = {
393 let mut pending_map = self.pending.write().await;
394 pending_map.remove(tool_id)
395 };
396
397 if let Some(confirmation) = pending {
398 let response = ConfirmationResponse {
399 approved: false,
400 reason: Some("Confirmation cancelled".to_string()),
401 };
402 let _ = confirmation.response_tx.send(response);
403 true
404 } else {
405 false
406 }
407 }
408
409 pub async fn cancel_all(&self) -> usize {
411 let pending_list: Vec<_> = {
412 let mut pending_map = self.pending.write().await;
413 pending_map.drain().collect()
414 };
415
416 let count = pending_list.len();
417
418 for (_, confirmation) in pending_list {
419 let response = ConfirmationResponse {
420 approved: false,
421 reason: Some("Confirmation cancelled".to_string()),
422 };
423 let _ = confirmation.response_tx.send(response);
424 }
425
426 count
427 }
428}
429
430#[async_trait::async_trait]
432impl ConfirmationProvider for ConfirmationManager {
433 async fn requires_confirmation(&self, tool_name: &str) -> bool {
434 self.requires_confirmation(tool_name).await
435 }
436
437 async fn request_confirmation(
438 &self,
439 tool_id: &str,
440 tool_name: &str,
441 args: &serde_json::Value,
442 ) -> oneshot::Receiver<ConfirmationResponse> {
443 self.request_confirmation(tool_id, tool_name, args).await
444 }
445
446 async fn confirm(
447 &self,
448 tool_id: &str,
449 approved: bool,
450 reason: Option<String>,
451 ) -> Result<bool, String> {
452 self.confirm(tool_id, approved, reason).await
453 }
454
455 async fn policy(&self) -> ConfirmationPolicy {
456 self.policy().await
457 }
458
459 async fn set_policy(&self, policy: ConfirmationPolicy) {
460 self.set_policy(policy).await
461 }
462
463 async fn check_timeouts(&self) -> usize {
464 self.check_timeouts().await
465 }
466
467 async fn cancel_all(&self) -> usize {
468 self.cancel_all().await
469 }
470
471 async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
472 self.pending_confirmation_details().await
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
485 fn test_session_lane() {
486 assert_eq!(SessionLane::from_tool_name("read"), SessionLane::Query);
487 assert_eq!(SessionLane::from_tool_name("grep"), SessionLane::Query);
488 assert_eq!(SessionLane::from_tool_name("bash"), SessionLane::Execute);
489 assert_eq!(SessionLane::from_tool_name("write"), SessionLane::Execute);
490 }
491
492 #[test]
493 fn test_session_lane_priority() {
494 assert_eq!(SessionLane::Control.priority(), 0);
495 assert_eq!(SessionLane::Query.priority(), 1);
496 assert_eq!(SessionLane::Execute.priority(), 2);
497 assert_eq!(SessionLane::Generate.priority(), 3);
498
499 assert!(SessionLane::Control.priority() < SessionLane::Query.priority());
501 assert!(SessionLane::Query.priority() < SessionLane::Execute.priority());
502 assert!(SessionLane::Execute.priority() < SessionLane::Generate.priority());
503 }
504
505 #[test]
506 fn test_session_lane_all_query() {
507 let query_tools = ["read", "glob", "ls", "grep", "list_files", "search"];
508 for tool in query_tools {
509 assert_eq!(
510 SessionLane::from_tool_name(tool),
511 SessionLane::Query,
512 "Tool '{}' should be in Query lane",
513 tool
514 );
515 }
516 }
517
518 #[test]
519 fn test_session_lane_all_execute() {
520 let execute_tools = ["bash", "write", "edit", "delete", "move", "copy", "execute"];
521 for tool in execute_tools {
522 assert_eq!(
523 SessionLane::from_tool_name(tool),
524 SessionLane::Execute,
525 "Tool '{}' should be in Execute lane",
526 tool
527 );
528 }
529 }
530
531 #[test]
540 fn test_confirmation_policy_default() {
541 let policy = ConfirmationPolicy::default();
542 assert!(!policy.enabled);
543 assert!(!policy.requires_confirmation("bash"));
545 assert!(!policy.requires_confirmation("write"));
546 assert!(!policy.requires_confirmation("read"));
547 }
548
549 #[test]
550 fn test_confirmation_policy_enabled() {
551 let policy = ConfirmationPolicy::enabled();
552 assert!(policy.enabled);
553 assert!(policy.requires_confirmation("bash"));
555 assert!(policy.requires_confirmation("write"));
556 assert!(policy.requires_confirmation("read"));
557 assert!(policy.requires_confirmation("grep"));
558 }
559
560 #[test]
561 fn test_confirmation_policy_yolo_mode() {
562 let policy = ConfirmationPolicy::enabled().with_yolo_lanes([SessionLane::Execute]);
563
564 assert!(!policy.requires_confirmation("bash")); assert!(!policy.requires_confirmation("write")); assert!(policy.requires_confirmation("read")); }
568
569 #[test]
570 fn test_confirmation_policy_yolo_multiple_lanes() {
571 let policy = ConfirmationPolicy::enabled()
572 .with_yolo_lanes([SessionLane::Query, SessionLane::Execute]);
573
574 assert!(!policy.requires_confirmation("bash")); assert!(!policy.requires_confirmation("read")); assert!(!policy.requires_confirmation("grep")); }
579
580 #[test]
581 fn test_confirmation_policy_is_yolo() {
582 let policy = ConfirmationPolicy::enabled().with_yolo_lanes([SessionLane::Execute]);
583
584 assert!(policy.is_yolo("bash")); assert!(policy.is_yolo("write")); assert!(!policy.is_yolo("read")); }
588
589 #[test]
590 fn test_confirmation_policy_disabled_is_always_yolo() {
591 let policy = ConfirmationPolicy::default(); assert!(policy.is_yolo("bash"));
593 assert!(policy.is_yolo("read"));
594 assert!(policy.is_yolo("unknown_tool"));
595 }
596
597 #[test]
598 fn test_confirmation_policy_with_timeout() {
599 let policy = ConfirmationPolicy::enabled().with_timeout(5000, TimeoutAction::AutoApprove);
600
601 assert_eq!(policy.default_timeout_ms, 5000);
602 assert_eq!(policy.timeout_action, TimeoutAction::AutoApprove);
603 }
604
605 #[tokio::test]
610 async fn test_confirmation_manager_no_hitl() {
611 let (event_tx, _) = broadcast::channel(100);
612 let manager = ConfirmationManager::new(ConfirmationPolicy::default(), event_tx);
613
614 assert!(!manager.requires_confirmation("bash").await);
615 }
616
617 #[tokio::test]
618 async fn test_confirmation_manager_with_hitl() {
619 let (event_tx, _) = broadcast::channel(100);
620 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
621
622 assert!(manager.requires_confirmation("bash").await);
624 assert!(manager.requires_confirmation("read").await);
625 }
626
627 #[tokio::test]
628 async fn test_confirmation_manager_with_yolo() {
629 let (event_tx, _) = broadcast::channel(100);
630 let policy = ConfirmationPolicy::enabled().with_yolo_lanes([SessionLane::Query]);
631 let manager = ConfirmationManager::new(policy, event_tx);
632
633 assert!(manager.requires_confirmation("bash").await); assert!(!manager.requires_confirmation("read").await); }
636
637 #[tokio::test]
638 async fn test_confirmation_manager_policy_update() {
639 let (event_tx, _) = broadcast::channel(100);
640 let manager = ConfirmationManager::new(ConfirmationPolicy::default(), event_tx);
641
642 assert!(!manager.requires_confirmation("bash").await);
644
645 manager.set_policy(ConfirmationPolicy::enabled()).await;
647 assert!(manager.requires_confirmation("bash").await);
648
649 manager
651 .set_policy(ConfirmationPolicy::enabled().with_yolo_lanes([SessionLane::Execute]))
652 .await;
653 assert!(!manager.requires_confirmation("bash").await);
654 }
655
656 #[tokio::test]
661 async fn test_confirmation_flow_approve() {
662 let (event_tx, mut event_rx) = broadcast::channel(100);
663 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
664
665 let rx = manager
667 .request_confirmation("tool-1", "bash", &serde_json::json!({"command": "ls"}))
668 .await;
669
670 let event = event_rx.recv().await.unwrap();
672 match event {
673 AgentEvent::ConfirmationRequired {
674 tool_id,
675 tool_name,
676 timeout_ms,
677 ..
678 } => {
679 assert_eq!(tool_id, "tool-1");
680 assert_eq!(tool_name, "bash");
681 assert_eq!(timeout_ms, 30_000); }
683 _ => panic!("Expected ConfirmationRequired event"),
684 }
685
686 let result = manager.confirm("tool-1", true, None).await;
688 assert!(result.is_ok());
689 assert!(result.unwrap());
690
691 let event = event_rx.recv().await.unwrap();
693 match event {
694 AgentEvent::ConfirmationReceived {
695 tool_id, approved, ..
696 } => {
697 assert_eq!(tool_id, "tool-1");
698 assert!(approved);
699 }
700 _ => panic!("Expected ConfirmationReceived event"),
701 }
702
703 let response = rx.await.unwrap();
705 assert!(response.approved);
706 assert!(response.reason.is_none());
707 }
708
709 #[tokio::test]
710 async fn test_confirmation_flow_reject() {
711 let (event_tx, mut event_rx) = broadcast::channel(100);
712 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
713
714 let rx = manager
716 .request_confirmation(
717 "tool-1",
718 "bash",
719 &serde_json::json!({"command": "rm -rf /"}),
720 )
721 .await;
722
723 let _ = event_rx.recv().await.unwrap();
725
726 let result = manager
728 .confirm("tool-1", false, Some("Dangerous command".to_string()))
729 .await;
730 assert!(result.is_ok());
731 assert!(result.unwrap());
732
733 let event = event_rx.recv().await.unwrap();
735 match event {
736 AgentEvent::ConfirmationReceived {
737 tool_id,
738 approved,
739 reason,
740 } => {
741 assert_eq!(tool_id, "tool-1");
742 assert!(!approved);
743 assert_eq!(reason, Some("Dangerous command".to_string()));
744 }
745 _ => panic!("Expected ConfirmationReceived event"),
746 }
747
748 let response = rx.await.unwrap();
750 assert!(!response.approved);
751 assert_eq!(response.reason, Some("Dangerous command".to_string()));
752 }
753
754 #[tokio::test]
755 async fn test_confirmation_not_found() {
756 let (event_tx, _) = broadcast::channel(100);
757 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
758
759 let result = manager.confirm("non-existent", true, None).await;
761 assert!(result.is_ok());
762 assert!(!result.unwrap()); }
764
765 #[tokio::test]
770 async fn test_multiple_confirmations() {
771 let (event_tx, _) = broadcast::channel(100);
772 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
773
774 let rx1 = manager
776 .request_confirmation("tool-1", "bash", &serde_json::json!({"cmd": "1"}))
777 .await;
778 let rx2 = manager
779 .request_confirmation("tool-2", "write", &serde_json::json!({"cmd": "2"}))
780 .await;
781 let rx3 = manager
782 .request_confirmation("tool-3", "edit", &serde_json::json!({"cmd": "3"}))
783 .await;
784
785 assert_eq!(manager.pending_count().await, 3);
787
788 manager.confirm("tool-1", true, None).await.unwrap();
790 let response1 = rx1.await.unwrap();
791 assert!(response1.approved);
792
793 manager.confirm("tool-2", false, None).await.unwrap();
795 let response2 = rx2.await.unwrap();
796 assert!(!response2.approved);
797
798 manager.confirm("tool-3", true, None).await.unwrap();
800 let response3 = rx3.await.unwrap();
801 assert!(response3.approved);
802
803 assert_eq!(manager.pending_count().await, 0);
805 }
806
807 #[tokio::test]
808 async fn test_pending_confirmations_info() {
809 let (event_tx, _) = broadcast::channel(100);
810 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
811
812 let _rx1 = manager
814 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
815 .await;
816 let _rx2 = manager
817 .request_confirmation("tool-2", "write", &serde_json::json!({}))
818 .await;
819
820 let pending = manager.pending_confirmations().await;
821 assert_eq!(pending.len(), 2);
822
823 let tool_ids: Vec<&str> = pending.iter().map(|(id, _, _)| id.as_str()).collect();
825 assert!(tool_ids.contains(&"tool-1"));
826 assert!(tool_ids.contains(&"tool-2"));
827 }
828
829 #[tokio::test]
834 async fn test_cancel_confirmation() {
835 let (event_tx, _) = broadcast::channel(100);
836 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
837
838 let rx = manager
840 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
841 .await;
842
843 assert_eq!(manager.pending_count().await, 1);
844
845 let cancelled = manager.cancel("tool-1").await;
847 assert!(cancelled);
848 assert_eq!(manager.pending_count().await, 0);
849
850 let response = rx.await.unwrap();
852 assert!(!response.approved);
853 assert_eq!(response.reason, Some("Confirmation cancelled".to_string()));
854 }
855
856 #[tokio::test]
857 async fn test_cancel_nonexistent() {
858 let (event_tx, _) = broadcast::channel(100);
859 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
860
861 let cancelled = manager.cancel("non-existent").await;
862 assert!(!cancelled);
863 }
864
865 #[tokio::test]
866 async fn test_cancel_all() {
867 let (event_tx, _) = broadcast::channel(100);
868 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
869
870 let rx1 = manager
872 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
873 .await;
874 let rx2 = manager
875 .request_confirmation("tool-2", "write", &serde_json::json!({}))
876 .await;
877 let rx3 = manager
878 .request_confirmation("tool-3", "edit", &serde_json::json!({}))
879 .await;
880
881 assert_eq!(manager.pending_count().await, 3);
882
883 let cancelled_count = manager.cancel_all().await;
885 assert_eq!(cancelled_count, 3);
886 assert_eq!(manager.pending_count().await, 0);
887
888 for rx in [rx1, rx2, rx3] {
890 let response = rx.await.unwrap();
891 assert!(!response.approved);
892 assert_eq!(response.reason, Some("Confirmation cancelled".to_string()));
893 }
894 }
895
896 #[tokio::test]
901 async fn test_timeout_reject() {
902 let (event_tx, mut event_rx) = broadcast::channel(100);
903 let policy = ConfirmationPolicy {
904 enabled: true,
905 default_timeout_ms: 50, timeout_action: TimeoutAction::Reject,
907 ..Default::default()
908 };
909 let manager = ConfirmationManager::new(policy, event_tx);
910
911 let rx = manager
913 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
914 .await;
915
916 let _ = event_rx.recv().await.unwrap();
918
919 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
921
922 let timed_out = manager.check_timeouts().await;
924 assert_eq!(timed_out, 1);
925
926 let event = event_rx.recv().await.unwrap();
928 match event {
929 AgentEvent::ConfirmationTimeout {
930 tool_id,
931 action_taken,
932 } => {
933 assert_eq!(tool_id, "tool-1");
934 assert_eq!(action_taken, "rejected");
935 }
936 _ => panic!("Expected ConfirmationTimeout event"),
937 }
938
939 let response = rx.await.unwrap();
941 assert!(!response.approved);
942 assert!(response.reason.as_ref().unwrap().contains("timed out"));
943 }
944
945 #[tokio::test]
946 async fn test_timeout_auto_approve() {
947 let (event_tx, mut event_rx) = broadcast::channel(100);
948 let policy = ConfirmationPolicy {
949 enabled: true,
950 default_timeout_ms: 50, timeout_action: TimeoutAction::AutoApprove,
952 ..Default::default()
953 };
954 let manager = ConfirmationManager::new(policy, event_tx);
955
956 let rx = manager
958 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
959 .await;
960
961 let _ = event_rx.recv().await.unwrap();
963
964 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
966
967 let timed_out = manager.check_timeouts().await;
969 assert_eq!(timed_out, 1);
970
971 let event = event_rx.recv().await.unwrap();
973 match event {
974 AgentEvent::ConfirmationTimeout {
975 tool_id,
976 action_taken,
977 } => {
978 assert_eq!(tool_id, "tool-1");
979 assert_eq!(action_taken, "auto_approved");
980 }
981 _ => panic!("Expected ConfirmationTimeout event"),
982 }
983
984 let response = rx.await.unwrap();
986 assert!(response.approved);
987 assert!(response.reason.as_ref().unwrap().contains("auto_approved"));
988 }
989
990 #[tokio::test]
991 async fn test_no_timeout_when_confirmed() {
992 let (event_tx, _) = broadcast::channel(100);
993 let policy = ConfirmationPolicy {
994 enabled: true,
995 default_timeout_ms: 50,
996 timeout_action: TimeoutAction::Reject,
997 ..Default::default()
998 };
999 let manager = ConfirmationManager::new(policy, event_tx);
1000
1001 let rx = manager
1003 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
1004 .await;
1005
1006 manager.confirm("tool-1", true, None).await.unwrap();
1008
1009 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
1011
1012 let timed_out = manager.check_timeouts().await;
1014 assert_eq!(timed_out, 0);
1015
1016 let response = rx.await.unwrap();
1018 assert!(response.approved);
1019 assert!(response.reason.is_none());
1020 }
1021}