vex_llm/tool.rs
1//! Tool definitions and execution framework for LLM function calling
2//!
3//! This module provides:
4//! - [`ToolDefinition`] - Metadata describing a tool's interface
5//! - [`Tool`] trait - The core interface all tools must implement
6//! - [`ToolRegistry`] - Dynamic registration and lookup of tools
7//! - [`Capability`] - Sandboxing hints for security isolation
8//!
9//! # VEX Innovation
10//!
11//! VEX tools are unique in that every execution is:
12//! 1. Validated against JSON schema
13//! 2. Executed with timeout protection
14//! 3. Hashed into the Merkle audit chain
15//!
16//! This provides cryptographic proof of what tools were used.
17//!
18//! # Security Considerations
19//!
20//! - Tools declare required capabilities for sandboxing
21//! - All tool execution has configurable timeouts (DoS protection)
22//! - Input validation is mandatory before execution
23//! - Registry prevents name collisions
24
25use async_trait::async_trait;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use std::collections::HashMap;
29use std::sync::Arc;
30use std::time::Duration;
31
32use crate::tool_error::ToolError;
33
34/// Definition of a tool that can be called by an LLM.
35///
36/// This struct holds the metadata about a tool: its name, description,
37/// and JSON Schema for parameters. It's used for:
38/// - Generating OpenAI/Anthropic-compatible tool specifications
39/// - Validating input arguments
40/// - Documentation
41///
42/// # Example
43/// ```
44/// use vex_llm::ToolDefinition;
45///
46/// const SEARCH_TOOL: ToolDefinition = ToolDefinition {
47/// name: "web_search",
48/// description: "Search the web for information",
49/// parameters: r#"{"type": "object", "properties": {"query": {"type": "string"}}}"#,
50/// };
51/// ```
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ToolDefinition {
54 /// Name of the tool (used in function calling)
55 /// Must be unique within a registry
56 pub name: &'static str,
57 /// Human-readable description of what the tool does
58 pub description: &'static str,
59 /// JSON Schema for the tool's parameters
60 pub parameters: &'static str,
61}
62
63impl ToolDefinition {
64 /// Create a new tool definition
65 pub const fn new(
66 name: &'static str,
67 description: &'static str,
68 parameters: &'static str,
69 ) -> Self {
70 Self {
71 name,
72 description,
73 parameters,
74 }
75 }
76
77 /// Convert to OpenAI-compatible tool format
78 pub fn to_openai_format(&self) -> serde_json::Value {
79 serde_json::json!({
80 "type": "function",
81 "function": {
82 "name": self.name,
83 "description": self.description,
84 "parameters": serde_json::from_str::<serde_json::Value>(self.parameters)
85 .unwrap_or(serde_json::json!({}))
86 }
87 })
88 }
89
90 /// Convert to Anthropic Claude-compatible tool format
91 pub fn to_anthropic_format(&self) -> serde_json::Value {
92 serde_json::json!({
93 "name": self.name,
94 "description": self.description,
95 "input_schema": serde_json::from_str::<serde_json::Value>(self.parameters)
96 .unwrap_or(serde_json::json!({}))
97 })
98 }
99}
100
101/// Capability requirements for sandboxing (future WASM isolation)
102///
103/// Tools declare what capabilities they need, enabling:
104/// - Security auditing (what can this tool access?)
105/// - Sandboxing decisions (can run in WASM if PureComputation only)
106/// - Permission management
107///
108/// # Security Model
109///
110/// Capabilities follow the principle of least privilege.
111/// Tools should request only what they need.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
113pub enum Capability {
114 /// Pure computation, no I/O whatsoever
115 /// Safe to run in any sandbox
116 PureComputation,
117 /// Requires network access (HTTP/TCP/UDP)
118 Network,
119 /// Requires filesystem access (read and/or write)
120 FileSystem,
121 /// Requires subprocess spawning
122 Subprocess,
123 /// Requires access to environment variables
124 Environment,
125 /// Requires cryptographic operations (signing, etc.)
126 Cryptography,
127}
128
129/// The core Tool trait — interface all tools must implement.
130///
131/// Tools are the bridge between LLM decision-making and real-world actions.
132/// Every tool must:
133/// 1. Provide its definition (name, description, schema)
134/// 2. Implement async execution
135///
136/// # Security
137///
138/// - `validate()` is called before `execute()` — reject bad input early
139/// - `capabilities()` declares what the tool needs for sandboxing
140/// - `timeout()` prevents DoS from hanging operations
141///
142/// # Example
143///
144/// ```ignore
145/// use vex_llm::{Tool, ToolDefinition, ToolError, Capability};
146/// use async_trait::async_trait;
147///
148/// pub struct MyTool {
149/// definition: ToolDefinition,
150/// }
151///
152/// #[async_trait]
153/// impl Tool for MyTool {
154/// fn definition(&self) -> &ToolDefinition {
155/// &self.definition
156/// }
157///
158/// async fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
159/// // Your implementation here
160/// Ok(serde_json::json!({"status": "done"}))
161/// }
162/// }
163/// ```
164#[async_trait]
165pub trait Tool: Send + Sync {
166 /// Returns the tool's metadata (name, description, schema)
167 fn definition(&self) -> &ToolDefinition;
168
169 /// Execute the tool with given arguments.
170 ///
171 /// # Arguments
172 ///
173 /// * `args` - JSON value matching the tool's parameter schema
174 ///
175 /// # Returns
176 ///
177 /// * `Ok(Value)` - The tool's output as JSON
178 /// * `Err(ToolError)` - If execution failed
179 ///
180 /// # Security
181 ///
182 /// Implementations should:
183 /// - Validate all inputs (even if validate() passed)
184 /// - Sanitize outputs (no raw filesystem paths, etc.)
185 /// - Respect timeouts (use tokio::select! if needed)
186 async fn execute(&self, args: Value) -> Result<Value, ToolError>;
187
188 /// Validate arguments before execution.
189 ///
190 /// Called by ToolExecutor before execute(). Override to add
191 /// custom validation beyond JSON schema checking.
192 ///
193 /// # Default
194 ///
195 /// Returns `Ok(())` — no additional validation.
196 fn validate(&self, _args: &Value) -> Result<(), ToolError> {
197 Ok(())
198 }
199
200 /// Required capabilities for sandboxing.
201 ///
202 /// # Default
203 ///
204 /// Returns `[PureComputation]` — safe for any sandbox.
205 fn capabilities(&self) -> Vec<Capability> {
206 vec![Capability::PureComputation]
207 }
208
209 /// Execution timeout.
210 ///
211 /// # Default
212 ///
213 /// 30 seconds — adjust for long-running tools.
214 fn timeout(&self) -> Duration {
215 Duration::from_secs(30)
216 }
217
218 /// Whether the tool is currently available.
219 ///
220 /// # Default
221 ///
222 /// Always returns `true`. Override to implement availability checks
223 /// (e.g., API health, rate limiting, maintenance windows).
224 fn is_available(&self) -> bool {
225 true
226 }
227}
228
229/// Registry for dynamically registered tools.
230///
231/// The registry provides:
232/// - O(1) tool lookup by name
233/// - Collision detection (no duplicate names)
234/// - Bulk operations (list, export)
235/// - Format conversion for OpenAI/Anthropic
236///
237/// # Thread Safety
238///
239/// The registry itself is not thread-safe. Wrap in `Arc<RwLock<ToolRegistry>>`
240/// if you need concurrent access. Tools within the registry are `Arc<dyn Tool>`.
241///
242/// # Example
243///
244/// ```ignore
245/// let mut registry = ToolRegistry::new();
246/// registry.register(Arc::new(MyCalculatorTool::new()));
247///
248/// if let Some(tool) = registry.get("calculator") {
249/// let result = tool.execute(json!({"expr": "2+2"})).await?;
250/// }
251/// ```
252#[derive(Default)]
253pub struct ToolRegistry {
254 tools: HashMap<String, Arc<dyn Tool>>,
255}
256
257impl ToolRegistry {
258 /// Create an empty registry
259 pub fn new() -> Self {
260 Self::default()
261 }
262
263 /// Register a tool.
264 ///
265 /// # Returns
266 ///
267 /// `true` if the tool was added, `false` if a tool with that name already exists.
268 ///
269 /// # Security
270 ///
271 /// Name collisions are rejected to prevent tool impersonation attacks.
272 pub fn register(&mut self, tool: Arc<dyn Tool>) -> bool {
273 let name = tool.definition().name.to_string();
274 if self.tools.contains_key(&name) {
275 tracing::warn!("Tool '{}' already registered, skipping duplicate", name);
276 return false;
277 }
278 self.tools.insert(name, tool);
279 true
280 }
281
282 /// Register a tool, replacing any existing tool with the same name.
283 ///
284 /// # Security Warning
285 ///
286 /// Use with caution — this can replace trusted tools with untrusted ones.
287 pub fn register_replace(&mut self, tool: Arc<dyn Tool>) {
288 let name = tool.definition().name.to_string();
289 if self.tools.contains_key(&name) {
290 tracing::warn!("Replacing existing tool '{}'", name);
291 }
292 self.tools.insert(name, tool);
293 }
294
295 /// Get a tool by name
296 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
297 self.tools.get(name).cloned()
298 }
299
300 /// Check if a tool exists
301 pub fn contains(&self, name: &str) -> bool {
302 self.tools.contains_key(name)
303 }
304
305 /// Remove a tool by name
306 pub fn remove(&mut self, name: &str) -> Option<Arc<dyn Tool>> {
307 self.tools.remove(name)
308 }
309
310 /// List all tool names
311 pub fn names(&self) -> Vec<&str> {
312 self.tools.keys().map(|s| s.as_str()).collect()
313 }
314
315 /// List all tool definitions
316 pub fn definitions(&self) -> Vec<&ToolDefinition> {
317 self.tools.values().map(|t| t.definition()).collect()
318 }
319
320 /// Generate OpenAI-compatible tool list
321 pub fn to_openai_format(&self) -> Vec<serde_json::Value> {
322 self.tools
323 .values()
324 .map(|t| t.definition().to_openai_format())
325 .collect()
326 }
327
328 /// Generate Anthropic Claude-compatible tool list
329 pub fn to_anthropic_format(&self) -> Vec<serde_json::Value> {
330 self.tools
331 .values()
332 .map(|t| t.definition().to_anthropic_format())
333 .collect()
334 }
335
336 /// Number of registered tools
337 pub fn len(&self) -> usize {
338 self.tools.len()
339 }
340
341 /// Check if registry is empty
342 pub fn is_empty(&self) -> bool {
343 self.tools.is_empty()
344 }
345
346 /// Get available tools only (where is_available() returns true)
347 pub fn available(&self) -> Vec<Arc<dyn Tool>> {
348 self.tools
349 .values()
350 .filter(|t| t.is_available())
351 .cloned()
352 .collect()
353 }
354}
355
356impl std::fmt::Debug for ToolRegistry {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 f.debug_struct("ToolRegistry")
359 .field("tools", &self.names())
360 .finish()
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_tool_definition() {
370 const TEST_TOOL: ToolDefinition =
371 ToolDefinition::new("test_tool", "A test tool", r#"{"type": "object"}"#);
372
373 assert_eq!(TEST_TOOL.name, "test_tool");
374 assert_eq!(TEST_TOOL.description, "A test tool");
375 }
376
377 #[test]
378 fn test_openai_format() {
379 let tool = ToolDefinition::new(
380 "search",
381 "Search the web",
382 r#"{"type": "object", "properties": {"query": {"type": "string"}}}"#,
383 );
384
385 let json = tool.to_openai_format();
386 assert_eq!(json["type"], "function");
387 assert_eq!(json["function"]["name"], "search");
388 }
389
390 #[test]
391 fn test_anthropic_format() {
392 let tool = ToolDefinition::new(
393 "search",
394 "Search the web",
395 r#"{"type": "object", "properties": {"query": {"type": "string"}}}"#,
396 );
397
398 let json = tool.to_anthropic_format();
399 assert_eq!(json["name"], "search");
400 assert!(json.get("input_schema").is_some());
401 }
402
403 // Mock tool for testing
404 struct MockTool {
405 definition: ToolDefinition,
406 }
407
408 impl MockTool {
409 fn new(name: &'static str) -> Self {
410 Self {
411 definition: ToolDefinition::new(name, "A mock tool", r#"{"type": "object"}"#),
412 }
413 }
414 }
415
416 #[async_trait]
417 impl Tool for MockTool {
418 fn definition(&self) -> &ToolDefinition {
419 &self.definition
420 }
421
422 async fn execute(&self, _args: Value) -> Result<Value, ToolError> {
423 Ok(serde_json::json!({"mock": true}))
424 }
425 }
426
427 #[test]
428 fn test_registry_basic() {
429 let mut registry = ToolRegistry::new();
430 assert!(registry.is_empty());
431
432 let tool = Arc::new(MockTool::new("mock"));
433 assert!(registry.register(tool));
434 assert_eq!(registry.len(), 1);
435 assert!(registry.contains("mock"));
436 }
437
438 #[test]
439 fn test_registry_duplicate_rejection() {
440 let mut registry = ToolRegistry::new();
441 registry.register(Arc::new(MockTool::new("dup")));
442
443 // Second registration should fail
444 let duplicate = Arc::new(MockTool::new("dup"));
445 assert!(!registry.register(duplicate));
446
447 // Still only one tool
448 assert_eq!(registry.len(), 1);
449 }
450
451 #[test]
452 fn test_registry_lookup() {
453 let mut registry = ToolRegistry::new();
454 registry.register(Arc::new(MockTool::new("finder")));
455
456 assert!(registry.get("finder").is_some());
457 assert!(registry.get("nonexistent").is_none());
458 }
459
460 #[test]
461 fn test_capability_enum() {
462 let caps = [Capability::PureComputation, Capability::Network];
463 assert!(caps.contains(&Capability::PureComputation));
464 assert!(!caps.contains(&Capability::FileSystem));
465 }
466}