vtcode_core/tools/registry/
mod.rs

1//! Tool registry and function declarations
2
3mod astgrep;
4mod builtins;
5mod cache;
6mod declarations;
7mod error;
8mod executors;
9mod legacy;
10mod policy;
11mod pty;
12mod registration;
13mod utils;
14
15pub use declarations::{
16    build_function_declarations, build_function_declarations_for_level,
17    build_function_declarations_with_mode,
18};
19pub use error::{ToolErrorType, ToolExecutionError, classify_error};
20pub use registration::{ToolExecutorFn, ToolHandler, ToolRegistration};
21
22use builtins::register_builtin_tools;
23use utils::normalize_tool_output;
24
25use crate::config::PtyConfig;
26use crate::config::ToolsConfig;
27use crate::config::constants::tools;
28use crate::tool_policy::{ToolPolicy, ToolPolicyManager};
29use crate::tools::ast_grep::AstGrepEngine;
30use crate::tools::grep_search::GrepSearchManager;
31use anyhow::{Result, anyhow};
32use serde_json::Value;
33use std::collections::{HashMap, HashSet};
34use std::path::PathBuf;
35use std::sync::Arc;
36use std::sync::atomic::AtomicUsize;
37use tracing::{debug, warn};
38
39use super::bash_tool::BashTool;
40use super::command::CommandTool;
41use super::curl_tool::CurlTool;
42use super::file_ops::FileOpsTool;
43use super::plan::PlanManager;
44use super::search::SearchTool;
45use super::simple_search::SimpleSearchTool;
46use super::srgn::SrgnTool;
47use crate::mcp_client::{McpClient, McpToolExecutor, McpToolInfo};
48
49#[cfg(test)]
50use super::traits::Tool;
51#[cfg(test)]
52use crate::config::types::CapabilityLevel;
53
54#[derive(Clone)]
55pub struct ToolRegistry {
56    workspace_root: PathBuf,
57    search_tool: SearchTool,
58    simple_search_tool: SimpleSearchTool,
59    bash_tool: BashTool,
60    file_ops_tool: FileOpsTool,
61    command_tool: CommandTool,
62    curl_tool: CurlTool,
63    grep_search: Arc<GrepSearchManager>,
64    ast_grep_engine: Option<Arc<AstGrepEngine>>,
65    tool_policy: Option<ToolPolicyManager>,
66    pty_config: PtyConfig,
67    active_pty_sessions: Arc<AtomicUsize>,
68    srgn_tool: SrgnTool,
69    plan_manager: PlanManager,
70    mcp_client: Option<Arc<McpClient>>,
71    mcp_tool_index: HashMap<String, Vec<String>>,
72    tool_registrations: Vec<ToolRegistration>,
73    tool_lookup: HashMap<&'static str, usize>,
74    preapproved_tools: HashSet<String>,
75    full_auto_allowlist: Option<HashSet<String>>,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ToolPermissionDecision {
80    Allow,
81    Deny,
82    Prompt,
83}
84
85impl ToolRegistry {
86    pub fn new(workspace_root: PathBuf) -> Self {
87        Self::build(workspace_root, PtyConfig::default(), true)
88    }
89
90    pub fn new_with_config(workspace_root: PathBuf, pty_config: PtyConfig) -> Self {
91        Self::build(workspace_root, pty_config, true)
92    }
93
94    pub fn new_with_features(workspace_root: PathBuf, todo_planning_enabled: bool) -> Self {
95        Self::build(workspace_root, PtyConfig::default(), todo_planning_enabled)
96    }
97
98    pub fn new_with_config_and_features(
99        workspace_root: PathBuf,
100        pty_config: PtyConfig,
101        todo_planning_enabled: bool,
102    ) -> Self {
103        Self::build(workspace_root, pty_config, todo_planning_enabled)
104    }
105
106    fn build(workspace_root: PathBuf, pty_config: PtyConfig, todo_planning_enabled: bool) -> Self {
107        let grep_search = Arc::new(GrepSearchManager::new(workspace_root.clone()));
108
109        let search_tool = SearchTool::new(workspace_root.clone(), grep_search.clone());
110        let simple_search_tool = SimpleSearchTool::new(workspace_root.clone());
111        let bash_tool = BashTool::new(workspace_root.clone());
112        let file_ops_tool = FileOpsTool::new(workspace_root.clone(), grep_search.clone());
113        let command_tool = CommandTool::new(workspace_root.clone());
114        let curl_tool = CurlTool::new();
115        let srgn_tool = SrgnTool::new(workspace_root.clone());
116        let plan_manager = PlanManager::new();
117
118        let ast_grep_engine = match AstGrepEngine::new() {
119            Ok(engine) => Some(Arc::new(engine)),
120            Err(err) => {
121                eprintln!("Warning: Failed to initialize AST-grep engine: {}", err);
122                None
123            }
124        };
125
126        let policy_manager = match ToolPolicyManager::new_with_workspace(&workspace_root) {
127            Ok(manager) => Some(manager),
128            Err(err) => {
129                eprintln!("Warning: Failed to initialize tool policy manager: {}", err);
130                None
131            }
132        };
133
134        let mut registry = Self {
135            workspace_root,
136            search_tool,
137            simple_search_tool,
138            bash_tool,
139            file_ops_tool,
140            command_tool,
141            curl_tool,
142            grep_search,
143            ast_grep_engine,
144            tool_policy: policy_manager,
145            pty_config,
146            active_pty_sessions: Arc::new(AtomicUsize::new(0)),
147            srgn_tool,
148            plan_manager,
149            mcp_client: None,
150            mcp_tool_index: HashMap::new(),
151            tool_registrations: Vec::new(),
152            tool_lookup: HashMap::new(),
153            preapproved_tools: HashSet::new(),
154            full_auto_allowlist: None,
155        };
156
157        register_builtin_tools(&mut registry, todo_planning_enabled);
158        registry
159    }
160
161    pub fn register_tool(&mut self, registration: ToolRegistration) -> Result<()> {
162        if self.tool_lookup.contains_key(registration.name()) {
163            return Err(anyhow!(format!(
164                "Tool '{}' is already registered",
165                registration.name()
166            )));
167        }
168
169        let index = self.tool_registrations.len();
170        self.tool_lookup.insert(registration.name(), index);
171        self.tool_registrations.push(registration);
172        Ok(())
173    }
174
175    pub fn available_tools(&self) -> Vec<String> {
176        self.tool_registrations
177            .iter()
178            .map(|registration| registration.name().to_string())
179            .collect()
180    }
181
182    fn mcp_policy_keys(&self) -> Vec<String> {
183        let mut keys = Vec::new();
184        for (provider, tools) in &self.mcp_tool_index {
185            for tool in tools {
186                keys.push(format!("mcp::{}::{}", provider, tool));
187            }
188        }
189        keys
190    }
191
192    fn find_mcp_provider(&self, tool_name: &str) -> Option<String> {
193        for (provider, tools) in &self.mcp_tool_index {
194            if tools.iter().any(|candidate| candidate == tool_name) {
195                return Some(provider.clone());
196            }
197        }
198        None
199    }
200
201    pub fn enable_full_auto_mode(&mut self, allowed_tools: &[String]) {
202        let mut normalized: HashSet<String> = HashSet::new();
203        if allowed_tools
204            .iter()
205            .any(|tool| tool.trim() == tools::WILDCARD_ALL)
206        {
207            for tool in self.available_tools() {
208                normalized.insert(tool);
209            }
210        } else {
211            for tool in allowed_tools {
212                let trimmed = tool.trim();
213                if !trimmed.is_empty() {
214                    normalized.insert(trimmed.to_string());
215                }
216            }
217        }
218
219        self.full_auto_allowlist = Some(normalized);
220    }
221
222    pub fn current_full_auto_allowlist(&self) -> Option<Vec<String>> {
223        self.full_auto_allowlist.as_ref().map(|set| {
224            let mut items: Vec<String> = set.iter().cloned().collect();
225            items.sort();
226            items
227        })
228    }
229
230    pub fn has_tool(&self, name: &str) -> bool {
231        self.tool_lookup.contains_key(name)
232    }
233
234    pub fn with_ast_grep(mut self, engine: Arc<AstGrepEngine>) -> Self {
235        self.ast_grep_engine = Some(engine);
236        self
237    }
238
239    pub fn workspace_root(&self) -> &PathBuf {
240        &self.workspace_root
241    }
242
243    pub fn plan_manager(&self) -> PlanManager {
244        self.plan_manager.clone()
245    }
246
247    pub fn current_plan(&self) -> crate::tools::TaskPlan {
248        self.plan_manager.snapshot()
249    }
250
251    pub async fn initialize_async(&mut self) -> Result<()> {
252        Ok(())
253    }
254
255    pub fn apply_config_policies(&mut self, tools_config: &ToolsConfig) -> Result<()> {
256        if let Ok(policy_manager) = self.policy_manager_mut() {
257            policy_manager.apply_tools_config(tools_config)?;
258        }
259
260        Ok(())
261    }
262
263    pub async fn execute_tool(&mut self, name: &str, args: Value) -> Result<Value> {
264        if let Some(allowlist) = &self.full_auto_allowlist
265            && !allowlist.contains(name)
266        {
267            let error = ToolExecutionError::new(
268                name.to_string(),
269                ToolErrorType::PolicyViolation,
270                format!(
271                    "Tool '{}' is not permitted while full-auto mode is active",
272                    name
273                ),
274            );
275            return Ok(error.to_json_value());
276        }
277
278        let skip_policy_prompt = self.preapproved_tools.remove(name);
279
280        if !skip_policy_prompt
281            && let Ok(policy_manager) = self.policy_manager_mut()
282            && !policy_manager.should_execute_tool(name)?
283        {
284            let error = ToolExecutionError::new(
285                name.to_string(),
286                ToolErrorType::PolicyViolation,
287                format!("Tool '{}' execution denied by policy", name),
288            );
289            return Ok(error.to_json_value());
290        }
291
292        let args = match self.apply_policy_constraints(name, args) {
293            Ok(args) => args,
294            Err(err) => {
295                let error = ToolExecutionError::with_original_error(
296                    name.to_string(),
297                    ToolErrorType::InvalidParameters,
298                    "Failed to apply policy constraints".to_string(),
299                    err.to_string(),
300                );
301                return Ok(error.to_json_value());
302            }
303        };
304
305        let registration = match self
306            .tool_lookup
307            .get(name)
308            .and_then(|index| self.tool_registrations.get(*index))
309        {
310            Some(registration) => registration,
311            None => {
312                // If not found in standard registry, check if it's an MCP tool
313                if let Some(mcp_client) = &self.mcp_client {
314                    // Check if it's an MCP tool (prefixed with "mcp_")
315                    if name.starts_with("mcp_") {
316                        let actual_tool_name = &name[4..]; // Remove "mcp_" prefix
317                        match mcp_client.has_mcp_tool(actual_tool_name).await {
318                            Ok(true) => {
319                                debug!(
320                                    "MCP tool '{}' found, executing via MCP client",
321                                    actual_tool_name
322                                );
323                                return self.execute_mcp_tool(actual_tool_name, args).await;
324                            }
325                            Ok(false) => {
326                                if let Some(resolved_name) =
327                                    self.resolve_mcp_tool_alias(actual_tool_name).await
328                                {
329                                    if resolved_name != actual_tool_name {
330                                        debug!(
331                                            "Resolved MCP tool alias '{}' to '{}'",
332                                            actual_tool_name, resolved_name
333                                        );
334                                        return self.execute_mcp_tool(&resolved_name, args).await;
335                                    }
336                                }
337
338                                // MCP client doesn't have this tool either
339                                let error = ToolExecutionError::new(
340                                    name.to_string(),
341                                    ToolErrorType::ToolNotFound,
342                                    format!("Unknown MCP tool: {}", actual_tool_name),
343                                );
344                                return Ok(error.to_json_value());
345                            }
346                            Err(e) => {
347                                warn!(
348                                    "Error checking MCP tool availability for '{}': {}",
349                                    actual_tool_name, e
350                                );
351                                let error = ToolExecutionError::with_original_error(
352                                    name.to_string(),
353                                    ToolErrorType::ExecutionError,
354                                    format!(
355                                        "Failed to verify MCP tool '{}' due to provider errors",
356                                        actual_tool_name
357                                    ),
358                                    e.to_string(),
359                                );
360                                return Ok(error.to_json_value());
361                            }
362                        }
363                    } else {
364                        // Check if MCP client has a tool with this exact name
365                        match mcp_client.has_mcp_tool(name).await {
366                            Ok(true) => {
367                                debug!(
368                                    "Tool '{}' not found in registry, delegating to MCP client",
369                                    name
370                                );
371                                return self.execute_mcp_tool(name, args).await;
372                            }
373                            Ok(false) => {
374                                // MCP client doesn't have this tool either
375                                let error = ToolExecutionError::new(
376                                    name.to_string(),
377                                    ToolErrorType::ToolNotFound,
378                                    format!("Unknown tool: {}", name),
379                                );
380                                return Ok(error.to_json_value());
381                            }
382                            Err(e) => {
383                                warn!("Error checking MCP tool availability for '{}': {}", name, e);
384                                let error = ToolExecutionError::with_original_error(
385                                    name.to_string(),
386                                    ToolErrorType::ExecutionError,
387                                    format!(
388                                        "Failed to verify MCP tool '{}' due to provider errors",
389                                        name
390                                    ),
391                                    e.to_string(),
392                                );
393                                return Ok(error.to_json_value());
394                            }
395                        }
396                    }
397                } else {
398                    // No MCP client available
399                    let error = ToolExecutionError::new(
400                        name.to_string(),
401                        ToolErrorType::ToolNotFound,
402                        format!("Unknown tool: {}", name),
403                    );
404                    return Ok(error.to_json_value());
405                }
406            }
407        };
408
409        let uses_pty = registration.uses_pty();
410        if uses_pty && let Err(err) = self.start_pty_session() {
411            let error = ToolExecutionError::with_original_error(
412                name.to_string(),
413                ToolErrorType::ExecutionError,
414                "Failed to start PTY session".to_string(),
415                err.to_string(),
416            );
417            return Ok(error.to_json_value());
418        }
419
420        let handler = registration.handler();
421        let result = match handler {
422            ToolHandler::RegistryFn(executor) => executor(self, args).await,
423            ToolHandler::TraitObject(tool) => tool.execute(args).await,
424        };
425
426        if uses_pty {
427            self.end_pty_session();
428        }
429
430        match result {
431            Ok(value) => Ok(normalize_tool_output(value)),
432            Err(err) => {
433                let error_type = classify_error(&err);
434                let error = ToolExecutionError::with_original_error(
435                    name.to_string(),
436                    error_type,
437                    format!("Tool execution failed: {}", err),
438                    err.to_string(),
439                );
440                Ok(error.to_json_value())
441            }
442        }
443    }
444
445    /// Set the MCP client for this registry
446    pub fn with_mcp_client(mut self, mcp_client: Arc<McpClient>) -> Self {
447        self.mcp_client = Some(mcp_client);
448        self
449    }
450
451    /// Attach an MCP client without consuming the registry
452    pub fn set_mcp_client(&mut self, mcp_client: Arc<McpClient>) {
453        self.mcp_client = Some(mcp_client);
454        self.mcp_tool_index.clear();
455    }
456
457    /// Get the MCP client if available
458    pub fn mcp_client(&self) -> Option<&Arc<McpClient>> {
459        self.mcp_client.as_ref()
460    }
461
462    /// List all MCP tools
463    pub async fn list_mcp_tools(&self) -> Result<Vec<McpToolInfo>> {
464        if let Some(mcp_client) = &self.mcp_client {
465            mcp_client.list_mcp_tools().await
466        } else {
467            Ok(Vec::new())
468        }
469    }
470
471    /// Check if an MCP tool exists
472    pub async fn has_mcp_tool(&self, tool_name: &str) -> bool {
473        if let Some(mcp_client) = &self.mcp_client {
474            match mcp_client.has_mcp_tool(tool_name).await {
475                Ok(true) => true,
476                Ok(false) => false,
477                Err(_) => {
478                    // Log error but return false to continue operation
479                    false
480                }
481            }
482        } else {
483            false
484        }
485    }
486
487    /// Execute an MCP tool
488    pub async fn execute_mcp_tool(&self, tool_name: &str, args: Value) -> Result<Value> {
489        if let Some(mcp_client) = &self.mcp_client {
490            mcp_client.execute_mcp_tool(tool_name, args).await
491        } else {
492            Err(anyhow::anyhow!("MCP client not available"))
493        }
494    }
495
496    async fn resolve_mcp_tool_alias(&self, tool_name: &str) -> Option<String> {
497        let Some(mcp_client) = &self.mcp_client else {
498            return None;
499        };
500
501        let normalized = normalize_mcp_tool_identifier(tool_name);
502        if normalized.is_empty() {
503            return None;
504        }
505
506        let tools = match mcp_client.list_mcp_tools().await {
507            Ok(list) => list,
508            Err(err) => {
509                warn!(
510                    "Failed to list MCP tools while resolving alias '{}': {}",
511                    tool_name, err
512                );
513                return None;
514            }
515        };
516
517        for tool in tools {
518            if normalize_mcp_tool_identifier(&tool.name) == normalized {
519                return Some(tool.name);
520            }
521        }
522
523        None
524    }
525
526    /// Refresh MCP tools (reconnect to providers and update tool lists)
527    pub async fn refresh_mcp_tools(&mut self) -> Result<()> {
528        if let Some(mcp_client) = &self.mcp_client {
529            debug!(
530                "Refreshing MCP tools for {} providers",
531                mcp_client.get_status().provider_count
532            );
533
534            let tools = mcp_client.list_mcp_tools().await?;
535            let mut provider_map: HashMap<String, Vec<String>> = HashMap::new();
536
537            for tool in tools {
538                provider_map
539                    .entry(tool.provider.clone())
540                    .or_default()
541                    .push(tool.name.clone());
542            }
543
544            for tools in provider_map.values_mut() {
545                tools.sort();
546                tools.dedup();
547            }
548
549            self.mcp_tool_index = provider_map;
550
551            if let Some(policy_manager) = self.tool_policy.as_mut() {
552                policy_manager.update_mcp_tools(&self.mcp_tool_index)?;
553                let allowlist = policy_manager.mcp_allowlist().clone();
554                mcp_client.update_allowlist(allowlist);
555            }
556
557            self.sync_policy_available_tools();
558            Ok(())
559        } else {
560            debug!("No MCP client configured, nothing to refresh");
561            Ok(())
562        }
563    }
564}
565
566impl ToolRegistry {
567    /// Prompt for permission before starting long-running tool executions to avoid spinner conflicts
568    pub fn preflight_tool_permission(&mut self, name: &str) -> Result<bool> {
569        match self.evaluate_tool_policy(name)? {
570            ToolPermissionDecision::Allow => Ok(true),
571            ToolPermissionDecision::Deny => Ok(false),
572            ToolPermissionDecision::Prompt => Ok(true),
573        }
574    }
575
576    pub fn evaluate_tool_policy(&mut self, name: &str) -> Result<ToolPermissionDecision> {
577        if let Some(tool_name) = name.strip_prefix("mcp_") {
578            return self.evaluate_mcp_tool_policy(name, tool_name);
579        }
580
581        if let Some(allowlist) = self.full_auto_allowlist.as_ref() {
582            if !allowlist.contains(name) {
583                return Ok(ToolPermissionDecision::Deny);
584            }
585
586            if let Some(policy_manager) = self.tool_policy.as_mut() {
587                match policy_manager.get_policy(name) {
588                    ToolPolicy::Deny => return Ok(ToolPermissionDecision::Deny),
589                    ToolPolicy::Allow | ToolPolicy::Prompt => {
590                        self.preapproved_tools.insert(name.to_string());
591                        return Ok(ToolPermissionDecision::Allow);
592                    }
593                }
594            }
595
596            self.preapproved_tools.insert(name.to_string());
597            return Ok(ToolPermissionDecision::Allow);
598        }
599
600        if let Some(policy_manager) = self.tool_policy.as_mut() {
601            match policy_manager.get_policy(name) {
602                ToolPolicy::Allow => {
603                    self.preapproved_tools.insert(name.to_string());
604                    Ok(ToolPermissionDecision::Allow)
605                }
606                ToolPolicy::Deny => Ok(ToolPermissionDecision::Deny),
607                ToolPolicy::Prompt => {
608                    if ToolPolicyManager::is_auto_allow_tool(name) {
609                        policy_manager.set_policy(name, ToolPolicy::Allow)?;
610                        self.preapproved_tools.insert(name.to_string());
611                        Ok(ToolPermissionDecision::Allow)
612                    } else {
613                        Ok(ToolPermissionDecision::Prompt)
614                    }
615                }
616            }
617        } else {
618            self.preapproved_tools.insert(name.to_string());
619            Ok(ToolPermissionDecision::Allow)
620        }
621    }
622
623    fn evaluate_mcp_tool_policy(
624        &mut self,
625        full_name: &str,
626        tool_name: &str,
627    ) -> Result<ToolPermissionDecision> {
628        let provider = match self.find_mcp_provider(tool_name) {
629            Some(provider) => provider,
630            None => {
631                // Unknown provider for this tool; default to prompt for safety
632                return Ok(ToolPermissionDecision::Prompt);
633            }
634        };
635
636        if let Some(allowlist) = self.full_auto_allowlist.as_ref() {
637            if !allowlist.contains(full_name) {
638                return Ok(ToolPermissionDecision::Deny);
639            }
640
641            if let Some(policy_manager) = self.tool_policy.as_mut() {
642                match policy_manager.get_mcp_tool_policy(&provider, tool_name) {
643                    ToolPolicy::Deny => return Ok(ToolPermissionDecision::Deny),
644                    ToolPolicy::Allow | ToolPolicy::Prompt => {
645                        self.preapproved_tools.insert(full_name.to_string());
646                        return Ok(ToolPermissionDecision::Allow);
647                    }
648                }
649            }
650
651            self.preapproved_tools.insert(full_name.to_string());
652            return Ok(ToolPermissionDecision::Allow);
653        }
654
655        if let Some(policy_manager) = self.tool_policy.as_mut() {
656            match policy_manager.get_mcp_tool_policy(&provider, tool_name) {
657                ToolPolicy::Allow => {
658                    self.preapproved_tools.insert(full_name.to_string());
659                    Ok(ToolPermissionDecision::Allow)
660                }
661                ToolPolicy::Deny => Ok(ToolPermissionDecision::Deny),
662                ToolPolicy::Prompt => Ok(ToolPermissionDecision::Prompt),
663            }
664        } else {
665            self.preapproved_tools.insert(full_name.to_string());
666            Ok(ToolPermissionDecision::Allow)
667        }
668    }
669
670    pub fn mark_tool_preapproved(&mut self, name: &str) {
671        self.preapproved_tools.insert(name.to_string());
672    }
673
674    pub fn persist_mcp_tool_policy(&mut self, name: &str, policy: ToolPolicy) -> Result<()> {
675        if !name.starts_with("mcp_") {
676            return Ok(());
677        }
678
679        let Some(tool_name) = name.strip_prefix("mcp_") else {
680            return Ok(());
681        };
682
683        let Some(provider) = self.find_mcp_provider(tool_name) else {
684            return Ok(());
685        };
686
687        if let Some(manager) = self.tool_policy.as_mut() {
688            manager.set_mcp_tool_policy(&provider, tool_name, policy)?;
689        }
690
691        Ok(())
692    }
693}
694
695fn normalize_mcp_tool_identifier(value: &str) -> String {
696    let mut normalized = String::new();
697    for ch in value.chars() {
698        if ch.is_ascii_alphanumeric() {
699            normalized.push(ch.to_ascii_lowercase());
700        }
701    }
702    normalized
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    use async_trait::async_trait;
709    use serde_json::json;
710    use tempfile::TempDir;
711
712    const CUSTOM_TOOL_NAME: &str = "custom_test_tool";
713
714    struct CustomEchoTool;
715
716    #[async_trait]
717    impl Tool for CustomEchoTool {
718        async fn execute(&self, args: Value) -> Result<Value> {
719            Ok(json!({
720                "success": true,
721                "args": args,
722            }))
723        }
724
725        fn name(&self) -> &'static str {
726            CUSTOM_TOOL_NAME
727        }
728
729        fn description(&self) -> &'static str {
730            "Custom echo tool for testing"
731        }
732    }
733
734    #[tokio::test]
735    async fn registers_builtin_tools() -> Result<()> {
736        let temp_dir = TempDir::new()?;
737        let registry = ToolRegistry::new(temp_dir.path().to_path_buf());
738        let available = registry.available_tools();
739
740        assert!(available.contains(&tools::READ_FILE.to_string()));
741        assert!(available.contains(&tools::RUN_TERMINAL_CMD.to_string()));
742        assert!(available.contains(&tools::CURL.to_string()));
743        Ok(())
744    }
745
746    #[tokio::test]
747    async fn allows_registering_custom_tools() -> Result<()> {
748        let temp_dir = TempDir::new()?;
749        let mut registry = ToolRegistry::new(temp_dir.path().to_path_buf());
750
751        registry.register_tool(ToolRegistration::from_tool_instance(
752            CUSTOM_TOOL_NAME,
753            CapabilityLevel::CodeSearch,
754            CustomEchoTool,
755        ))?;
756
757        registry.sync_policy_available_tools();
758
759        registry.allow_all_tools().ok();
760
761        let available = registry.available_tools();
762        assert!(available.contains(&CUSTOM_TOOL_NAME.to_string()));
763
764        let response = registry
765            .execute_tool(CUSTOM_TOOL_NAME, json!({"input": "value"}))
766            .await?;
767        assert!(response["success"].as_bool().unwrap_or(false));
768        Ok(())
769    }
770
771    #[tokio::test]
772    async fn full_auto_allowlist_enforced() -> Result<()> {
773        let temp_dir = TempDir::new()?;
774        let mut registry = ToolRegistry::new(temp_dir.path().to_path_buf());
775
776        registry.enable_full_auto_mode(&vec![tools::READ_FILE.to_string()]);
777
778        assert!(registry.preflight_tool_permission(tools::READ_FILE)?);
779        assert!(!registry.preflight_tool_permission(tools::RUN_TERMINAL_CMD)?);
780
781        Ok(())
782    }
783
784    #[test]
785    fn normalizes_mcp_tool_identifiers() {
786        assert_eq!(
787            normalize_mcp_tool_identifier("sequential-thinking"),
788            "sequentialthinking"
789        );
790        assert_eq!(
791            normalize_mcp_tool_identifier("Context7.Lookup"),
792            "context7lookup"
793        );
794        assert_eq!(normalize_mcp_tool_identifier("alpha_beta"), "alphabeta");
795    }
796}