Skip to main content

machi_tools/
approval.rs

1//! Approval gate for destructive or privileged tool calls.
2
3use async_trait::async_trait;
4use serde_json::Value;
5
6use crate::error::{ToolError, codes};
7use crate::metadata::ToolMetadata;
8use crate::tool::DynTool;
9
10/// Decision for a pending tool call.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub enum ApprovalDecision {
14    /// Allow execution.
15    Allow,
16    /// Deny execution (fail-closed to the model as a tool error).
17    Deny,
18}
19
20/// Host-supplied gate consulted before running tools that need confirmation.
21#[async_trait]
22pub trait ApprovalGate: Send + Sync {
23    /// Decide whether `tool` may run with `arguments`.
24    async fn approve(
25        &self,
26        tool: &dyn DynTool,
27        metadata: &ToolMetadata,
28        arguments: &Value,
29    ) -> Result<ApprovalDecision, ToolError>;
30}
31
32/// Always allows (library tests / trusted offline hosts).
33#[derive(Debug, Default, Clone, Copy)]
34pub struct AutoApprove;
35
36#[async_trait]
37impl ApprovalGate for AutoApprove {
38    async fn approve(
39        &self,
40        _tool: &dyn DynTool,
41        _metadata: &ToolMetadata,
42        _arguments: &Value,
43    ) -> Result<ApprovalDecision, ToolError> {
44        Ok(ApprovalDecision::Allow)
45    }
46}
47
48/// Always denies (negative tests).
49#[derive(Debug, Default, Clone, Copy)]
50pub struct AlwaysDeny;
51
52#[async_trait]
53impl ApprovalGate for AlwaysDeny {
54    async fn approve(
55        &self,
56        tool: &dyn DynTool,
57        _metadata: &ToolMetadata,
58        _arguments: &Value,
59    ) -> Result<ApprovalDecision, ToolError> {
60        Err(codes::approval_denied(format!(
61            "approval denied for tool {}",
62            tool.name()
63        )))
64    }
65}
66
67/// Map deny decision to a tool error.
68#[must_use]
69pub fn denied_error(tool_name: &str) -> ToolError {
70    codes::approval_denied(format!("approval denied for tool {tool_name}"))
71}