1use crate::exposure;
8use crate::spec::*;
9use serde_json::{json, Value};
10use std::collections::{BTreeMap, HashMap};
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Arc, Mutex};
13use std::time::Instant;
14
15#[derive(Debug, Clone)]
17pub struct ToolRequest {
18 pub caller: String,
19 pub tool: String,
20 pub subject: Option<String>,
21 pub effect: Effect,
22 pub arg_keys: Vec<String>,
24 pub arg_bytes: usize,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Verdict {
29 Allow,
30 Deny(String),
31}
32
33pub trait ToolAdmission: Send + Sync {
37 fn approve(&self, req: &ToolRequest) -> Verdict;
38 fn settle(&self, _req: &ToolRequest, _ok: bool, _error_code: Option<&str>) {}
39}
40
41#[derive(Debug, Clone, PartialEq)]
43pub enum ToolEvent {
44 Started {
45 caller: String,
46 tool: String,
47 },
48 Denied {
49 caller: String,
50 tool: String,
51 by: DeniedBy,
52 reason: String,
53 },
54 Settled {
55 caller: String,
56 tool: String,
57 ok: bool,
58 error_code: Option<String>,
59 millis: u64,
60 },
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum DeniedBy {
65 Exposure,
66 Allowlist,
67 Arguments,
68 Admission,
69}
70
71impl ToolEvent {
72 pub fn to_value(&self) -> Value {
73 match self {
74 ToolEvent::Started { caller, tool } => {
75 json!({"event": "started", "caller": caller, "tool": tool})
76 }
77 ToolEvent::Denied {
78 caller,
79 tool,
80 by,
81 reason,
82 } => {
83 json!({"event": "denied", "caller": caller, "tool": tool, "by": format!("{by:?}").to_lowercase(), "reason": reason})
84 }
85 ToolEvent::Settled {
86 caller,
87 tool,
88 ok,
89 error_code,
90 millis,
91 } => {
92 json!({"event": "settled", "caller": caller, "tool": tool, "ok": ok, "error": error_code, "ms": millis})
93 }
94 }
95 }
96}
97
98pub trait Journal: Send + Sync {
99 fn record(&self, event: &ToolEvent);
100}
101
102#[derive(Default)]
103pub struct MemoryJournal(Mutex<Vec<ToolEvent>>);
104impl MemoryJournal {
105 pub fn new() -> Arc<Self> {
106 Arc::new(Self::default())
107 }
108 pub fn events(&self) -> Vec<ToolEvent> {
109 self.0.lock().unwrap_or_else(|e| e.into_inner()).clone()
110 }
111}
112impl Journal for MemoryJournal {
113 fn record(&self, event: &ToolEvent) {
114 self.0
115 .lock()
116 .unwrap_or_else(|e| e.into_inner())
117 .push(event.clone());
118 }
119}
120
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
123pub struct Metrics {
124 pub calls: u64,
125 pub ok: u64,
126 pub failed: u64,
127 pub denied: u64,
128}
129
130#[derive(Default)]
131struct Counters {
132 calls: AtomicU64,
133 ok: AtomicU64,
134 failed: AtomicU64,
135 denied: AtomicU64,
136}
137
138fn pattern_matches(pattern: &str, candidate: &str) -> bool {
143 match pattern.strip_suffix('*') {
144 Some(prefix) => candidate.starts_with(prefix),
145 None => pattern == candidate,
146 }
147}
148
149pub struct Toolbox {
150 tools: BTreeMap<String, Arc<dyn Tool>>,
151 allow: HashMap<String, Vec<String>>,
152 admission: Option<Arc<dyn ToolAdmission>>,
153 journals: Vec<Arc<dyn Journal>>,
154 counters: Counters,
155 dictation_apps: std::collections::HashSet<String>,
157}
158
159impl Default for Toolbox {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165impl Toolbox {
166 pub fn new() -> Self {
167 Self {
168 tools: BTreeMap::new(),
169 allow: HashMap::new(),
170 admission: None,
171 journals: vec![],
172 counters: Counters::default(),
173 dictation_apps: Default::default(),
174 }
175 }
176
177 pub fn protect_dictation(&mut self, app: impl Into<String>) {
180 self.dictation_apps.insert(app.into());
181 }
182
183 fn exposure_forbidden(&self, tool: &str, name: &str) -> Option<String> {
185 let app = tool.split('.').next().unwrap_or_default();
186 exposure::forbidden_with(name, self.dictation_apps.contains(app))
187 }
188
189 pub fn register(&mut self, tool: Arc<dyn Tool>) -> Result<(), ToolError> {
192 let name = tool.spec().name.clone();
193 if let Some(why) = self.exposure_forbidden(&name, &name) {
194 return Err(ToolError::new(ERR_NOT_EXPOSED, why));
195 }
196 self.tools.insert(name, tool);
197 Ok(())
198 }
199
200 pub fn register_all(&mut self, tools: Vec<Arc<dyn Tool>>) -> Result<(), ToolError> {
201 tools.into_iter().try_for_each(|t| self.register(t))
202 }
203
204 pub fn allow<I: IntoIterator<Item = S>, S: Into<String>>(
206 &mut self,
207 caller: &str,
208 patterns: I,
209 ) -> &mut Self {
210 self.allow
211 .entry(caller.to_string())
212 .or_default()
213 .extend(patterns.into_iter().map(Into::into));
214 self
215 }
216
217 pub fn set_admission(&mut self, hook: Arc<dyn ToolAdmission>) -> &mut Self {
218 self.admission = Some(hook);
219 self
220 }
221
222 pub fn add_journal(&mut self, j: Arc<dyn Journal>) -> &mut Self {
223 self.journals.push(j);
224 self
225 }
226
227 pub fn metrics(&self) -> Metrics {
228 Metrics {
229 calls: self.counters.calls.load(Ordering::SeqCst),
230 ok: self.counters.ok.load(Ordering::SeqCst),
231 failed: self.counters.failed.load(Ordering::SeqCst),
232 denied: self.counters.denied.load(Ordering::SeqCst),
233 }
234 }
235
236 fn permitted(&self, caller: &str, tool: &str, subject: Option<&str>) -> bool {
237 let Some(patterns) = self.allow.get(caller) else {
238 return false;
239 };
240 match subject {
241 Some(s) => {
242 let full = format!("{tool}:{s}");
243 patterns.iter().any(|p| pattern_matches(p, &full))
244 }
245 None => patterns.iter().any(|p| pattern_matches(p, tool)),
246 }
247 }
248
249 pub fn list_for(&self, caller: &str) -> Vec<ToolSpec> {
252 let Some(patterns) = self.allow.get(caller) else {
253 return vec![];
254 };
255 self.tools
256 .values()
257 .filter(|t| {
258 let n = &t.spec().name;
259 patterns.iter().any(|p| {
260 pattern_matches(p, n)
261 || p.starts_with(&format!("{n}:"))
262 || (p.ends_with('*') && n.starts_with(p.trim_end_matches('*')))
263 })
264 })
265 .map(|t| t.spec().clone())
266 .collect()
267 }
268
269 pub fn list_all(&self) -> Vec<ToolSpec> {
270 self.tools.values().map(|t| t.spec().clone()).collect()
271 }
272
273 fn emit(&self, ev: ToolEvent) {
274 for j in &self.journals {
275 j.record(&ev);
276 }
277 }
278
279 fn deny(
280 &self,
281 caller: &str,
282 tool: &str,
283 by: DeniedBy,
284 code: &str,
285 reason: String,
286 ) -> ToolError {
287 self.counters.denied.fetch_add(1, Ordering::SeqCst);
288 self.emit(ToolEvent::Denied {
289 caller: caller.into(),
290 tool: tool.into(),
291 by,
292 reason: reason.clone(),
293 });
294 ToolError::new(code, reason)
295 }
296
297 pub fn call(&self, caller: &Caller, name: &str, args: Value) -> Result<Value, ToolError> {
298 self.call_cancellable(caller, name, args, &CancelToken::new())
299 }
300
301 pub fn call_cancellable(
302 &self,
303 caller: &Caller,
304 name: &str,
305 args: Value,
306 cancel: &CancelToken,
307 ) -> Result<Value, ToolError> {
308 self.counters.calls.fetch_add(1, Ordering::SeqCst);
309 let tool = self
310 .tools
311 .get(name)
312 .cloned()
313 .ok_or_else(|| ToolError::new(ERR_UNKNOWN_TOOL, format!("no tool `{name}`")))?;
314 let subject = tool.subject(&args);
315
316 if let Some(why) = self.exposure_forbidden(name, name).or_else(|| {
317 subject
318 .as_deref()
319 .and_then(|s| self.exposure_forbidden(name, s))
320 }) {
321 return Err(self.deny(&caller.app, name, DeniedBy::Exposure, ERR_NOT_EXPOSED, why));
322 }
323 if !self.permitted(&caller.app, name, subject.as_deref()) {
324 let what = subject
325 .as_deref()
326 .map(|s| format!("{name}:{s}"))
327 .unwrap_or_else(|| name.to_string());
328 return Err(self.deny(
329 &caller.app,
330 name,
331 DeniedBy::Allowlist,
332 ERR_NOT_ALLOWED,
333 format!("`{}` may not call `{what}`", caller.app),
334 ));
335 }
336 if let Err(e) = validate_args(&tool.spec().input_schema, &args) {
337 return Err(self.deny(&caller.app, name, DeniedBy::Arguments, ERR_INVALID_ARGS, e));
338 }
339
340 let req = ToolRequest {
341 caller: caller.app.clone(),
342 tool: name.to_string(),
343 subject,
344 effect: tool.spec().effect,
345 arg_keys: args
346 .as_object()
347 .map(|o| o.keys().cloned().collect())
348 .unwrap_or_default(),
349 arg_bytes: args.to_string().len(),
350 };
351 if let Some(h) = &self.admission {
352 if let Verdict::Deny(reason) = h.approve(&req) {
353 return Err(self.deny(&caller.app, name, DeniedBy::Admission, ERR_DENIED, reason));
354 }
355 }
356
357 self.emit(ToolEvent::Started {
358 caller: caller.app.clone(),
359 tool: name.into(),
360 });
361 let started = Instant::now();
362 let ctx = CallCtx { caller, cancel };
363 let res = if cancel.is_cancelled() {
364 Err(ToolError::new(ERR_CANCELLED, "cancelled before start"))
365 } else {
366 tool.call(&ctx, args)
367 };
368 let (ok, code) = match &res {
369 Ok(_) => (true, None),
370 Err(e) => (false, Some(e.code.clone())),
371 };
372 if ok {
373 self.counters.ok.fetch_add(1, Ordering::SeqCst);
374 } else {
375 self.counters.failed.fetch_add(1, Ordering::SeqCst);
376 }
377 if let Some(h) = &self.admission {
378 h.settle(&req, ok, code.as_deref());
379 }
380 self.emit(ToolEvent::Settled {
381 caller: caller.app.clone(),
382 tool: name.into(),
383 ok,
384 error_code: code,
385 millis: started.elapsed().as_millis() as u64,
386 });
387 res
388 }
389}