1use serde::Serialize;
5use serde_json::{json, Value};
6use std::fmt;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum Effect {
14 Observe,
16 Act,
18 Launch,
20 Capture,
22 Network,
24}
25
26impl Effect {
27 pub fn label(self) -> &'static str {
28 match self {
29 Effect::Observe => "observe",
30 Effect::Act => "act",
31 Effect::Launch => "launch",
32 Effect::Capture => "capture",
33 Effect::Network => "network",
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize)]
39pub struct ToolSpec {
40 pub name: String,
42 pub description: String,
43 pub input_schema: Value,
45 pub effect: Effect,
46}
47
48impl ToolSpec {
49 pub fn new(
50 name: impl Into<String>,
51 description: impl Into<String>,
52 effect: Effect,
53 input_schema: Value,
54 ) -> Self {
55 Self {
56 name: name.into(),
57 description: description.into(),
58 input_schema,
59 effect,
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize)]
66pub struct ToolError {
67 pub code: String,
68 pub message: String,
69 pub retryable: bool,
70}
71
72impl ToolError {
73 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
74 Self {
75 code: code.into(),
76 message: message.into(),
77 retryable: false,
78 }
79 }
80 pub fn retryable(code: impl Into<String>, message: impl Into<String>) -> Self {
81 Self {
82 code: code.into(),
83 message: message.into(),
84 retryable: true,
85 }
86 }
87 pub fn to_value(&self) -> Value {
88 json!({"code": self.code, "message": self.message, "retryable": self.retryable})
89 }
90}
91
92impl fmt::Display for ToolError {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "{}: {}", self.code, self.message)
95 }
96}
97impl std::error::Error for ToolError {}
98
99pub const ERR_UNKNOWN_TOOL: &str = "unknown_tool";
100pub const ERR_NOT_EXPOSED: &str = "not_exposed";
101pub const ERR_NOT_ALLOWED: &str = "tool_not_allowed";
102pub const ERR_DENIED: &str = "denied_by_admission";
103pub const ERR_INVALID_ARGS: &str = "invalid_arguments";
104pub const ERR_CANCELLED: &str = "cancelled";
105
106#[derive(Debug, Clone)]
108pub struct Caller {
109 pub app: String,
110}
111
112impl Caller {
113 pub fn new(app: impl Into<String>) -> Self {
114 Self { app: app.into() }
115 }
116}
117
118#[derive(Clone, Default)]
120pub struct CancelToken(Arc<AtomicBool>);
121impl CancelToken {
122 pub fn new() -> Self {
123 Self::default()
124 }
125 pub fn cancel(&self) {
126 self.0.store(true, Ordering::SeqCst);
127 }
128 pub fn is_cancelled(&self) -> bool {
129 self.0.load(Ordering::SeqCst)
130 }
131}
132
133pub struct CallCtx<'a> {
134 pub caller: &'a Caller,
135 pub cancel: &'a CancelToken,
136}
137
138pub trait Tool: Send + Sync {
139 fn spec(&self) -> &ToolSpec;
140 fn subject(&self, _args: &Value) -> Option<String> {
144 None
145 }
146 fn call(&self, ctx: &CallCtx, args: Value) -> Result<Value, ToolError>;
147}
148
149pub fn validate_args_strict(schema: &Value, args: &Value) -> Result<(), String> {
156 validate_args(schema, args)?;
157 let open = match &schema["additionalProperties"] {
158 Value::Bool(b) => *b,
159 Value::Object(_) => true,
160 _ => false,
161 };
162 if open {
163 return Ok(());
164 }
165 if let Value::Object(obj) = args {
166 let props = schema["properties"].as_object();
167 let mut unknown: Vec<&str> = obj
168 .keys()
169 .map(String::as_str)
170 .filter(|k| !props.is_some_and(|p| p.contains_key(*k)))
171 .collect();
172 unknown.sort_unstable();
173 if let Some(first) = unknown.first() {
174 return Err(format!("unknown argument `{first}`"));
175 }
176 }
177 Ok(())
178}
179
180pub fn validate_args(schema: &Value, args: &Value) -> Result<(), String> {
182 let obj = match args {
183 Value::Object(o) => o,
184 Value::Null => return validate_args(schema, &json!({})),
185 _ => return Err("arguments must be a JSON object".into()),
186 };
187 if let Some(req) = schema["required"].as_array() {
188 for r in req.iter().filter_map(Value::as_str) {
189 if !obj.contains_key(r) {
190 return Err(format!("missing required argument `{r}`"));
191 }
192 }
193 }
194 if let Some(props) = schema["properties"].as_object() {
195 for (k, v) in obj {
196 let Some(p) = props.get(k) else { continue };
197 if let Some(t) = p["type"].as_str() {
198 let ok = match t {
199 "string" => v.is_string(),
200 "number" => v.is_number(),
201 "integer" => v.is_i64() || v.is_u64(),
202 "boolean" => v.is_boolean(),
203 "object" => v.is_object(),
204 "array" => v.is_array(),
205 _ => true,
206 };
207 if !ok {
208 return Err(format!("argument `{k}` must be {t}"));
209 }
210 }
211 if let Some(e) = p["enum"].as_array() {
212 if !e.contains(v) {
213 return Err(format!("argument `{k}` is not one of the allowed values"));
214 }
215 }
216 }
217 }
218 Ok(())
219}
220
221#[cfg(test)]
222mod strict_tests {
223 use super::*;
224
225 fn schema() -> Value {
226 json!({"type": "object", "required": ["a"], "properties": {"a": {"type": "string"}}})
227 }
228
229 #[test]
230 fn strict_rejects_unknown_field() {
231 assert!(validate_args_strict(&schema(), &json!({"a": "x"})).is_ok());
232 let err = validate_args_strict(&schema(), &json!({"a": "x", "b": 1})).unwrap_err();
233 assert!(err.contains("`b`"));
234 assert!(validate_args(&schema(), &json!({"a": "x", "b": 1})).is_ok());
236 }
237
238 #[test]
239 fn strict_still_enforces_base_rules() {
240 assert!(validate_args_strict(&schema(), &json!({})).is_err());
241 assert!(validate_args_strict(&schema(), &json!({"a": 1})).is_err());
242 assert!(validate_args_strict(&schema(), &json!("x")).is_err());
243 }
244
245 #[test]
246 fn strict_honors_explicit_additional_properties() {
247 let mut open = schema();
248 open["additionalProperties"] = json!(true);
249 assert!(validate_args_strict(&open, &json!({"a": "x", "b": 1})).is_ok());
250 let mut closed = schema();
251 closed["additionalProperties"] = json!(false);
252 assert!(validate_args_strict(&closed, &json!({"a": "x", "b": 1})).is_err());
253 }
254
255 #[test]
256 fn strict_with_no_properties_rejects_any_field() {
257 assert!(validate_args_strict(&json!({"type": "object"}), &json!({"z": 1})).is_err());
258 assert!(validate_args_strict(&json!({"type": "object"}), &Value::Null).is_ok());
259 }
260}