1use crate::agent::AgentEvent;
11use crate::queue::SessionLane;
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16use tokio::sync::{broadcast, oneshot, RwLock};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
20pub enum TimeoutAction {
21 #[default]
23 Reject,
24 AutoApprove,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ConfirmationPolicy {
35 pub enabled: bool,
37
38 pub default_timeout_ms: u64,
40
41 pub timeout_action: TimeoutAction,
43
44 pub yolo_lanes: HashSet<SessionLane>,
48}
49
50impl Default for ConfirmationPolicy {
51 fn default() -> Self {
52 Self {
53 enabled: false, default_timeout_ms: 30_000, timeout_action: TimeoutAction::Reject,
56 yolo_lanes: HashSet::new(), }
58 }
59}
60
61impl ConfirmationPolicy {
62 pub fn enabled() -> Self {
64 Self {
65 enabled: true,
66 ..Default::default()
67 }
68 }
69
70 pub fn with_yolo_lanes(mut self, lanes: impl IntoIterator<Item = SessionLane>) -> Self {
72 self.yolo_lanes = lanes.into_iter().collect();
73 self
74 }
75
76 pub fn with_timeout(mut self, timeout_ms: u64, action: TimeoutAction) -> Self {
78 self.default_timeout_ms = timeout_ms;
79 self.timeout_action = action;
80 self
81 }
82
83 pub fn is_yolo(&self, tool_name: &str) -> bool {
88 if !self.enabled {
89 return true; }
91 let lane = SessionLane::from_tool_name(tool_name);
92 self.yolo_lanes.contains(&lane)
93 }
94
95 pub fn requires_confirmation(&self, tool_name: &str) -> bool {
100 !self.is_yolo(tool_name)
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ConfirmationResponse {
107 pub approved: bool,
109 pub reason: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct PendingConfirmationInfo {
116 pub tool_id: String,
117 pub tool_name: String,
118 pub args: serde_json::Value,
119 pub remaining_ms: u64,
120}
121
122#[async_trait::async_trait]
127pub trait ConfirmationProvider: Send + Sync {
128 async fn requires_confirmation(&self, tool_name: &str) -> bool;
130
131 async fn request_confirmation(
135 &self,
136 tool_id: &str,
137 tool_name: &str,
138 args: &serde_json::Value,
139 ) -> oneshot::Receiver<ConfirmationResponse>;
140
141 async fn confirm(
146 &self,
147 tool_id: &str,
148 approved: bool,
149 reason: Option<String>,
150 ) -> Result<bool, String>;
151
152 async fn policy(&self) -> ConfirmationPolicy;
154
155 async fn set_policy(&self, policy: ConfirmationPolicy);
157
158 async fn check_timeouts(&self) -> usize;
160
161 async fn cancel_all(&self) -> usize;
163
164 async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
166 Vec::new()
167 }
168}
169
170pub struct PendingConfirmation {
172 pub tool_id: String,
174 pub tool_name: String,
176 pub args: serde_json::Value,
178 pub created_at: Instant,
180 pub timeout_ms: u64,
182 response_tx: oneshot::Sender<ConfirmationResponse>,
184}
185
186impl PendingConfirmation {
187 pub fn is_timed_out(&self) -> bool {
189 self.created_at.elapsed() > Duration::from_millis(self.timeout_ms)
190 }
191
192 pub fn remaining_ms(&self) -> u64 {
194 let elapsed = self.created_at.elapsed().as_millis() as u64;
195 self.timeout_ms.saturating_sub(elapsed)
196 }
197}
198
199pub struct ConfirmationManager {
201 policy: RwLock<ConfirmationPolicy>,
203 pending: Arc<RwLock<HashMap<String, PendingConfirmation>>>,
205 event_tx: broadcast::Sender<AgentEvent>,
207}
208
209impl ConfirmationManager {
210 pub fn new(policy: ConfirmationPolicy, event_tx: broadcast::Sender<AgentEvent>) -> Self {
212 Self {
213 policy: RwLock::new(policy),
214 pending: Arc::new(RwLock::new(HashMap::new())),
215 event_tx,
216 }
217 }
218
219 pub async fn policy(&self) -> ConfirmationPolicy {
221 self.policy.read().await.clone()
222 }
223
224 pub async fn set_policy(&self, policy: ConfirmationPolicy) {
226 *self.policy.write().await = policy;
227 }
228
229 pub async fn requires_confirmation(&self, tool_name: &str) -> bool {
231 self.policy.read().await.requires_confirmation(tool_name)
232 }
233
234 pub async fn request_confirmation(
239 &self,
240 tool_id: &str,
241 tool_name: &str,
242 args: &serde_json::Value,
243 ) -> oneshot::Receiver<ConfirmationResponse> {
244 let (tx, rx) = oneshot::channel();
245
246 let policy = self.policy.read().await;
247 let timeout_ms = policy.default_timeout_ms;
248 drop(policy);
249
250 let pending = PendingConfirmation {
251 tool_id: tool_id.to_string(),
252 tool_name: tool_name.to_string(),
253 args: args.clone(),
254 created_at: Instant::now(),
255 timeout_ms,
256 response_tx: tx,
257 };
258
259 {
261 let mut pending_map = self.pending.write().await;
262 pending_map.insert(tool_id.to_string(), pending);
263 }
264
265 let _ = self.event_tx.send(AgentEvent::ConfirmationRequired {
267 tool_id: tool_id.to_string(),
268 tool_name: tool_name.to_string(),
269 args: args.clone(),
270 timeout_ms,
271 });
272
273 rx
274 }
275
276 pub async fn confirm(
281 &self,
282 tool_id: &str,
283 approved: bool,
284 reason: Option<String>,
285 ) -> Result<bool, String> {
286 let pending = {
287 let mut pending_map = self.pending.write().await;
288 pending_map.remove(tool_id)
289 };
290
291 if let Some(confirmation) = pending {
292 let _ = self.event_tx.send(AgentEvent::ConfirmationReceived {
294 tool_id: tool_id.to_string(),
295 approved,
296 reason: reason.clone(),
297 });
298
299 let response = ConfirmationResponse { approved, reason };
301 let _ = confirmation.response_tx.send(response);
302
303 Ok(true)
304 } else {
305 Ok(false)
306 }
307 }
308
309 pub async fn check_timeouts(&self) -> usize {
313 let policy = self.policy.read().await;
314 let timeout_action = policy.timeout_action;
315 drop(policy);
316
317 let mut timed_out = Vec::new();
318
319 {
321 let pending_map = self.pending.read().await;
322 for (tool_id, pending) in pending_map.iter() {
323 if pending.is_timed_out() {
324 timed_out.push(tool_id.clone());
325 }
326 }
327 }
328
329 for tool_id in &timed_out {
331 let pending = {
332 let mut pending_map = self.pending.write().await;
333 pending_map.remove(tool_id)
334 };
335
336 if let Some(confirmation) = pending {
337 let (approved, action_taken) = match timeout_action {
338 TimeoutAction::Reject => (false, "rejected"),
339 TimeoutAction::AutoApprove => (true, "auto_approved"),
340 };
341
342 let _ = self.event_tx.send(AgentEvent::ConfirmationTimeout {
344 tool_id: tool_id.clone(),
345 action_taken: action_taken.to_string(),
346 });
347
348 let response = ConfirmationResponse {
350 approved,
351 reason: Some(format!("Confirmation timed out, action: {}", action_taken)),
352 };
353 let _ = confirmation.response_tx.send(response);
354 }
355 }
356
357 timed_out.len()
358 }
359
360 pub async fn pending_count(&self) -> usize {
362 self.pending.read().await.len()
363 }
364
365 pub async fn pending_confirmations(&self) -> Vec<(String, String, u64)> {
367 let pending_map = self.pending.read().await;
368 pending_map
369 .values()
370 .map(|p| (p.tool_id.clone(), p.tool_name.clone(), p.remaining_ms()))
371 .collect()
372 }
373
374 pub async fn pending_confirmation_details(&self) -> Vec<PendingConfirmationInfo> {
376 let pending_map = self.pending.read().await;
377 pending_map
378 .values()
379 .map(|p| PendingConfirmationInfo {
380 tool_id: p.tool_id.clone(),
381 tool_name: p.tool_name.clone(),
382 args: p.args.clone(),
383 remaining_ms: p.remaining_ms(),
384 })
385 .collect()
386 }
387
388 pub async fn cancel(&self, tool_id: &str) -> bool {
390 let pending = {
391 let mut pending_map = self.pending.write().await;
392 pending_map.remove(tool_id)
393 };
394
395 if let Some(confirmation) = pending {
396 let response = ConfirmationResponse {
397 approved: false,
398 reason: Some("Confirmation cancelled".to_string()),
399 };
400 let _ = confirmation.response_tx.send(response);
401 true
402 } else {
403 false
404 }
405 }
406
407 pub async fn cancel_all(&self) -> usize {
409 let pending_list: Vec<_> = {
410 let mut pending_map = self.pending.write().await;
411 pending_map.drain().collect()
412 };
413
414 let count = pending_list.len();
415
416 for (_, confirmation) in pending_list {
417 let response = ConfirmationResponse {
418 approved: false,
419 reason: Some("Confirmation cancelled".to_string()),
420 };
421 let _ = confirmation.response_tx.send(response);
422 }
423
424 count
425 }
426}
427
428#[async_trait::async_trait]
430impl ConfirmationProvider for ConfirmationManager {
431 async fn requires_confirmation(&self, tool_name: &str) -> bool {
432 self.requires_confirmation(tool_name).await
433 }
434
435 async fn request_confirmation(
436 &self,
437 tool_id: &str,
438 tool_name: &str,
439 args: &serde_json::Value,
440 ) -> oneshot::Receiver<ConfirmationResponse> {
441 self.request_confirmation(tool_id, tool_name, args).await
442 }
443
444 async fn confirm(
445 &self,
446 tool_id: &str,
447 approved: bool,
448 reason: Option<String>,
449 ) -> Result<bool, String> {
450 self.confirm(tool_id, approved, reason).await
451 }
452
453 async fn policy(&self) -> ConfirmationPolicy {
454 self.policy().await
455 }
456
457 async fn set_policy(&self, policy: ConfirmationPolicy) {
458 self.set_policy(policy).await
459 }
460
461 async fn check_timeouts(&self) -> usize {
462 self.check_timeouts().await
463 }
464
465 async fn cancel_all(&self) -> usize {
466 self.cancel_all().await
467 }
468
469 async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
470 self.pending_confirmation_details().await
471 }
472}
473
474pub struct AutoApproveConfirmation;
480
481#[async_trait::async_trait]
482impl ConfirmationProvider for AutoApproveConfirmation {
483 async fn requires_confirmation(&self, _tool_name: &str) -> bool {
484 false
485 }
486
487 async fn request_confirmation(
488 &self,
489 _tool_id: &str,
490 _tool_name: &str,
491 _args: &serde_json::Value,
492 ) -> oneshot::Receiver<ConfirmationResponse> {
493 let (tx, rx) = oneshot::channel();
494 let _ = tx.send(ConfirmationResponse {
495 approved: true,
496 reason: None,
497 });
498 rx
499 }
500
501 async fn confirm(
502 &self,
503 _tool_id: &str,
504 _approved: bool,
505 _reason: Option<String>,
506 ) -> Result<bool, String> {
507 Ok(false)
508 }
509
510 async fn policy(&self) -> ConfirmationPolicy {
511 ConfirmationPolicy {
512 enabled: false,
513 ..ConfirmationPolicy::default()
514 }
515 }
516
517 async fn set_policy(&self, _policy: ConfirmationPolicy) {}
518
519 async fn check_timeouts(&self) -> usize {
520 0
521 }
522
523 async fn cancel_all(&self) -> usize {
524 0
525 }
526}
527
528#[cfg(test)]
529#[path = "hitl/tests.rs"]
530mod tests;