use async_trait::async_trait;
use serde_json::Value;
use super::{Content, ToolContext};
use crate::types::{AgentResult, ApprovalRequest, RiskLevel};
#[derive(Clone, Debug)]
pub enum ToolDecision {
Proceed,
Block(String),
Modify(Value),
}
#[async_trait]
pub trait ToolPolicy: Send + Sync {
async fn evaluate_approval(&self, tool_name: &str, args: &Value) -> Option<ApprovalRequest>;
fn before_call(
&self,
tool_name: &str,
args: &Value,
ctx: &ToolContext,
) -> AgentResult<ToolDecision> {
let _ = (tool_name, args, ctx);
Ok(ToolDecision::Proceed)
}
fn after_call(
&self,
tool_name: &str,
args: &Value,
result: &[Content],
ctx: &ToolContext,
) -> AgentResult<()> {
let _ = (tool_name, args, result, ctx);
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct DenyAllToolPolicy;
#[async_trait]
impl ToolPolicy for DenyAllToolPolicy {
async fn evaluate_approval(&self, tool_name: &str, _args: &Value) -> Option<ApprovalRequest> {
Some(ApprovalRequest {
title: format!("Permission required: {tool_name}"),
message: format!("This agent has no permission to call `{tool_name}`."),
action_key: None,
risk_level: RiskLevel::Destructive,
raw: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
struct NoopPolicy;
#[async_trait]
impl ToolPolicy for NoopPolicy {
async fn evaluate_approval(
&self,
_tool_name: &str,
_args: &Value,
) -> Option<ApprovalRequest> {
None
}
}
#[test]
fn default_hooks_are_noop() {
let p = NoopPolicy;
let ctx = ToolContext::for_test();
let args = json!({ "x": 1 });
let decision = p.before_call("echo", &args, &ctx).unwrap();
assert!(matches!(decision, ToolDecision::Proceed));
assert!(p.after_call("echo", &args, &[], &ctx).is_ok());
}
#[tokio::test]
async fn deny_all_policy_requires_approval_for_every_tool() {
let p = DenyAllToolPolicy;
let req = p
.evaluate_approval("any_tool", &json!({}))
.await
.expect("every tool should require approval");
assert_eq!(req.risk_level, RiskLevel::Destructive);
assert!(req.title.contains("any_tool"));
}
}