zeroclaw/src/tools/mod.rs
Chummy 85fc12bcf7
feat(browser): add optional rust-native backend via fantoccini
* feat(browser): add optional rust-native automation backend

* style: align channels module with stable rustfmt

* fix(browser): switch rust-native backend to fantoccini

Replace headless_chrome with fantoccini to satisfy license checks and keep browser-native optional. Adds native_webdriver_url wiring, migrates native backend session/actions to WebDriver, updates docs/config defaults, and keeps backend auto-resolution behavior intact.

* test(config): serialize env override tests with lock

Prevent flaky CI failures caused by concurrent environment variable mutation across config env-override tests.

* style: apply rustfmt 1.92 for CI parity

* chore(ci): sync lockfile and rustfmt with current main

Resolve feature table drift after rebasing onto latest main, refresh Cargo.lock for browser-native fantoccini, and apply rustfmt 1.92 formatting required by CI.
2026-02-16 05:25:27 -05:00

362 lines
12 KiB
Rust

pub mod browser;
pub mod browser_open;
pub mod composio;
pub mod delegate;
pub mod file_read;
pub mod file_write;
pub mod http_request;
pub mod image_info;
pub mod memory_forget;
pub mod memory_recall;
pub mod memory_store;
pub mod screenshot;
pub mod shell;
pub mod traits;
pub use browser::BrowserTool;
pub use browser_open::BrowserOpenTool;
pub use composio::ComposioTool;
pub use delegate::DelegateTool;
pub use file_read::FileReadTool;
pub use file_write::FileWriteTool;
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 screenshot::ScreenshotTool;
pub use shell::ShellTool;
pub use traits::Tool;
#[allow(unused_imports)]
pub use traits::{ToolResult, ToolSpec};
use crate::config::DelegateAgentConfig;
use crate::memory::Memory;
use crate::runtime::{NativeRuntime, RuntimeAdapter};
use crate::security::SecurityPolicy;
use std::collections::HashMap;
use std::sync::Arc;
/// Create the default tool registry
pub fn default_tools(security: Arc<SecurityPolicy>) -> Vec<Box<dyn Tool>> {
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<SecurityPolicy>,
runtime: Arc<dyn RuntimeAdapter>,
) -> Vec<Box<dyn Tool>> {
vec![
Box::new(ShellTool::new(security.clone(), runtime)),
Box::new(FileReadTool::new(security.clone())),
Box::new(FileWriteTool::new(security)),
]
}
/// Create full tool registry including memory tools and optional Composio
#[allow(clippy::implicit_hasher)]
pub fn all_tools(
security: &Arc<SecurityPolicy>,
memory: Arc<dyn Memory>,
composio_key: Option<&str>,
browser_config: &crate::config::BrowserConfig,
http_config: &crate::config::HttpRequestConfig,
agents: &HashMap<String, DelegateAgentConfig>,
fallback_api_key: Option<&str>,
) -> Vec<Box<dyn Tool>> {
all_tools_with_runtime(
security,
Arc::new(NativeRuntime::new()),
memory,
composio_key,
browser_config,
http_config,
agents,
fallback_api_key,
)
}
/// Create full tool registry including memory tools and optional Composio.
#[allow(clippy::implicit_hasher)]
pub fn all_tools_with_runtime(
security: &Arc<SecurityPolicy>,
runtime: Arc<dyn RuntimeAdapter>,
memory: Arc<dyn Memory>,
composio_key: Option<&str>,
browser_config: &crate::config::BrowserConfig,
http_config: &crate::config::HttpRequestConfig,
agents: &HashMap<String, DelegateAgentConfig>,
fallback_api_key: Option<&str>,
) -> Vec<Box<dyn Tool>> {
let mut tools: Vec<Box<dyn Tool>> = vec![
Box::new(ShellTool::new(security.clone(), runtime)),
Box::new(FileReadTool::new(security.clone())),
Box::new(FileWriteTool::new(security.clone())),
Box::new(MemoryStoreTool::new(memory.clone())),
Box::new(MemoryRecallTool::new(memory.clone())),
Box::new(MemoryForgetTool::new(memory)),
];
if browser_config.enabled {
// Add legacy browser_open tool for simple URL opening
tools.push(Box::new(BrowserOpenTool::new(
security.clone(),
browser_config.allowed_domains.clone(),
)));
// Add full browser automation tool (pluggable backend)
tools.push(Box::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(),
)));
}
if http_config.enabled {
tools.push(Box::new(HttpRequestTool::new(
security.clone(),
http_config.allowed_domains.clone(),
http_config.max_response_size,
http_config.timeout_secs,
)));
}
// Vision tools are always available
tools.push(Box::new(ScreenshotTool::new(security.clone())));
tools.push(Box::new(ImageInfoTool::new(security.clone())));
if let Some(key) = composio_key {
if !key.is_empty() {
tools.push(Box::new(ComposioTool::new(key)));
}
}
// Add delegation tool when agents are configured
if !agents.is_empty() {
tools.push(Box::new(DelegateTool::new(
agents.clone(),
fallback_api_key.map(String::from),
)));
}
tools
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{BrowserConfig, MemoryConfig};
use tempfile::TempDir;
#[test]
fn default_tools_has_three() {
let security = Arc::new(SecurityPolicy::default());
let tools = default_tools(security);
assert_eq!(tools.len(), 3);
}
#[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<dyn Memory> =
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 tools = all_tools(&security, mem, None, &browser, &http, &HashMap::new(), None);
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(!names.contains(&"browser_open"));
}
#[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<dyn Memory> =
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 tools = all_tools(&security, mem, None, &browser, &http, &HashMap::new(), None);
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(names.contains(&"browser_open"));
}
#[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"));
}
#[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<dyn Memory> =
Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
let browser = BrowserConfig::default();
let http = crate::config::HttpRequestConfig::default();
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,
},
);
let tools = all_tools(
&security,
mem,
None,
&browser,
&http,
&agents,
Some("sk-test"),
);
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<dyn Memory> =
Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
let browser = BrowserConfig::default();
let http = crate::config::HttpRequestConfig::default();
let tools = all_tools(&security, mem, None, &browser, &http, &HashMap::new(), None);
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(!names.contains(&"delegate"));
}
}