use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
pub mod analyzer;
pub mod browser;
pub mod cargo;
pub mod clarify;
pub mod code_metrics;
pub mod codemap;
pub mod computer;
pub mod container;
pub mod context;
pub mod file;
pub mod fim;
pub mod git;
pub mod git_worktree;
pub mod grep_search;
#[cfg(feature = "hot-reload")]
pub mod hot_reload;
pub mod http;
pub mod introspect;
pub mod knowledge;
pub mod localize_issue;
pub mod lsp_tools;
pub mod net_policy;
pub mod package;
pub mod page_controller;
pub mod patch_apply;
pub mod process;
pub mod prompt;
pub mod pty_shell;
pub mod screen_capture;
pub mod search;
pub mod shell_exec;
pub mod task_focus;
pub mod tool_search;
pub mod vision;
use browser::{BrowserEval, BrowserFetch, BrowserLinks, BrowserPdf, BrowserScreenshot};
use cargo::{CargoCheck, CargoClippy, CargoFmt, CargoTest};
use container::{
ComposeDown, ComposeUp, ContainerBuild, ContainerExec, ContainerImages, ContainerList,
ContainerLogs, ContainerPull, ContainerRemove, ContainerRun, ContainerStop,
};
use file::{DirectoryTree, FileDelete, FileEdit, FileMultiEdit, FileRead, FileWrite};
use git::{GitCheckpoint, GitCommit, GitDiff, GitPush, GitStatus};
use git_worktree::{EnterWorktreeTool, ExitWorktreeTool, ListWorktreesTool};
use grep_search::GrepSearch;
use http::HttpRequest;
use knowledge::{
KnowledgeAdd, KnowledgeAutoExtract, KnowledgeClear, KnowledgeExport, KnowledgeQuery,
KnowledgeRelate, KnowledgeRemove, KnowledgeStats as KnowledgeStatsTool,
};
use localize_issue::LocalizeIssue;
use package::{NpmInstall, NpmRun, NpmScripts, PipFreeze, PipInstall, PipList, YarnInstall};
use page_controller::PageControlTool;
use patch_apply::PatchApply;
use process::{PortCheck, ProcessList, ProcessLogs, ProcessRestart, ProcessStart, ProcessStop};
use pty_shell::PtyShellTool;
use screen_capture::ScreenCapture;
use search::{GlobFind, SymbolSearch};
use shell_exec::ShellExec;
use vision::{VisionAnalyze, VisionCompare};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaginationInfo {
pub offset: usize,
pub limit: usize,
pub total_chars: usize,
pub has_more: bool,
}
pub fn truncate_with_pagination(
output: &str,
offset: usize,
limit: usize,
) -> (String, PaginationInfo) {
let total_chars = output.chars().count();
let page: String = output.chars().skip(offset).take(limit).collect();
let consumed = offset + page.chars().count();
let info = PaginationInfo {
offset,
limit,
total_chars,
has_more: consumed < total_chars,
};
(page, info)
}
pub(crate) const DANGEROUS_SHELL_PATTERNS: &[&str] = &[
"/dev/tcp/",
"/dev/udp/",
"| bash -i",
"| sh -i",
"| bash",
"| sh",
"bash -i",
"sh -i",
"exec bash -i",
"exec sh -i",
"mkfifo /tmp",
"rm -rf /",
"rm -rf ~",
"rm -rf *",
":(){ :|:& };:",
"> /dev/sda",
"dd if=/dev/zero of=/dev/sda",
"chmod -R 777 /",
"chown -R 0:0 /",
];
pub(crate) fn find_dangerous_shell_pattern(command: &str) -> Option<&'static str> {
let normalized = command
.to_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
DANGEROUS_SHELL_PATTERNS
.iter()
.find(|pattern| {
if pattern.contains("sh") {
pattern_matches_sh_word(&normalized, pattern)
} else {
normalized.contains(**pattern)
}
})
.copied()
}
fn pattern_matches_sh_word(normalized: &str, pattern: &str) -> bool {
let (sh_offset, sh_len) = pattern
.find("bash")
.map(|p| (p, 4))
.or_else(|| pattern.find("sh").map(|p| (p, 2)))
.expect("sh/bash pattern must contain 'sh' or 'bash'");
let mut search_from = 0;
while let Some(pos) = normalized[search_from..].find(pattern) {
let abs_pos = search_from + pos;
let after_sh = abs_pos + sh_offset + sh_len;
let is_boundary = after_sh >= normalized.len()
|| normalized[after_sh..]
.starts_with(|c: char| c.is_whitespace() || ";&|<>()".contains(c));
if is_boundary {
return true;
}
search_from = abs_pos + 1;
}
false
}
pub(crate) fn truncate_output(output: &str, max_len: usize) -> String {
if output.len() <= max_len {
return output.to_string();
}
let mut end = max_len;
while end > 0 && !output.is_char_boundary(end) {
end -= 1;
}
format!(
"{}... [truncated, {} total chars]",
&output[..end],
output.len()
)
}
pub fn validate_tool_arguments_schema(tool_name: &str, schema: &Value, args: &Value) -> Result<()> {
if schema.get("type").and_then(|v| v.as_str()) == Some("object") && !args.is_object() {
anyhow::bail!(
"Schema validation failed for tool '{}': expected JSON object arguments",
tool_name
);
}
let Some(required) = schema.get("required").and_then(|v| v.as_array()) else {
return Ok(());
};
let Some(args_obj) = args.as_object() else {
return Ok(());
};
let missing: Vec<&str> = required
.iter()
.filter_map(|value| value.as_str())
.filter(|field| args_obj.get(*field).is_none_or(|value| value.is_null()))
.collect();
if missing.is_empty() {
Ok(())
} else {
anyhow::bail!(
"Schema validation failed for tool '{}': missing required field(s): {}",
tool_name,
missing.join(", ")
);
}
}
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn schema(&self) -> Value;
async fn execute(&self, args: Value) -> Result<Value>;
fn is_readonly(&self) -> bool {
self.metadata().read_only
}
fn is_destructive(&self) -> bool {
self.metadata().destructive
}
fn risk_level(&self) -> crate::safety::RiskLevel {
self.metadata().risk_level
}
fn metadata(&self) -> crate::safety::ToolMetadata {
crate::safety::default_tool_metadata(self.name())
}
}
#[async_trait]
impl Tool for Arc<dyn Tool> {
fn name(&self) -> &str {
(**self).name()
}
fn description(&self) -> &str {
(**self).description()
}
fn schema(&self) -> Value {
(**self).schema()
}
async fn execute(&self, args: Value) -> Result<Value> {
(**self).execute(args).await
}
}
#[derive(Clone)]
pub struct ToolInfo {
pub tool: Arc<dyn Tool>,
pub is_critical: bool,
pub category: String,
}
pub struct ToolRegistry {
all_tools: HashMap<String, ToolInfo>,
activated_tools: HashSet<String>,
tool_search_index: Arc<std::sync::RwLock<Vec<tool_search::ToolSearchResult>>>,
}
pub const CRITICAL_TOOLS: &[&str] = &[
"file_read",
"file_write",
"file_edit",
"file_multi_edit",
"file_delete",
"directory_tree",
"shell_exec",
"grep_search",
"glob_find",
"tool_search",
];
impl ToolRegistry {
pub fn new() -> Self {
Self::with_safety_config(None)
}
pub fn with_safety_config(safety_config: Option<&crate::config::SafetyConfig>) -> Self {
let tool_search_index = Arc::new(std::sync::RwLock::new(Vec::new()));
let mut registry = Self {
all_tools: HashMap::new(),
activated_tools: HashSet::new(),
tool_search_index: Arc::clone(&tool_search_index),
};
registry.register_critical(tool_search::ToolSearchTool::new(tool_search_index));
if let Some(cfg) = safety_config {
registry.register_critical(FileRead::with_safety_config(cfg.clone()));
registry.register_critical(FileWrite::with_safety_config(cfg.clone()));
registry.register_critical(FileEdit::with_safety_config(cfg.clone()));
registry.register_critical(FileMultiEdit::with_safety_config(cfg.clone()));
registry.register_critical(FileDelete::with_safety_config(cfg.clone()));
registry.register_critical(DirectoryTree::with_safety_config(cfg.clone()));
} else {
registry.register_critical(FileRead::new());
registry.register_critical(FileWrite::new());
registry.register_critical(FileEdit::new());
registry.register_critical(FileMultiEdit::new());
registry.register_critical(FileDelete::new());
registry.register_critical(DirectoryTree::new());
}
registry.register_critical(ShellExec);
registry.register_critical(GrepSearch);
registry.register_critical(GlobFind);
if let Some(cfg) = safety_config {
registry.register_deferred(GitStatus::with_safety_config(cfg.clone()));
registry.register_deferred(GitDiff::with_safety_config(cfg.clone()));
registry.register_deferred(GitCommit::with_safety_config(cfg.clone()));
registry.register_deferred(GitPush::with_safety_config(cfg.clone()));
registry.register_deferred(GitCheckpoint::with_safety_config(cfg.clone()));
registry.register_deferred(EnterWorktreeTool::with_safety_config(cfg.clone()));
registry.register_deferred(ExitWorktreeTool::with_safety_config(cfg.clone()));
registry.register_deferred(ListWorktreesTool::with_safety_config(cfg.clone()));
} else {
registry.register_deferred(GitStatus::new());
registry.register_deferred(EnterWorktreeTool::new());
registry.register_deferred(ExitWorktreeTool::new());
registry.register_deferred(ListWorktreesTool::new());
registry.register_deferred(GitDiff::new());
registry.register_deferred(GitCommit::new());
registry.register_deferred(GitPush::new());
registry.register_deferred(GitCheckpoint::new());
}
registry.register_deferred(CargoTest);
registry.register_deferred(CargoCheck);
registry.register_deferred(CargoClippy);
registry.register_deferred(CargoFmt);
registry.register_deferred(PtyShellTool);
registry.register_deferred(SymbolSearch);
registry.register_deferred(HttpRequest);
registry.register_deferred(ProcessStart);
registry.register_deferred(ProcessStop);
registry.register_deferred(ProcessList);
registry.register_deferred(ProcessLogs);
registry.register_deferred(ProcessRestart);
registry.register_deferred(PortCheck);
registry.register_deferred(NpmInstall);
registry.register_deferred(NpmRun);
registry.register_deferred(NpmScripts);
registry.register_deferred(PipInstall);
registry.register_deferred(PipList);
registry.register_deferred(PipFreeze);
registry.register_deferred(YarnInstall);
registry.register_deferred(ContainerRun);
registry.register_deferred(ContainerStop);
registry.register_deferred(ContainerList);
registry.register_deferred(ContainerLogs);
registry.register_deferred(ContainerExec);
registry.register_deferred(ContainerBuild);
registry.register_deferred(ContainerImages);
registry.register_deferred(ContainerPull);
registry.register_deferred(ContainerRemove);
registry.register_deferred(ComposeUp);
registry.register_deferred(ComposeDown);
registry.register_deferred(ScreenCapture);
registry.register_deferred(VisionAnalyze);
registry.register_deferred(VisionCompare);
registry.register_deferred(BrowserFetch);
registry.register_deferred(BrowserScreenshot);
registry.register_deferred(BrowserPdf);
registry.register_deferred(BrowserEval);
registry.register_deferred(BrowserLinks);
registry.register_deferred(PageControlTool::new());
registry.register_deferred(KnowledgeAdd);
registry.register_deferred(KnowledgeAutoExtract);
registry.register_deferred(KnowledgeRelate);
registry.register_deferred(KnowledgeQuery);
registry.register_deferred(KnowledgeStatsTool);
registry.register_deferred(KnowledgeClear);
registry.register_deferred(KnowledgeRemove);
registry.register_deferred(KnowledgeExport);
registry.register_deferred(computer::ComputerMouseTool);
registry.register_deferred(computer::ComputerKeyboardTool);
registry.register_deferred(computer::ComputerScreenTool);
registry.register_deferred(computer::ComputerWindowTool);
registry.register_deferred(LocalizeIssue);
let project_root =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let (lsp_goto, lsp_refs, lsp_syms, lsp_hover) =
lsp_tools::create_lsp_tools(project_root.clone(), safety_config.cloned());
registry.register_deferred(lsp_goto);
registry.register_deferred(lsp_refs);
registry.register_deferred(lsp_syms);
registry.register_deferred(lsp_hover);
let (lsp_diag, lsp_ws, lsp_impl) =
lsp_tools::create_extra_lsp_tools(project_root, safety_config.cloned());
registry.register_deferred(lsp_diag);
registry.register_deferred(lsp_ws);
registry.register_deferred(lsp_impl);
registry.register_deferred(introspect::CodeIntrospect::new());
registry.register_deferred(introspect::CodeQuery::new());
registry.register_deferred(introspect::CodePlan::new());
registry.register_deferred(introspect::CodeDiffPlan::new());
registry.register_deferred(code_metrics::CodeMetricsTool::new());
registry.register_deferred(codemap::CodeMapTool);
registry.register_deferred(codemap::ContextBudgetTool);
registry.register_deferred(codemap::ContextActionTool);
registry.register_deferred(PatchApply);
#[cfg(feature = "hot-reload")]
registry.register_deferred(hot_reload::HotReloadTool::new());
registry.register_deferred(clarify::ClarificationTool::default());
registry.rebuild_search_index();
registry
}
pub fn register_critical<T: Tool + 'static>(&mut self, tool: T) {
let name = tool.name().to_string();
let category = tool_search::categorize_tool(&name).to_string();
self.all_tools.insert(
name.clone(),
ToolInfo {
tool: Arc::new(tool),
is_critical: true,
category,
},
);
self.activated_tools.insert(name);
}
pub fn register_deferred<T: Tool + 'static>(&mut self, tool: T) {
let name = tool.name().to_string();
let category = tool_search::categorize_tool(&name).to_string();
self.all_tools.insert(
name.clone(),
ToolInfo {
tool: Arc::new(tool),
is_critical: false,
category,
},
);
}
pub fn register<T: Tool + 'static>(&mut self, tool: T) {
self.register_deferred(tool);
}
pub fn rebuild_search_index(&mut self) {
let mut index = self
.tool_search_index
.write()
.expect("tool_search index poisoned");
index.clear();
index.extend(
self.all_tools
.values()
.map(|info| tool_search::ToolSearchResult {
name: info.tool.name().to_string(),
description: info.tool.description().to_string(),
schema: info.tool.schema(),
is_critical: info.is_critical,
category: info.category.clone(),
}),
);
}
pub fn get(&self, name: &str) -> Option<&dyn Tool> {
self.all_tools.get(name).map(|info| info.tool.as_ref())
}
pub fn get_activated(&self, name: &str) -> Option<&dyn Tool> {
if self.activated_tools.contains(name) {
self.all_tools.get(name).map(|info| info.tool.as_ref())
} else {
None
}
}
pub fn is_activated(&self, name: &str) -> bool {
self.activated_tools.contains(name)
}
pub fn activate(&mut self, name: &str) -> bool {
if self.all_tools.contains_key(name) {
self.activated_tools.insert(name.to_string());
true
} else {
false
}
}
pub fn list(&self) -> Vec<&dyn Tool> {
self.all_tools
.values()
.map(|info| info.tool.as_ref())
.collect()
}
pub fn list_activated(&self) -> Vec<&dyn Tool> {
self.activated_tools
.iter()
.filter_map(|name| self.all_tools.get(name).map(|info| info.tool.as_ref()))
.collect()
}
pub fn list_critical(&self) -> Vec<&dyn Tool> {
self.all_tools
.values()
.filter(|info| info.is_critical)
.map(|info| info.tool.as_ref())
.collect()
}
pub fn list_deferred(&self) -> Vec<&dyn Tool> {
self.all_tools
.values()
.filter(|info| !info.is_critical && !self.activated_tools.contains(info.tool.name()))
.map(|info| info.tool.as_ref())
.collect()
}
pub async fn execute(&self, name: &str, args: serde_json::Value) -> Result<serde_json::Value> {
let tool = self
.get_activated(name)
.ok_or_else(|| anyhow::anyhow!("Unknown or inactive tool: {}", name))?;
tool.execute(args).await
}
pub async fn execute_any(
&self,
name: &str,
args: serde_json::Value,
) -> Result<serde_json::Value> {
let tool = self
.get(name)
.ok_or_else(|| anyhow::anyhow!("Unknown tool: {}", name))?;
tool.execute(args).await
}
pub fn definitions(&self) -> Vec<crate::api::types::ToolDefinition> {
self.list_activated()
.into_iter()
.map(|tool| crate::api::types::ToolDefinition {
def_type: "function".to_string(),
function: crate::api::types::FunctionDefinition {
name: tool.name().to_string(),
description: tool.description().to_string(),
parameters: tool.schema(),
},
})
.collect()
}
pub fn critical_definitions(&self) -> Vec<crate::api::types::ToolDefinition> {
self.list_critical()
.into_iter()
.map(|tool| crate::api::types::ToolDefinition {
def_type: "function".to_string(),
function: crate::api::types::FunctionDefinition {
name: tool.name().to_string(),
description: tool.description().to_string(),
parameters: tool.schema(),
},
})
.collect()
}
pub fn search(&self, query: &str, limit: usize) -> Vec<tool_search::ToolSearchResult> {
let query_lower = query.to_lowercase();
self.all_tools
.values()
.filter(|info| {
let name_match = info.tool.name().to_lowercase().contains(&query_lower);
let desc_match = info
.tool
.description()
.to_lowercase()
.contains(&query_lower);
name_match || desc_match
})
.take(limit)
.map(|info| tool_search::ToolSearchResult {
name: info.tool.name().to_string(),
description: info.tool.description().to_string(),
schema: info.tool.schema(),
is_critical: info.is_critical,
category: info.category.clone(),
})
.collect()
}
pub fn total_count(&self) -> usize {
self.all_tools.len()
}
pub fn activated_count(&self) -> usize {
self.activated_tools.len()
}
pub fn filter_by_readonly(&self) -> Vec<&dyn Tool> {
self.list_activated()
.into_iter()
.filter(|tool| tool.is_readonly())
.collect()
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "../../tests/unit/tools/mod_test.rs"]
mod tests;