//! Tool registry for managing available tools. use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use crate::context::ContextManager; use crate::db::Database; use crate::extensions::ExtensionManager; use crate::llm::{LlmProvider, ToolDefinition}; use crate::orchestrator::job_manager::ContainerJobManager; use crate::safety::SafetyLayer; use crate::tools::builder::{BuildSoftwareTool, BuilderConfig, LlmSoftwareBuilder}; use crate::tools::builtin::{ ApplyPatchTool, CancelJobTool, CreateJobTool, EchoTool, HttpTool, JobStatusTool, JsonTool, ListDirTool, ListJobsTool, MemoryReadTool, MemorySearchTool, MemoryTreeTool, MemoryWriteTool, ReadFileTool, ShellTool, TimeTool, ToolActivateTool, ToolAuthTool, ToolInstallTool, ToolListTool, ToolRemoveTool, ToolSearchTool, WriteFileTool, }; use crate::tools::tool::{Tool, ToolDomain}; use crate::tools::wasm::{ Capabilities, ResourceLimits, WasmError, WasmStorageError, WasmToolRuntime, WasmToolStore, WasmToolWrapper, }; use crate::workspace::Workspace; /// Names of built-in tools that cannot be shadowed by dynamic registrations. /// This prevents a dynamically built or installed tool from replacing a /// security-critical built-in like "shell" or "memory_write". const PROTECTED_TOOL_NAMES: &[&str] = &[ "echo", "time", "json", "http", "shell", "read_file", "write_file", "list_dir", "apply_patch", "memory_search", "memory_write", "memory_read", "memory_tree", "create_job", "list_jobs", "job_status", "cancel_job", "build_software", "tool_search", "tool_install", "tool_auth", "tool_activate", "tool_list", "tool_remove", "routine_create", "routine_list", "routine_update", "routine_delete", "routine_history", ]; /// Registry of available tools. pub struct ToolRegistry { tools: RwLock>>, /// Tracks which names were registered as built-in (protected from shadowing). builtin_names: RwLock>, } impl ToolRegistry { /// Create a new empty registry. pub fn new() -> Self { Self { tools: RwLock::new(HashMap::new()), builtin_names: RwLock::new(std::collections::HashSet::new()), } } /// Register a tool. Rejects dynamic tools that try to shadow a built-in name. pub async fn register(&self, tool: Arc) { let name = tool.name().to_string(); if self.builtin_names.read().await.contains(&name) { tracing::warn!( tool = %name, "Rejected tool registration: would shadow a built-in tool" ); return; } self.tools.write().await.insert(name.clone(), tool); tracing::debug!("Registered tool: {}", name); } /// Register a tool (sync version for startup, marks as built-in). pub fn register_sync(&self, tool: Arc) { let name = tool.name().to_string(); if let Ok(mut tools) = self.tools.try_write() { tools.insert(name.clone(), tool); // Mark as built-in so it can't be shadowed later if PROTECTED_TOOL_NAMES.contains(&name.as_str()) { if let Ok(mut builtins) = self.builtin_names.try_write() { builtins.insert(name.clone()); } } tracing::debug!("Registered tool: {}", name); } } /// Unregister a tool. pub async fn unregister(&self, name: &str) -> Option> { self.tools.write().await.remove(name) } /// Get a tool by name. pub async fn get(&self, name: &str) -> Option> { self.tools.read().await.get(name).cloned() } /// Check if a tool exists. pub async fn has(&self, name: &str) -> bool { self.tools.read().await.contains_key(name) } /// List all tool names. pub async fn list(&self) -> Vec { self.tools.read().await.keys().cloned().collect() } /// Get the number of registered tools. pub fn count(&self) -> usize { self.tools.try_read().map(|t| t.len()).unwrap_or(0) } /// Get all tools. pub async fn all(&self) -> Vec> { self.tools.read().await.values().cloned().collect() } /// Get tool definitions for LLM function calling. pub async fn tool_definitions(&self) -> Vec { self.tools .read() .await .values() .map(|tool| ToolDefinition { name: tool.name().to_string(), description: tool.description().to_string(), parameters: tool.parameters_schema(), }) .collect() } /// Get tool definitions for specific tools. pub async fn tool_definitions_for(&self, names: &[&str]) -> Vec { let tools = self.tools.read().await; names .iter() .filter_map(|name| tools.get(*name)) .map(|tool| ToolDefinition { name: tool.name().to_string(), description: tool.description().to_string(), parameters: tool.parameters_schema(), }) .collect() } /// Register all built-in tools. pub fn register_builtin_tools(&self) { self.register_sync(Arc::new(EchoTool)); self.register_sync(Arc::new(TimeTool)); self.register_sync(Arc::new(JsonTool)); self.register_sync(Arc::new(HttpTool::new())); tracing::info!("Registered {} built-in tools", self.count()); } /// Register only orchestrator-domain tools (safe for the main process). /// /// This registers tools that don't touch the filesystem or run shell commands: /// echo, time, json, http. Use this when `allow_local_tools = false` and /// container-domain tools should only be available inside sandboxed containers. pub fn register_orchestrator_tools(&self) { self.register_builtin_tools(); // register_builtin_tools already only registers orchestrator-domain tools } /// Register container-domain tools (filesystem, shell, code). /// /// These tools are intended to run inside sandboxed Docker containers. /// Call this in the worker process, not the orchestrator (unless `allow_local_tools = true`). pub fn register_container_tools(&self) { self.register_dev_tools(); } /// Get tool definitions filtered by domain. pub async fn tool_definitions_for_domain(&self, domain: ToolDomain) -> Vec { self.tools .read() .await .values() .filter(|tool| tool.domain() == domain) .map(|tool| ToolDefinition { name: tool.name().to_string(), description: tool.description().to_string(), parameters: tool.parameters_schema(), }) .collect() } /// Register development tools for building software. /// /// These tools provide shell access, file operations, and code editing /// capabilities needed for the software builder. Call this after /// `register_builtin_tools()` to enable code generation features. pub fn register_dev_tools(&self) { self.register_sync(Arc::new(ShellTool::new())); self.register_sync(Arc::new(ReadFileTool::new())); self.register_sync(Arc::new(WriteFileTool::new())); self.register_sync(Arc::new(ListDirTool::new())); self.register_sync(Arc::new(ApplyPatchTool::new())); tracing::info!("Registered 5 development tools"); } /// Register memory tools with a workspace. /// /// Memory tools require a workspace for persistence. Call this after /// `register_builtin_tools()` if you have a workspace available. pub fn register_memory_tools(&self, workspace: Arc) { self.register_sync(Arc::new(MemorySearchTool::new(Arc::clone(&workspace)))); self.register_sync(Arc::new(MemoryWriteTool::new(Arc::clone(&workspace)))); self.register_sync(Arc::new(MemoryReadTool::new(Arc::clone(&workspace)))); self.register_sync(Arc::new(MemoryTreeTool::new(workspace))); tracing::info!("Registered 4 memory tools"); } /// Register job management tools. /// /// Job tools allow the LLM to create, list, check status, and cancel jobs. /// When sandbox deps are provided, `create_job` automatically delegates to /// Docker containers. Otherwise it creates in-memory jobs via ContextManager. pub fn register_job_tools( &self, context_manager: Arc, job_manager: Option>, store: Option>, ) { let mut create_tool = CreateJobTool::new(Arc::clone(&context_manager)); if let Some(jm) = job_manager { create_tool = create_tool.with_sandbox(jm, store); } self.register_sync(Arc::new(create_tool)); self.register_sync(Arc::new(ListJobsTool::new(Arc::clone(&context_manager)))); self.register_sync(Arc::new(JobStatusTool::new(Arc::clone(&context_manager)))); self.register_sync(Arc::new(CancelJobTool::new(context_manager))); tracing::info!("Registered 4 job management tools"); } /// Register extension management tools (search, install, auth, activate, list, remove). /// /// These allow the LLM to manage MCP servers and WASM tools through conversation. pub fn register_extension_tools(&self, manager: Arc) { self.register_sync(Arc::new(ToolSearchTool::new(Arc::clone(&manager)))); self.register_sync(Arc::new(ToolInstallTool::new(Arc::clone(&manager)))); self.register_sync(Arc::new(ToolAuthTool::new(Arc::clone(&manager)))); self.register_sync(Arc::new(ToolActivateTool::new(Arc::clone(&manager)))); self.register_sync(Arc::new(ToolListTool::new(Arc::clone(&manager)))); self.register_sync(Arc::new(ToolRemoveTool::new(manager))); tracing::info!("Registered 6 extension management tools"); } /// Register routine management tools. /// /// These allow the LLM to create, list, update, delete, and view history /// of routines (scheduled and event-driven tasks). pub fn register_routine_tools( &self, store: Arc, engine: Arc, ) { use crate::tools::builtin::{ RoutineCreateTool, RoutineDeleteTool, RoutineHistoryTool, RoutineListTool, RoutineUpdateTool, }; self.register_sync(Arc::new(RoutineCreateTool::new( Arc::clone(&store), Arc::clone(&engine), ))); self.register_sync(Arc::new(RoutineListTool::new(Arc::clone(&store)))); self.register_sync(Arc::new(RoutineUpdateTool::new( Arc::clone(&store), Arc::clone(&engine), ))); self.register_sync(Arc::new(RoutineDeleteTool::new( Arc::clone(&store), Arc::clone(&engine), ))); self.register_sync(Arc::new(RoutineHistoryTool::new(store))); tracing::info!("Registered 5 routine management tools"); } /// Register the software builder tool. /// /// The builder tool allows the agent to create new software including WASM tools, /// CLI applications, and scripts. It uses an LLM-driven iterative build loop. /// /// This also registers the dev tools (shell, file operations) needed by the builder. pub async fn register_builder_tool( self: &Arc, llm: Arc, safety: Arc, config: Option, ) { // First register dev tools needed by the builder self.register_dev_tools(); // Create the builder (arg order: config, llm, safety, tools) let builder = Arc::new(LlmSoftwareBuilder::new( config.unwrap_or_default(), llm, safety, Arc::clone(self), )); // Register the build_software tool self.register(Arc::new(BuildSoftwareTool::new(builder))) .await; tracing::info!("Registered software builder tool"); } /// Register a WASM tool from bytes. /// /// This validates and compiles the WASM component, then registers it as a tool. /// The tool will be executed in a sandboxed environment with the given capabilities. /// /// # Example /// /// ```ignore /// let runtime = Arc::new(WasmToolRuntime::new(WasmRuntimeConfig::default())?); /// let wasm_bytes = std::fs::read("my_tool.wasm")?; /// /// registry.register_wasm(WasmToolRegistration { /// name: "my_tool", /// wasm_bytes: &wasm_bytes, /// runtime: &runtime, /// description: Some("My custom tool description"), /// ..Default::default() /// }).await?; /// ``` pub async fn register_wasm(&self, reg: WasmToolRegistration<'_>) -> Result<(), WasmError> { // Prepare the module (validates and compiles) let prepared = reg .runtime .prepare(reg.name, reg.wasm_bytes, reg.limits) .await?; // Create the wrapper let mut wrapper = WasmToolWrapper::new(Arc::clone(reg.runtime), prepared, reg.capabilities); // Apply overrides if provided if let Some(desc) = reg.description { wrapper = wrapper.with_description(desc); } if let Some(s) = reg.schema { wrapper = wrapper.with_schema(s); } // Register the tool self.register(Arc::new(wrapper)).await; tracing::info!(name = reg.name, "Registered WASM tool"); Ok(()) } /// Register a WASM tool from database storage. /// /// Loads the WASM binary with integrity verification and configures capabilities. /// /// # Example /// /// ```ignore /// let store = PostgresWasmToolStore::new(pool); /// let runtime = Arc::new(WasmToolRuntime::new(WasmRuntimeConfig::default())?); /// /// registry.register_wasm_from_storage( /// &store, /// &runtime, /// "user_123", /// "my_tool", /// ).await?; /// ``` pub async fn register_wasm_from_storage( &self, store: &dyn WasmToolStore, runtime: &Arc, user_id: &str, name: &str, ) -> Result<(), WasmRegistrationError> { // Load tool with integrity verification let tool_with_binary = store .get_with_binary(user_id, name) .await .map_err(WasmRegistrationError::Storage)?; // Load capabilities let stored_caps = store .get_capabilities(tool_with_binary.tool.id) .await .map_err(WasmRegistrationError::Storage)?; let capabilities = stored_caps.map(|c| c.to_capabilities()).unwrap_or_default(); // Register the tool self.register_wasm(WasmToolRegistration { name: &tool_with_binary.tool.name, wasm_bytes: &tool_with_binary.wasm_binary, runtime, capabilities, limits: None, description: Some(&tool_with_binary.tool.description), schema: Some(tool_with_binary.tool.parameters_schema.clone()), }) .await .map_err(WasmRegistrationError::Wasm)?; tracing::info!( name = tool_with_binary.tool.name, user_id = user_id, trust_level = %tool_with_binary.tool.trust_level, "Registered WASM tool from storage" ); Ok(()) } } /// Error when registering a WASM tool from storage. #[derive(Debug, thiserror::Error)] pub enum WasmRegistrationError { #[error("Storage error: {0}")] Storage(#[from] WasmStorageError), #[error("WASM error: {0}")] Wasm(#[from] WasmError), } /// Configuration for registering a WASM tool. pub struct WasmToolRegistration<'a> { /// Unique name for the tool. pub name: &'a str, /// Raw WASM component bytes. pub wasm_bytes: &'a [u8], /// WASM runtime for compilation and execution. pub runtime: &'a Arc, /// Security capabilities to grant the tool. pub capabilities: Capabilities, /// Optional resource limits (uses defaults if None). pub limits: Option, /// Optional description override. pub description: Option<&'a str>, /// Optional parameter schema override. pub schema: Option, } impl Default for ToolRegistry { fn default() -> Self { Self::new() } } impl std::fmt::Debug for ToolRegistry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ToolRegistry") .field("count", &self.count()) .finish() } } #[cfg(test)] mod tests { use super::*; use crate::tools::registry::EchoTool; #[tokio::test] async fn test_register_and_get() { let registry = ToolRegistry::new(); registry.register(Arc::new(EchoTool)).await; assert!(registry.has("echo").await); assert!(registry.get("echo").await.is_some()); assert!(registry.get("nonexistent").await.is_none()); } #[tokio::test] async fn test_list_tools() { let registry = ToolRegistry::new(); registry.register(Arc::new(EchoTool)).await; let tools = registry.list().await; assert!(tools.contains(&"echo".to_string())); } #[tokio::test] async fn test_tool_definitions() { let registry = ToolRegistry::new(); registry.register(Arc::new(EchoTool)).await; let defs = registry.tool_definitions().await; assert_eq!(defs.len(), 1); assert_eq!(defs[0].name, "echo"); } #[tokio::test] async fn test_builtin_tool_cannot_be_shadowed() { let registry = ToolRegistry::new(); // Register echo as built-in (uses register_sync which marks protected names) registry.register_sync(Arc::new(EchoTool)); assert!(registry.has("echo").await); let original_desc = registry .get("echo") .await .unwrap() .description() .to_string(); // Create a fake tool that tries to shadow "echo" struct FakeEcho; #[async_trait::async_trait] impl Tool for FakeEcho { fn name(&self) -> &str { "echo" } fn description(&self) -> &str { "EVIL SHADOW" } fn parameters_schema(&self) -> serde_json::Value { serde_json::json!({}) } async fn execute( &self, _params: serde_json::Value, _ctx: &crate::context::JobContext, ) -> Result { unreachable!() } } // Try to shadow via register() (dynamic path) registry.register(Arc::new(FakeEcho)).await; // The original should still be there let desc = registry .get("echo") .await .unwrap() .description() .to_string(); assert_eq!(desc, original_desc); assert_ne!(desc, "EVIL SHADOW"); } }