Files
tkmind_go/crates/goose/src/agents/tool_confirmation_router.rs
T

145 lines
4.3 KiB
Rust

use std::collections::HashMap;
use tokio::sync::{oneshot, Mutex};
use tracing::warn;
use crate::permission::PermissionConfirmation;
pub struct ToolConfirmationRouter {
pending: Mutex<HashMap<String, oneshot::Sender<PermissionConfirmation>>>,
}
impl ToolConfirmationRouter {
pub fn new() -> Self {
Self {
pending: Mutex::new(HashMap::new()),
}
}
pub async fn register(&self, request_id: String) -> oneshot::Receiver<PermissionConfirmation> {
let (tx, rx) = oneshot::channel();
let mut pending = self.pending.lock().await;
pending.retain(|_, sender| !sender.is_closed());
pending.insert(request_id, tx);
rx
}
pub async fn deliver(&self, request_id: String, confirmation: PermissionConfirmation) -> bool {
if let Some(tx) = self.pending.lock().await.remove(&request_id) {
if tx.send(confirmation).is_err() {
warn!(
request_id = %request_id,
"Confirmation receiver was dropped (task cancelled)"
);
false
} else {
true
}
} else {
warn!(
request_id = %request_id,
"No task waiting for confirmation"
);
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::permission::permission_confirmation::PrincipalType;
use crate::permission::Permission;
fn test_confirmation() -> PermissionConfirmation {
PermissionConfirmation {
principal_type: PrincipalType::Tool,
permission: Permission::AllowOnce,
}
}
#[tokio::test]
async fn test_register_then_deliver() {
let router = ToolConfirmationRouter::new();
let rx = router.register("req_1".to_string()).await;
assert!(
router
.deliver("req_1".to_string(), test_confirmation())
.await
);
let confirmation = rx.await.unwrap();
assert_eq!(confirmation.permission, Permission::AllowOnce);
}
#[tokio::test]
async fn test_deliver_unknown_request() {
let router = ToolConfirmationRouter::new();
assert!(
!router
.deliver("unknown".to_string(), test_confirmation())
.await
);
}
#[tokio::test]
async fn test_cancelled_receiver() {
let router = ToolConfirmationRouter::new();
let rx = router.register("req_1".to_string()).await;
drop(rx); // simulate task cancellation
assert!(
!router
.deliver("req_1".to_string(), test_confirmation())
.await
);
}
#[tokio::test]
async fn test_stale_entries_pruned_on_register() {
let router = ToolConfirmationRouter::new();
let rx = router.register("req_1".to_string()).await;
drop(rx); // simulate task cancellation — entry is now stale
assert_eq!(router.pending.lock().await.len(), 1);
let _rx2 = router.register("req_2".to_string()).await;
assert_eq!(router.pending.lock().await.len(), 1); // only req_2 remains
assert!(router.pending.lock().await.contains_key("req_2"));
}
#[tokio::test]
async fn test_concurrent_requests_out_of_order() {
use std::sync::Arc;
let router = Arc::new(ToolConfirmationRouter::new());
// Register two requests
let rx1 = router.register("req_1".to_string()).await;
let rx2 = router.register("req_2".to_string()).await;
// Deliver in reverse order
assert!(
router
.deliver(
"req_2".to_string(),
PermissionConfirmation {
principal_type: PrincipalType::Tool,
permission: Permission::DenyOnce,
}
)
.await
);
assert_eq!(router.pending.lock().await.len(), 1);
assert!(
router
.deliver("req_1".to_string(), test_confirmation())
.await
);
assert_eq!(router.pending.lock().await.len(), 0);
let c1 = rx1.await.unwrap();
assert_eq!(c1.permission, Permission::AllowOnce);
let c2 = rx2.await.unwrap();
assert_eq!(c2.permission, Permission::DenyOnce);
}
}