//! Tool subsystem for agent-callable capabilities. //! //! This module implements the tool execution surface exposed to the LLM during //! agentic loops. Each tool implements the [`Tool`] trait defined in [`traits`], //! which requires a name, description, JSON parameter schema, and an async //! `execute` method returning a structured [`ToolResult`]. //! //! Tools are assembled into registries by [`default_tools`] (shell, file read/write) //! and [`all_tools`] (full set including memory, browser, cron, HTTP, delegation, //! and optional integrations). Security policy enforcement is injected via //! [`SecurityPolicy`](crate::security::SecurityPolicy) at construction time. //! //! # Extension //! //! To add a new tool, implement [`Tool`] in a new submodule and register it in //! [`all_tools_with_runtime`]. See `AGENTS.md` ยง7.3 for the full change playbook. pub mod browser; pub mod browser_open; pub mod cli_discovery; pub mod composio; pub mod content_search; pub mod cron_add; pub mod cron_list; pub mod cron_remove; pub mod cron_run; pub mod cron_runs; pub mod cron_update; pub mod delegate; pub mod file_edit; pub mod file_read; pub mod file_write; pub mod git_operations; pub mod glob_search; pub mod hardware_board_info; pub mod hardware_memory_map; pub mod hardware_memory_read; pub mod http_request; pub mod image_info; pub mod memory_forget; pub mod memory_recall; pub mod memory_store; pub mod model_routing_config; pub mod pdf_read; pub mod proxy_config; pub mod pushover; pub mod schedule; pub mod schema; pub mod screenshot; pub mod shell; pub mod traits; pub mod web_search_tool; pub use browser::{BrowserTool, ComputerUseConfig}; pub use browser_open::BrowserOpenTool; pub use composio::ComposioTool; pub use content_search::ContentSearchTool; pub use cron_add::CronAddTool; pub use cron_list::CronListTool; pub use cron_remove::CronRemoveTool; pub use cron_run::CronRunTool; pub use cron_runs::CronRunsTool; pub use cron_update::CronUpdateTool; pub use delegate::DelegateTool; pub use file_edit::FileEditTool; pub use file_read::FileReadTool; pub use file_write::FileWriteTool; pub use git_operations::GitOperationsTool; pub use glob_search::GlobSearchTool; pub use hardware_board_info::HardwareBoardInfoTool; pub use hardware_memory_map::HardwareMemoryMapTool; pub use hardware_memory_read::HardwareMemoryReadTool; pub use http_request::HttpRequestTool; pub use image_info::ImageInfoTool; pub use memory_forget::MemoryForgetTool; pub use memory_recall::MemoryRecallTool; pub use memory_store::MemoryStoreTool; pub use model_routing_config::ModelRoutingConfigTool; pub use pdf_read::PdfReadTool; pub use proxy_config::ProxyConfigTool; pub use pushover::PushoverTool; pub use schedule::ScheduleTool; #[allow(unused_imports)] pub use schema::{CleaningStrategy, SchemaCleanr}; pub use screenshot::ScreenshotTool; pub use shell::ShellTool; pub use traits::Tool; #[allow(unused_imports)] pub use traits::{ToolResult, ToolSpec}; pub use web_search_tool::WebSearchTool; use crate::config::{Config, DelegateAgentConfig}; use crate::memory::Memory; use crate::runtime::{NativeRuntime, RuntimeAdapter}; use crate::security::SecurityPolicy; use async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; #[derive(Clone)] struct ArcDelegatingTool { inner: Arc, } impl ArcDelegatingTool { fn boxed(inner: Arc) -> Box { Box::new(Self { inner }) } } #[async_trait] impl Tool for ArcDelegatingTool { fn name(&self) -> &str { self.inner.name() } fn description(&self) -> &str { self.inner.description() } fn parameters_schema(&self) -> serde_json::Value { self.inner.parameters_schema() } async fn execute(&self, args: serde_json::Value) -> anyhow::Result { self.inner.execute(args).await } } fn boxed_registry_from_arcs(tools: Vec>) -> Vec> { tools.into_iter().map(ArcDelegatingTool::boxed).collect() } /// Create the default tool registry pub fn default_tools(security: Arc) -> Vec> { default_tools_with_runtime(security, Arc::new(NativeRuntime::new())) } /// Create the default tool registry with explicit runtime adapter. pub fn default_tools_with_runtime( security: Arc, runtime: Arc, ) -> Vec> { vec![ Box::new(ShellTool::new(security.clone(), runtime)), Box::new(FileReadTool::new(security.clone())), Box::new(FileWriteTool::new(security.clone())), Box::new(FileEditTool::new(security.clone())), Box::new(GlobSearchTool::new(security.clone())), Box::new(ContentSearchTool::new(security)), ] } /// Create full tool registry including memory tools and optional Composio #[allow(clippy::implicit_hasher, clippy::too_many_arguments)] pub fn all_tools( config: Arc, security: &Arc, memory: Arc, composio_key: Option<&str>, composio_entity_id: Option<&str>, browser_config: &crate::config::BrowserConfig, http_config: &crate::config::HttpRequestConfig, workspace_dir: &std::path::Path, agents: &HashMap, fallback_api_key: Option<&str>, root_config: &crate::config::Config, ) -> Vec> { all_tools_with_runtime( config, security, Arc::new(NativeRuntime::new()), memory, composio_key, composio_entity_id, browser_config, http_config, workspace_dir, agents, fallback_api_key, root_config, ) } /// Create full tool registry including memory tools and optional Composio. #[allow(clippy::implicit_hasher, clippy::too_many_arguments)] pub fn all_tools_with_runtime( config: Arc, security: &Arc, runtime: Arc, memory: Arc, composio_key: Option<&str>, composio_entity_id: Option<&str>, browser_config: &crate::config::BrowserConfig, http_config: &crate::config::HttpRequestConfig, workspace_dir: &std::path::Path, agents: &HashMap, fallback_api_key: Option<&str>, root_config: &crate::config::Config, ) -> Vec> { let mut tool_arcs: Vec> = vec![ Arc::new(ShellTool::new(security.clone(), runtime)), Arc::new(FileReadTool::new(security.clone())), Arc::new(FileWriteTool::new(security.clone())), Arc::new(FileEditTool::new(security.clone())), Arc::new(GlobSearchTool::new(security.clone())), Arc::new(ContentSearchTool::new(security.clone())), Arc::new(CronAddTool::new(config.clone(), security.clone())), Arc::new(CronListTool::new(config.clone())), Arc::new(CronRemoveTool::new(config.clone(), security.clone())), Arc::new(CronUpdateTool::new(config.clone(), security.clone())), Arc::new(CronRunTool::new(config.clone(), security.clone())), Arc::new(CronRunsTool::new(config.clone())), Arc::new(MemoryStoreTool::new(memory.clone(), security.clone())), Arc::new(MemoryRecallTool::new(memory.clone())), Arc::new(MemoryForgetTool::new(memory, security.clone())), Arc::new(ScheduleTool::new(security.clone(), root_config.clone())), Arc::new(ModelRoutingConfigTool::new( config.clone(), security.clone(), )), Arc::new(ProxyConfigTool::new(config.clone(), security.clone())), Arc::new(GitOperationsTool::new( security.clone(), workspace_dir.to_path_buf(), )), Arc::new(PushoverTool::new( security.clone(), workspace_dir.to_path_buf(), )), ]; if browser_config.enabled { // Add legacy browser_open tool for simple URL opening tool_arcs.push(Arc::new(BrowserOpenTool::new( security.clone(), browser_config.allowed_domains.clone(), ))); // Add full browser automation tool (pluggable backend) tool_arcs.push(Arc::new(BrowserTool::new_with_backend( security.clone(), browser_config.allowed_domains.clone(), browser_config.session_name.clone(), browser_config.backend.clone(), browser_config.native_headless, browser_config.native_webdriver_url.clone(), browser_config.native_chrome_path.clone(), ComputerUseConfig { endpoint: browser_config.computer_use.endpoint.clone(), api_key: browser_config.computer_use.api_key.clone(), timeout_ms: browser_config.computer_use.timeout_ms, allow_remote_endpoint: browser_config.computer_use.allow_remote_endpoint, window_allowlist: browser_config.computer_use.window_allowlist.clone(), max_coordinate_x: browser_config.computer_use.max_coordinate_x, max_coordinate_y: browser_config.computer_use.max_coordinate_y, }, ))); } if http_config.enabled { tool_arcs.push(Arc::new(HttpRequestTool::new( security.clone(), http_config.allowed_domains.clone(), http_config.max_response_size, http_config.timeout_secs, ))); } // Web search tool (enabled by default for GLM and other models) if root_config.web_search.enabled { tool_arcs.push(Arc::new(WebSearchTool::new( root_config.web_search.provider.clone(), root_config.web_search.brave_api_key.clone(), root_config.web_search.max_results, root_config.web_search.timeout_secs, ))); } // PDF extraction (feature-gated at compile time via rag-pdf) tool_arcs.push(Arc::new(PdfReadTool::new(security.clone()))); // Vision tools are always available tool_arcs.push(Arc::new(ScreenshotTool::new(security.clone()))); tool_arcs.push(Arc::new(ImageInfoTool::new(security.clone()))); if let Some(key) = composio_key { if !key.is_empty() { tool_arcs.push(Arc::new(ComposioTool::new( key, composio_entity_id, security.clone(), ))); } } // Add delegation tool when agents are configured if !agents.is_empty() { let delegate_agents: HashMap = agents .iter() .map(|(name, cfg)| (name.clone(), cfg.clone())) .collect(); let delegate_fallback_credential = fallback_api_key.and_then(|value| { let trimmed_value = value.trim(); (!trimmed_value.is_empty()).then(|| trimmed_value.to_owned()) }); let parent_tools = Arc::new(tool_arcs.clone()); let delegate_tool = DelegateTool::new_with_options( delegate_agents, delegate_fallback_credential, security.clone(), crate::providers::ProviderRuntimeOptions { auth_profile_override: None, zeroclaw_dir: root_config .config_path .parent() .map(std::path::PathBuf::from), secrets_encrypt: root_config.secrets.encrypt, reasoning_enabled: root_config.runtime.reasoning_enabled, }, ) .with_parent_tools(parent_tools) .with_multimodal_config(root_config.multimodal.clone()); tool_arcs.push(Arc::new(delegate_tool)); } boxed_registry_from_arcs(tool_arcs) } #[cfg(test)] mod tests { use super::*; use crate::config::{BrowserConfig, Config, MemoryConfig}; use tempfile::TempDir; fn test_config(tmp: &TempDir) -> Config { Config { workspace_dir: tmp.path().join("workspace"), config_path: tmp.path().join("config.toml"), ..Config::default() } } #[test] fn default_tools_has_expected_count() { let security = Arc::new(SecurityPolicy::default()); let tools = default_tools(security); assert_eq!(tools.len(), 6); } #[test] fn all_tools_excludes_browser_when_disabled() { let tmp = TempDir::new().unwrap(); let security = Arc::new(SecurityPolicy::default()); let mem_cfg = MemoryConfig { backend: "markdown".into(), ..MemoryConfig::default() }; let mem: Arc = Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap()); let browser = BrowserConfig { enabled: false, allowed_domains: vec!["example.com".into()], session_name: None, ..BrowserConfig::default() }; let http = crate::config::HttpRequestConfig::default(); let cfg = test_config(&tmp); let tools = all_tools( Arc::new(Config::default()), &security, mem, None, None, &browser, &http, tmp.path(), &HashMap::new(), None, &cfg, ); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(!names.contains(&"browser_open")); assert!(names.contains(&"schedule")); assert!(names.contains(&"model_routing_config")); assert!(names.contains(&"pushover")); assert!(names.contains(&"proxy_config")); } #[test] fn all_tools_includes_browser_when_enabled() { let tmp = TempDir::new().unwrap(); let security = Arc::new(SecurityPolicy::default()); let mem_cfg = MemoryConfig { backend: "markdown".into(), ..MemoryConfig::default() }; let mem: Arc = Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap()); let browser = BrowserConfig { enabled: true, allowed_domains: vec!["example.com".into()], session_name: None, ..BrowserConfig::default() }; let http = crate::config::HttpRequestConfig::default(); let cfg = test_config(&tmp); let tools = all_tools( Arc::new(Config::default()), &security, mem, None, None, &browser, &http, tmp.path(), &HashMap::new(), None, &cfg, ); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(names.contains(&"browser_open")); assert!(names.contains(&"content_search")); assert!(names.contains(&"model_routing_config")); assert!(names.contains(&"pushover")); assert!(names.contains(&"proxy_config")); } #[test] fn default_tools_names() { let security = Arc::new(SecurityPolicy::default()); let tools = default_tools(security); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(names.contains(&"shell")); assert!(names.contains(&"file_read")); assert!(names.contains(&"file_write")); assert!(names.contains(&"file_edit")); assert!(names.contains(&"glob_search")); assert!(names.contains(&"content_search")); } #[test] fn default_tools_all_have_descriptions() { let security = Arc::new(SecurityPolicy::default()); let tools = default_tools(security); for tool in &tools { assert!( !tool.description().is_empty(), "Tool {} has empty description", tool.name() ); } } #[test] fn default_tools_all_have_schemas() { let security = Arc::new(SecurityPolicy::default()); let tools = default_tools(security); for tool in &tools { let schema = tool.parameters_schema(); assert!( schema.is_object(), "Tool {} schema is not an object", tool.name() ); assert!( schema["properties"].is_object(), "Tool {} schema has no properties", tool.name() ); } } #[test] fn tool_spec_generation() { let security = Arc::new(SecurityPolicy::default()); let tools = default_tools(security); for tool in &tools { let spec = tool.spec(); assert_eq!(spec.name, tool.name()); assert_eq!(spec.description, tool.description()); assert!(spec.parameters.is_object()); } } #[test] fn tool_result_serde() { let result = ToolResult { success: true, output: "hello".into(), error: None, }; let json = serde_json::to_string(&result).unwrap(); let parsed: ToolResult = serde_json::from_str(&json).unwrap(); assert!(parsed.success); assert_eq!(parsed.output, "hello"); assert!(parsed.error.is_none()); } #[test] fn tool_result_with_error_serde() { let result = ToolResult { success: false, output: String::new(), error: Some("boom".into()), }; let json = serde_json::to_string(&result).unwrap(); let parsed: ToolResult = serde_json::from_str(&json).unwrap(); assert!(!parsed.success); assert_eq!(parsed.error.as_deref(), Some("boom")); } #[test] fn tool_spec_serde() { let spec = ToolSpec { name: "test".into(), description: "A test tool".into(), parameters: serde_json::json!({"type": "object"}), }; let json = serde_json::to_string(&spec).unwrap(); let parsed: ToolSpec = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.name, "test"); assert_eq!(parsed.description, "A test tool"); } #[test] fn all_tools_includes_delegate_when_agents_configured() { let tmp = TempDir::new().unwrap(); let security = Arc::new(SecurityPolicy::default()); let mem_cfg = MemoryConfig { backend: "markdown".into(), ..MemoryConfig::default() }; let mem: Arc = Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap()); let browser = BrowserConfig::default(); let http = crate::config::HttpRequestConfig::default(); let cfg = test_config(&tmp); let mut agents = HashMap::new(); agents.insert( "researcher".to_string(), DelegateAgentConfig { provider: "ollama".to_string(), model: "llama3".to_string(), system_prompt: None, api_key: None, temperature: None, max_depth: 3, agentic: false, allowed_tools: Vec::new(), max_iterations: 10, }, ); let tools = all_tools( Arc::new(Config::default()), &security, mem, None, None, &browser, &http, tmp.path(), &agents, Some("delegate-test-credential"), &cfg, ); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(names.contains(&"delegate")); } #[test] fn all_tools_excludes_delegate_when_no_agents() { let tmp = TempDir::new().unwrap(); let security = Arc::new(SecurityPolicy::default()); let mem_cfg = MemoryConfig { backend: "markdown".into(), ..MemoryConfig::default() }; let mem: Arc = Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap()); let browser = BrowserConfig::default(); let http = crate::config::HttpRequestConfig::default(); let cfg = test_config(&tmp); let tools = all_tools( Arc::new(Config::default()), &security, mem, None, None, &browser, &http, tmp.path(), &HashMap::new(), None, &cfg, ); let names: Vec<&str> = tools.iter().map(|t| t.name()).collect(); assert!(!names.contains(&"delegate")); } }