Skip to main content

agent_base/tool/
policy.rs

1use async_trait::async_trait;
2use serde_json::Value;
3
4use super::{Content, ToolContext};
5use crate::types::{AgentResult, ApprovalRequest, RiskLevel};
6
7/// Decision returned by [`ToolPolicy::before_call`] to control tool execution.
8///
9/// This extends the simple Ok/Err model with the ability to modify tool
10/// arguments before execution — enabling hooks that auto-inject flags,
11/// rewrite paths, or sanitize inputs without blocking the call.
12#[derive(Clone, Debug)]
13pub enum ToolDecision {
14    /// Proceed with the original arguments.
15    Proceed,
16    /// Block the tool call entirely. The string is surfaced to the LLM as an
17    /// error message so it can correct its approach.
18    Block(String),
19    /// Proceed, but replace the tool's arguments with the provided value.
20    /// The tool executes as if the LLM had sent `modified_args` originally.
21    Modify(Value),
22}
23
24/// Policy-based control over tool execution.
25///
26/// Implement this trait to customise how tools are approved, monitored, and
27/// validated during an agent run. The pipeline calls each hook at a specific
28/// point in the tool lifecycle:
29///
30/// ```text
31/// evaluate_approval  →  before_call  →  (tool executes)  →  after_call
32/// ```
33///
34/// # Example: auto-approve read-only tools
35///
36/// ```ignore
37/// struct ReadOnlyPolicy;
38///
39/// #[async_trait]
40/// impl ToolPolicy for ReadOnlyPolicy {
41///     async fn evaluate_approval(&self, tool_name: &str, _args: &Value) -> Option<ApprovalRequest> {
42///         if tool_name == "read_file" || tool_name == "search" {
43///             None  // auto-approve — no prompt for user
44///         } else {
45///             Some(ApprovalRequest { message: format!("Allow {}?", tool_name) })
46///         }
47///     }
48/// }
49/// ```
50///
51/// All hooks have default no-op implementations, so you only need to override
52/// the ones you care about.
53#[async_trait]
54pub trait ToolPolicy: Send + Sync {
55    /// Called **before** every tool call.
56    ///
57    /// Return `None` to auto-approve (skip the approval handler entirely).
58    /// Return `Some(ApprovalRequest)` to defer to the configured
59    /// [`ApprovalHandler`](crate::ApprovalHandler).
60    ///
61    /// This is the primary hook for implementing permission guards — for
62    /// example, auto-approving read-only operations while prompting for
63    /// destructive ones.
64    async fn evaluate_approval(&self, tool_name: &str, args: &Value) -> Option<ApprovalRequest>;
65
66    /// Called immediately **before** a tool executes, after approval has been
67    /// granted (or auto-approved).
68    ///
69    /// Return a [`ToolDecision`] to control execution:
70    /// - `ToolDecision::Proceed` — execute with the original arguments (default).
71    /// - `ToolDecision::Block(msg)` — cancel the call; `msg` is sent to the LLM.
72    /// - `ToolDecision::Modify(new_args)` — execute with replacement arguments.
73    ///
74    /// Use `Modify` for:
75    /// - Auto-injecting flags (e.g. `--no-color` to shell commands).
76    /// - Path normalization or sandboxing.
77    /// - Sanitizing inputs before execution.
78    fn before_call(
79        &self,
80        tool_name: &str,
81        args: &Value,
82        ctx: &ToolContext,
83    ) -> AgentResult<ToolDecision> {
84        let _ = (tool_name, args, ctx);
85        Ok(ToolDecision::Proceed)
86    }
87
88    /// Called immediately **after** a tool executes, before the result is
89    /// returned to the LLM.
90    ///
91    /// **Note:** When [`ToolDecision::Modify`] is used in `before_call`, the
92    /// `args` parameter here reflects the **modified** arguments, not the
93    /// original ones from the LLM.
94    ///
95    /// Use this for:
96    /// - Output scrubbing / redaction (strip secrets from tool results).
97    /// - Truncation or formatting of large outputs.
98    /// - Recording metrics or audit trails.
99    ///
100    /// The `result` is the raw [`Vec<Content>`](Content) produced by the tool.
101    /// You can inspect it but not modify it through this hook — if you need to
102    /// transform the output, use a middleware instead.
103    ///
104    /// Return an `Err` to **reject** the result. The error message is surfaced
105    /// to the LLM.
106    fn after_call(
107        &self,
108        tool_name: &str,
109        args: &Value,
110        result: &[Content],
111        ctx: &ToolContext,
112    ) -> AgentResult<()> {
113        let _ = (tool_name, args, result, ctx);
114        Ok(())
115    }
116}
117
118/// A [`ToolPolicy`] that requires approval for every tool call.
119///
120/// Combined with a deny-all approval handler, this denies every tool for the
121/// agent that carries it. Used by multi-agent runtimes as a fallback when a
122/// child should have "no permission" but its parent has no [`ToolPolicy`] of
123/// its own to inherit (so there is no notion of which tools are "dangerous").
124#[derive(Debug, Clone, Default)]
125pub struct DenyAllToolPolicy;
126
127#[async_trait]
128impl ToolPolicy for DenyAllToolPolicy {
129    async fn evaluate_approval(&self, tool_name: &str, _args: &Value) -> Option<ApprovalRequest> {
130        Some(ApprovalRequest {
131            title: format!("Permission required: {tool_name}"),
132            message: format!("This agent has no permission to call `{tool_name}`."),
133            action_key: None,
134            risk_level: RiskLevel::Destructive,
135            raw: None,
136        })
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use serde_json::json;
144
145    /// Minimal policy that auto-approves everything; exercises the default
146    /// no-op `before_call` / `after_call` hooks.
147    struct NoopPolicy;
148
149    #[async_trait]
150    impl ToolPolicy for NoopPolicy {
151        async fn evaluate_approval(
152            &self,
153            _tool_name: &str,
154            _args: &Value,
155        ) -> Option<ApprovalRequest> {
156            None
157        }
158    }
159
160    #[test]
161    fn default_hooks_are_noop() {
162        let p = NoopPolicy;
163        let ctx = ToolContext::for_test();
164        let args = json!({ "x": 1 });
165        // before_call now returns ToolDecision::Proceed by default
166        let decision = p.before_call("echo", &args, &ctx).unwrap();
167        assert!(matches!(decision, ToolDecision::Proceed));
168        assert!(p.after_call("echo", &args, &[], &ctx).is_ok());
169    }
170
171    #[tokio::test]
172    async fn deny_all_policy_requires_approval_for_every_tool() {
173        let p = DenyAllToolPolicy;
174        let req = p
175            .evaluate_approval("any_tool", &json!({}))
176            .await
177            .expect("every tool should require approval");
178        assert_eq!(req.risk_level, RiskLevel::Destructive);
179        assert!(req.title.contains("any_tool"));
180    }
181}