use std::collections::HashMap;
use std::sync::Arc;
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use crate::hooks::RunInfo;
use crate::tools::{Tool, ToolCall, ToolEffect};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "decision", rename_all = "snake_case")]
pub enum ToolDecision {
Allow,
RequireApproval {
reason: String,
},
Deny {
reason: String,
},
}
pub trait ToolPolicy: Send + Sync + std::fmt::Debug {
fn decide<'a>(
&'a self,
call: &'a ToolCall,
tool: Option<&'a dyn Tool>,
info: &'a RunInfo,
) -> BoxFuture<'a, ToolDecision>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AllowAll;
impl ToolPolicy for AllowAll {
fn decide<'a>(
&'a self,
_call: &'a ToolCall,
_tool: Option<&'a dyn Tool>,
_info: &'a RunInfo,
) -> BoxFuture<'a, ToolDecision> {
Box::pin(std::future::ready(ToolDecision::Allow))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Rule {
Allow,
Ask,
Deny(String),
}
#[derive(Debug, Clone, Default)]
pub struct ToolRules {
by_name: HashMap<String, Rule>,
by_effect: HashMap<ToolEffect, Rule>,
}
impl ToolRules {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn read_only() -> Self {
let deny = Rule::Deny("this run may only read data and update its own notes".to_owned());
Self::new()
.effect(ToolEffect::Write, deny.clone())
.effect(ToolEffect::External, deny)
}
#[must_use]
pub fn ask_before_acting() -> Self {
Self::new()
.effect(ToolEffect::Write, Rule::Ask)
.effect(ToolEffect::External, Rule::Ask)
}
#[must_use]
pub fn tool(mut self, name: impl Into<String>, rule: Rule) -> Self {
self.by_name.insert(name.into(), rule);
self
}
#[must_use]
pub fn effect(mut self, effect: ToolEffect, rule: Rule) -> Self {
self.by_effect.insert(effect, rule);
self
}
#[must_use]
pub fn decide_now(&self, call: &ToolCall, tool: Option<&dyn Tool>) -> ToolDecision {
let rule = self.by_name.get(&call.name).or_else(|| {
tool.map(Tool::effect)
.and_then(|effect| self.by_effect.get(&effect))
});
match rule {
None | Some(Rule::Allow) => ToolDecision::Allow,
Some(Rule::Ask) => ToolDecision::RequireApproval {
reason: format!("{} needs approval before it runs", call.name),
},
Some(Rule::Deny(reason)) => ToolDecision::Deny {
reason: reason.clone(),
},
}
}
}
impl ToolPolicy for ToolRules {
fn decide<'a>(
&'a self,
call: &'a ToolCall,
tool: Option<&'a dyn Tool>,
_info: &'a RunInfo,
) -> BoxFuture<'a, ToolDecision> {
Box::pin(std::future::ready(self.decide_now(call, tool)))
}
}
#[derive(Debug, Clone)]
pub struct Strictest(Vec<Arc<dyn ToolPolicy>>);
impl Strictest {
#[must_use]
pub const fn new(policies: Vec<Arc<dyn ToolPolicy>>) -> Self {
Self(policies)
}
}
impl ToolPolicy for Strictest {
fn decide<'a>(
&'a self,
call: &'a ToolCall,
tool: Option<&'a dyn Tool>,
info: &'a RunInfo,
) -> BoxFuture<'a, ToolDecision> {
Box::pin(async move {
let mut verdict = ToolDecision::Allow;
for policy in &self.0 {
match policy.decide(call, tool, info).await {
deny @ ToolDecision::Deny { .. } => return deny,
ask @ ToolDecision::RequireApproval { .. } => {
if verdict == ToolDecision::Allow {
verdict = ask;
}
}
ToolDecision::Allow => {}
}
}
verdict
})
}
}
#[cfg(test)]
mod tests;