autumn_plugin_agent/
policy.rs1use std::collections::HashMap;
18use std::sync::Arc;
19
20use futures::future::BoxFuture;
21use serde::{Deserialize, Serialize};
22
23use crate::hooks::RunInfo;
24use crate::tools::{Tool, ToolCall, ToolEffect};
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(tag = "decision", rename_all = "snake_case")]
29pub enum ToolDecision {
30 Allow,
32 RequireApproval {
34 reason: String,
36 },
37 Deny {
39 reason: String,
41 },
42}
43
44pub trait ToolPolicy: Send + Sync + std::fmt::Debug {
46 fn decide<'a>(
49 &'a self,
50 call: &'a ToolCall,
51 tool: Option<&'a dyn Tool>,
52 info: &'a RunInfo,
53 ) -> BoxFuture<'a, ToolDecision>;
54}
55
56#[derive(Debug, Clone, Copy, Default)]
58pub struct AllowAll;
59
60impl ToolPolicy for AllowAll {
61 fn decide<'a>(
62 &'a self,
63 _call: &'a ToolCall,
64 _tool: Option<&'a dyn Tool>,
65 _info: &'a RunInfo,
66 ) -> BoxFuture<'a, ToolDecision> {
67 Box::pin(std::future::ready(ToolDecision::Allow))
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum Rule {
74 Allow,
76 Ask,
78 Deny(String),
80}
81
82#[derive(Debug, Clone, Default)]
99pub struct ToolRules {
100 by_name: HashMap<String, Rule>,
101 by_effect: HashMap<ToolEffect, Rule>,
102}
103
104impl ToolRules {
105 #[must_use]
107 pub fn new() -> Self {
108 Self::default()
109 }
110
111 #[must_use]
114 pub fn read_only() -> Self {
115 let deny = Rule::Deny("this run may only read data and update its own notes".to_owned());
116 Self::new()
117 .effect(ToolEffect::Write, deny.clone())
118 .effect(ToolEffect::External, deny)
119 }
120
121 #[must_use]
124 pub fn ask_before_acting() -> Self {
125 Self::new()
126 .effect(ToolEffect::Write, Rule::Ask)
127 .effect(ToolEffect::External, Rule::Ask)
128 }
129
130 #[must_use]
132 pub fn tool(mut self, name: impl Into<String>, rule: Rule) -> Self {
133 self.by_name.insert(name.into(), rule);
134 self
135 }
136
137 #[must_use]
139 pub fn effect(mut self, effect: ToolEffect, rule: Rule) -> Self {
140 self.by_effect.insert(effect, rule);
141 self
142 }
143
144 #[must_use]
146 pub fn decide_now(&self, call: &ToolCall, tool: Option<&dyn Tool>) -> ToolDecision {
147 let rule = self.by_name.get(&call.name).or_else(|| {
148 tool.map(Tool::effect)
149 .and_then(|effect| self.by_effect.get(&effect))
150 });
151 match rule {
152 None | Some(Rule::Allow) => ToolDecision::Allow,
153 Some(Rule::Ask) => ToolDecision::RequireApproval {
154 reason: format!("{} needs approval before it runs", call.name),
155 },
156 Some(Rule::Deny(reason)) => ToolDecision::Deny {
157 reason: reason.clone(),
158 },
159 }
160 }
161}
162
163impl ToolPolicy for ToolRules {
164 fn decide<'a>(
165 &'a self,
166 call: &'a ToolCall,
167 tool: Option<&'a dyn Tool>,
168 _info: &'a RunInfo,
169 ) -> BoxFuture<'a, ToolDecision> {
170 Box::pin(std::future::ready(self.decide_now(call, tool)))
171 }
172}
173
174#[derive(Debug, Clone)]
180pub struct Strictest(Vec<Arc<dyn ToolPolicy>>);
181
182impl Strictest {
183 #[must_use]
185 pub const fn new(policies: Vec<Arc<dyn ToolPolicy>>) -> Self {
186 Self(policies)
187 }
188}
189
190impl ToolPolicy for Strictest {
191 fn decide<'a>(
192 &'a self,
193 call: &'a ToolCall,
194 tool: Option<&'a dyn Tool>,
195 info: &'a RunInfo,
196 ) -> BoxFuture<'a, ToolDecision> {
197 Box::pin(async move {
198 let mut verdict = ToolDecision::Allow;
199 for policy in &self.0 {
200 match policy.decide(call, tool, info).await {
201 deny @ ToolDecision::Deny { .. } => return deny,
202 ask @ ToolDecision::RequireApproval { .. } => {
203 if verdict == ToolDecision::Allow {
204 verdict = ask;
205 }
206 }
207 ToolDecision::Allow => {}
208 }
209 }
210 verdict
211 })
212 }
213}
214
215#[cfg(test)]
216mod tests;