Skip to main content

oxios_kernel/tools/
pending_tool_approvals.rs

1//! Pending tool approvals — runtime capability escalation via user consent.
2//!
3//! When a `GatedTool` denies a tool call due to missing CSpace capabilities,
4//! it can register a pending approval here and block on a oneshot. The frontend
5//! renders an approval card; the user's decision resolves the oneshot.
6//!
7//! Pattern: identical to `PendingQuestionnaires` (RFC-016).
8
9use parking_lot::Mutex;
10use std::collections::HashMap;
11use tokio::sync::oneshot;
12use uuid::Uuid;
13
14/// Result of a tool approval request.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ToolApprovalResult {
17    /// User approved — retry the tool call.
18    Approved,
19    /// User denied — return error to agent.
20    Denied,
21}
22
23struct PendingEntry {
24    tool_name: String,
25    sender: oneshot::Sender<ToolApprovalResult>,
26}
27
28/// Thread-safe registry of in-flight tool approval requests.
29#[derive(Default)]
30pub struct PendingToolApprovals {
31    inner: Mutex<HashMap<Uuid, PendingEntry>>,
32}
33
34impl PendingToolApprovals {
35    /// Create a new empty registry.
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Register a new pending tool approval.
41    /// Returns the approval ID and a receiver to await the user's decision.
42    pub fn register(&self, tool_name: String) -> (Uuid, oneshot::Receiver<ToolApprovalResult>) {
43        let id = Uuid::new_v4();
44        let (tx, rx) = oneshot::channel();
45        self.inner.lock().insert(
46            id,
47            PendingEntry {
48                tool_name,
49                sender: tx,
50            },
51        );
52        (id, rx)
53    }
54
55    /// Resolve a pending approval with the user's decision.
56    /// Returns the tool name if the entry existed.
57    pub fn resolve(&self, id: Uuid, result: ToolApprovalResult) -> Option<String> {
58        let entry = self.inner.lock().remove(&id)?;
59        let _ = entry.sender.send(result);
60        Some(entry.tool_name)
61    }
62
63    /// Cancel all pending entries (e.g., on shutdown).
64    pub fn cancel_all(&self) {
65        let mut guard = self.inner.lock();
66        for (_, entry) in guard.drain() {
67            let _ = entry.sender.send(ToolApprovalResult::Denied);
68        }
69    }
70}