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}