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>,
47}
48
49impl Default for ConfirmationPolicy {
50 fn default() -> Self {
51 Self {
52 enabled: false, default_timeout_ms: 30_000, timeout_action: TimeoutAction::Reject,
55 yolo_lanes: HashSet::new(), }
57 }
58}
59
60impl ConfirmationPolicy {
61 pub fn enabled() -> Self {
63 Self {
64 enabled: true,
65 ..Default::default()
66 }
67 }
68
69 pub fn with_yolo_lanes(mut self, lanes: impl IntoIterator<Item = SessionLane>) -> Self {
71 self.yolo_lanes = lanes.into_iter().collect();
72 self
73 }
74
75 pub fn with_timeout(mut self, timeout_ms: u64, action: TimeoutAction) -> Self {
77 self.default_timeout_ms = timeout_ms;
78 self.timeout_action = action;
79 self
80 }
81
82 pub fn is_yolo(&self, _tool_name: &str) -> bool {
84 false
85 }
86
87 pub fn requires_confirmation(&self, _tool_name: &str) -> bool {
92 self.enabled
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ConfirmationResponse {
99 pub approved: bool,
101 pub reason: Option<String>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct PendingConfirmationInfo {
108 pub tool_id: String,
109 pub tool_name: String,
110 pub args: serde_json::Value,
111 pub remaining_ms: u64,
112}
113
114#[async_trait::async_trait]
119pub trait ConfirmationProvider: Send + Sync {
120 fn snapshot_for_run(&self) -> Option<Arc<dyn ConfirmationProvider>> {
127 None
128 }
129
130 async fn requires_confirmation(&self, tool_name: &str) -> bool;
132
133 async fn requires_confirmation_for(&self, tool_name: &str, _args: &serde_json::Value) -> bool {
139 self.requires_confirmation(tool_name).await
140 }
141
142 async fn confirmation_available_for(
149 &self,
150 _tool_name: &str,
151 _args: &serde_json::Value,
152 ) -> bool {
153 true
154 }
155
156 async fn policy_for(&self, _tool_name: &str, _args: &serde_json::Value) -> ConfirmationPolicy {
162 self.policy().await
163 }
164
165 async fn request_confirmation(
169 &self,
170 tool_id: &str,
171 tool_name: &str,
172 args: &serde_json::Value,
173 ) -> oneshot::Receiver<ConfirmationResponse>;
174
175 async fn confirm(
180 &self,
181 tool_id: &str,
182 approved: bool,
183 reason: Option<String>,
184 ) -> Result<bool, String>;
185
186 async fn policy(&self) -> ConfirmationPolicy;
188
189 async fn set_policy(&self, policy: ConfirmationPolicy);
191
192 async fn check_timeouts(&self) -> usize;
194
195 async fn cancel(&self, tool_id: &str) -> bool {
202 self.confirm(tool_id, false, Some("Confirmation cancelled".to_string()))
203 .await
204 .unwrap_or(false)
205 }
206
207 async fn expire(&self, _tool_id: &str, _action: TimeoutAction) -> bool {
209 false
210 }
211
212 async fn cancel_all(&self) -> usize;
214
215 async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
217 Vec::new()
218 }
219}
220
221pub struct PendingConfirmation {
223 pub tool_id: String,
225 pub tool_name: String,
227 pub args: serde_json::Value,
229 pub created_at: Instant,
231 pub timeout_ms: u64,
233 response_tx: oneshot::Sender<ConfirmationResponse>,
235}
236
237impl PendingConfirmation {
238 pub fn is_timed_out(&self) -> bool {
240 self.created_at.elapsed() > Duration::from_millis(self.timeout_ms)
241 }
242
243 pub fn remaining_ms(&self) -> u64 {
245 let elapsed = self.created_at.elapsed().as_millis() as u64;
246 self.timeout_ms.saturating_sub(elapsed)
247 }
248}
249
250pub struct ConfirmationManager {
252 policy: RwLock<ConfirmationPolicy>,
254 pending: Arc<RwLock<HashMap<String, PendingConfirmation>>>,
256 event_tx: broadcast::Sender<AgentEvent>,
258}
259
260impl ConfirmationManager {
261 pub fn new(policy: ConfirmationPolicy, event_tx: broadcast::Sender<AgentEvent>) -> Self {
263 Self {
264 policy: RwLock::new(policy),
265 pending: Arc::new(RwLock::new(HashMap::new())),
266 event_tx,
267 }
268 }
269
270 pub async fn policy(&self) -> ConfirmationPolicy {
272 self.policy.read().await.clone()
273 }
274
275 pub async fn set_policy(&self, policy: ConfirmationPolicy) {
277 *self.policy.write().await = policy;
278 }
279
280 pub async fn requires_confirmation(&self, tool_name: &str) -> bool {
282 self.policy.read().await.requires_confirmation(tool_name)
283 }
284
285 pub async fn request_confirmation(
290 &self,
291 tool_id: &str,
292 tool_name: &str,
293 args: &serde_json::Value,
294 ) -> oneshot::Receiver<ConfirmationResponse> {
295 let (tx, rx) = oneshot::channel();
296
297 let policy = self.policy.read().await;
298 let timeout_ms = policy.default_timeout_ms;
299 drop(policy);
300
301 let pending = PendingConfirmation {
302 tool_id: tool_id.to_string(),
303 tool_name: tool_name.to_string(),
304 args: args.clone(),
305 created_at: Instant::now(),
306 timeout_ms,
307 response_tx: tx,
308 };
309
310 let collision = {
315 let mut pending_map = self.pending.write().await;
316 match pending_map.entry(tool_id.to_string()) {
317 std::collections::hash_map::Entry::Vacant(entry) => {
318 entry.insert(pending);
319 None
320 }
321 std::collections::hash_map::Entry::Occupied(entry) => {
322 Some((entry.remove(), pending))
323 }
324 }
325 };
326 if let Some((existing, duplicate)) = collision {
327 let reason = Some(format!(
328 "Duplicate confirmation tool id '{tool_id}'; both requests were rejected"
329 ));
330 let response = ConfirmationResponse {
331 approved: false,
332 reason: reason.clone(),
333 };
334 let _ = existing.response_tx.send(response.clone());
335 let _ = duplicate.response_tx.send(response);
336 let _ = self.event_tx.send(AgentEvent::ConfirmationReceived {
337 tool_id: tool_id.to_string(),
338 approved: false,
339 reason,
340 });
341 return rx;
342 }
343
344 let _ = self.event_tx.send(AgentEvent::ConfirmationRequired {
346 tool_id: tool_id.to_string(),
347 tool_name: tool_name.to_string(),
348 args: args.clone(),
349 timeout_ms,
350 });
351
352 rx
353 }
354
355 pub async fn confirm(
360 &self,
361 tool_id: &str,
362 approved: bool,
363 reason: Option<String>,
364 ) -> Result<bool, String> {
365 let pending = {
366 let mut pending_map = self.pending.write().await;
367 pending_map.remove(tool_id)
368 };
369
370 if let Some(confirmation) = pending {
371 let _ = self.event_tx.send(AgentEvent::ConfirmationReceived {
373 tool_id: tool_id.to_string(),
374 approved,
375 reason: reason.clone(),
376 });
377
378 let response = ConfirmationResponse { approved, reason };
380 let _ = confirmation.response_tx.send(response);
381
382 Ok(true)
383 } else {
384 Ok(false)
385 }
386 }
387
388 pub async fn check_timeouts(&self) -> usize {
392 0
393 }
394
395 pub async fn pending_count(&self) -> usize {
397 self.pending.read().await.len()
398 }
399
400 pub async fn pending_confirmations(&self) -> Vec<(String, String, u64)> {
402 let pending_map = self.pending.read().await;
403 pending_map
404 .values()
405 .map(|p| (p.tool_id.clone(), p.tool_name.clone(), p.remaining_ms()))
406 .collect()
407 }
408
409 pub async fn pending_confirmation_details(&self) -> Vec<PendingConfirmationInfo> {
411 let pending_map = self.pending.read().await;
412 pending_map
413 .values()
414 .map(|p| PendingConfirmationInfo {
415 tool_id: p.tool_id.clone(),
416 tool_name: p.tool_name.clone(),
417 args: p.args.clone(),
418 remaining_ms: p.remaining_ms(),
419 })
420 .collect()
421 }
422
423 pub async fn cancel(&self, tool_id: &str) -> bool {
425 let pending = {
426 let mut pending_map = self.pending.write().await;
427 pending_map.remove(tool_id)
428 };
429
430 if let Some(confirmation) = pending {
431 let response = ConfirmationResponse {
432 approved: false,
433 reason: Some("Confirmation cancelled".to_string()),
434 };
435 let _ = confirmation.response_tx.send(response);
436 true
437 } else {
438 false
439 }
440 }
441
442 pub async fn expire(&self, _tool_id: &str, _action: TimeoutAction) -> bool {
444 false
445 }
446
447 pub async fn cancel_all(&self) -> usize {
449 let pending_list: Vec<_> = {
450 let mut pending_map = self.pending.write().await;
451 pending_map.drain().collect()
452 };
453
454 let count = pending_list.len();
455
456 for (_, confirmation) in pending_list {
457 let response = ConfirmationResponse {
458 approved: false,
459 reason: Some("Confirmation cancelled".to_string()),
460 };
461 let _ = confirmation.response_tx.send(response);
462 }
463
464 count
465 }
466}
467
468#[async_trait::async_trait]
470impl ConfirmationProvider for ConfirmationManager {
471 async fn requires_confirmation(&self, tool_name: &str) -> bool {
472 self.requires_confirmation(tool_name).await
473 }
474
475 async fn request_confirmation(
476 &self,
477 tool_id: &str,
478 tool_name: &str,
479 args: &serde_json::Value,
480 ) -> oneshot::Receiver<ConfirmationResponse> {
481 self.request_confirmation(tool_id, tool_name, args).await
482 }
483
484 async fn confirm(
485 &self,
486 tool_id: &str,
487 approved: bool,
488 reason: Option<String>,
489 ) -> Result<bool, String> {
490 self.confirm(tool_id, approved, reason).await
491 }
492
493 async fn policy(&self) -> ConfirmationPolicy {
494 self.policy().await
495 }
496
497 async fn set_policy(&self, policy: ConfirmationPolicy) {
498 self.set_policy(policy).await
499 }
500
501 async fn check_timeouts(&self) -> usize {
502 self.check_timeouts().await
503 }
504
505 async fn cancel(&self, tool_id: &str) -> bool {
506 self.cancel(tool_id).await
507 }
508
509 async fn expire(&self, tool_id: &str, action: TimeoutAction) -> bool {
510 self.expire(tool_id, action).await
511 }
512
513 async fn cancel_all(&self) -> usize {
514 self.cancel_all().await
515 }
516
517 async fn pending_confirmations(&self) -> Vec<PendingConfirmationInfo> {
518 self.pending_confirmation_details().await
519 }
520}
521
522pub struct AutoApproveConfirmation;
528
529#[async_trait::async_trait]
530impl ConfirmationProvider for AutoApproveConfirmation {
531 async fn requires_confirmation(&self, _tool_name: &str) -> bool {
532 false
533 }
534
535 async fn request_confirmation(
536 &self,
537 _tool_id: &str,
538 _tool_name: &str,
539 _args: &serde_json::Value,
540 ) -> oneshot::Receiver<ConfirmationResponse> {
541 let (tx, rx) = oneshot::channel();
542 let _ = tx.send(ConfirmationResponse {
543 approved: true,
544 reason: None,
545 });
546 rx
547 }
548
549 async fn confirm(
550 &self,
551 _tool_id: &str,
552 _approved: bool,
553 _reason: Option<String>,
554 ) -> Result<bool, String> {
555 Ok(false)
556 }
557
558 async fn policy(&self) -> ConfirmationPolicy {
559 ConfirmationPolicy {
560 enabled: false,
561 ..ConfirmationPolicy::default()
562 }
563 }
564
565 async fn set_policy(&self, _policy: ConfirmationPolicy) {}
566
567 async fn check_timeouts(&self) -> usize {
568 0
569 }
570
571 async fn cancel(&self, _tool_id: &str) -> bool {
572 false
573 }
574
575 async fn expire(&self, _tool_id: &str, _action: TimeoutAction) -> bool {
576 false
577 }
578
579 async fn cancel_all(&self) -> usize {
580 0
581 }
582}
583
584#[cfg(test)]
585#[path = "hitl/tests.rs"]
586mod tests;