//! Tool system: trait, registry, risk classification, and built-in tools. //! //! All tools implement the [`Tool`] trait. The [`ToolRegistry`] collects them //! and provides lookup by name plus generation of [`ToolDefinition`]s for the //! model provider. //! //! Tools execute through [`crate::sandbox::Sandbox`] -- they must never use //! `std::fs` or `std::process::Command` directly. mod list_directory; mod read_file; mod shell_exec; mod write_file; use async_trait::async_trait; use crate::core::types::ToolDefinition; use crate::sandbox::Sandbox; /// The output of a tool execution. #[derive(Debug)] pub struct ToolOutput { /// The text content returned to the model. pub content: String, /// Whether the tool encountered an error. pub is_error: bool, } /// Risk classification for tool approval gating. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RiskLevel { /// Safe to execute without user confirmation (e.g. read-only operations). AutoApprove, /// Requires explicit user approval before execution (e.g. writes, shell). RequiresApproval, } /// A tool that the model can invoke. /// /// All file I/O and process spawning must go through the [`Sandbox`] passed /// to [`Tool::execute`]. Tools must never use `std::fs` or /// `std::process::Command` directly. /// /// The `execute` method is async so that tool implementations can use /// `tokio::fs` and `tokio::process` without blocking a Tokio worker thread. /// `#[async_trait]` desugars the async fn to a boxed future, which is required /// for `dyn Tool` to remain object-safe. #[async_trait] pub trait Tool: Send + Sync { /// The name the model uses to invoke this tool. fn name(&self) -> &str; /// Human-readable description for the model. fn description(&self) -> &str; /// JSON Schema for the tool's input parameters. fn input_schema(&self) -> serde_json::Value; /// The risk level of this tool. fn risk_level(&self) -> RiskLevel; /// Execute the tool with the given input, confined by `sandbox`. async fn execute( &self, input: &serde_json::Value, sandbox: &Sandbox, ) -> Result; } /// Errors from tool execution. #[derive(Debug, thiserror::Error)] pub enum ToolError { /// An I/O error during tool execution. #[error("I/O error: {0}")] Io(#[from] std::io::Error), /// A required input field is missing or has the wrong type. #[error("invalid input: {0}")] InvalidInput(String), /// A sandbox error during tool execution. #[error("sandbox error: {0}")] Sandbox(#[from] crate::sandbox::SandboxError), } /// Collection of available tools with name-based lookup. pub struct ToolRegistry { tools: Vec>, } impl ToolRegistry { /// Create an empty registry with no tools. #[allow(dead_code)] pub fn empty() -> Self { Self { tools: Vec::new() } } /// Create a registry with the default built-in tools. pub fn default_tools() -> Self { Self { tools: vec![ Box::new(read_file::ReadFile), Box::new(list_directory::ListDirectory), Box::new(write_file::WriteFile), Box::new(shell_exec::ShellExec), ], } } /// Look up a tool by name. pub fn get(&self, name: &str) -> Option<&dyn Tool> { self.tools.iter().find(|t| t.name() == name).map(|t| &**t) } /// Generate [`ToolDefinition`]s for the model provider. pub fn definitions(&self) -> Vec { self.tools .iter() .map(|t| ToolDefinition { name: t.name().to_string(), description: t.description().to_string(), input_schema: t.input_schema(), }) .collect() } } #[cfg(test)] mod tests { use super::*; use crate::sandbox::test_sandbox; use tempfile::TempDir; #[test] fn validate_path_allows_subpath() { let dir = TempDir::new().unwrap(); let sub = dir.path().join("sub"); std::fs::create_dir(&sub).unwrap(); let sandbox = test_sandbox(dir.path()); let result = sandbox.validate_path("sub"); assert!(result.is_ok()); assert!( result .unwrap() .starts_with(dir.path().canonicalize().unwrap()) ); } #[test] fn validate_path_rejects_traversal() { let dir = TempDir::new().unwrap(); let sandbox = test_sandbox(dir.path()); let result = sandbox.validate_path("../../../etc/passwd"); assert!(result.is_err()); } #[test] fn validate_path_rejects_absolute_outside() { let dir = TempDir::new().unwrap(); let sandbox = test_sandbox(dir.path()); let result = sandbox.validate_path("/etc/passwd"); assert!(result.is_err()); } #[test] fn validate_path_allows_new_file_in_working_dir() { let dir = TempDir::new().unwrap(); let sandbox = test_sandbox(dir.path()); let result = sandbox.validate_path("new_file.txt"); assert!(result.is_ok()); } #[test] fn registry_default_has_all_tools() { let reg = ToolRegistry::default_tools(); assert!(reg.get("read_file").is_some()); assert!(reg.get("list_directory").is_some()); assert!(reg.get("write_file").is_some()); assert!(reg.get("shell_exec").is_some()); assert!(reg.get("nonexistent").is_none()); } #[test] fn registry_definitions_match_tools() { let reg = ToolRegistry::default_tools(); let defs = reg.definitions(); assert_eq!(defs.len(), 4); assert!(defs.iter().any(|d| d.name == "read_file")); } }