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    /// Get the MCP client if available
452    pub fn mcp_client(&self) -> Option<&Arc<McpClient>> {
453        self.mcp_client.as_ref()
454    }
455
456    /// List all MCP tools
457    pub async fn list_mcp_tools(&self) -> Result<Vec<McpToolInfo>> {
458        if let Some(mcp_client) = &self.mcp_client {
459            mcp_client.list_mcp_tools().await
460        } else {
461            Ok(Vec::new())
462        }
463    }
464
465    /// Check if an MCP tool exists
466    pub async fn has_mcp_tool(&self, tool_name: &str) -> bool {
467        if let Some(mcp_client) = &self.mcp_client {
468            match mcp_client.has_mcp_tool(tool_name).await {
469                Ok(true) => true,
470                Ok(false) => false,
471                Err(_) => {
472                    // Log error but return false to continue operation
473                    false
474                }
475            }
476        } else {
477            false
478        }
479    }
480
481    /// Execute an MCP tool
482    pub async fn execute_mcp_tool(&self, tool_name: &str, args: Value) -> Result<Value> {
483        if let Some(mcp_client) = &self.mcp_client {
484            mcp_client.execute_mcp_tool(tool_name, args).await
485        } else {
486            Err(anyhow::anyhow!("MCP client not available"))
487        }
488    }
489
490    async fn resolve_mcp_tool_alias(&self, tool_name: &str) -> Option<String> {
491        let Some(mcp_client) = &self.mcp_client else {
492            return None;
493        };
494
495        let normalized = normalize_mcp_tool_identifier(tool_name);
496        if normalized.is_empty() {
497            return None;
498        }
499
500        let tools = match mcp_client.list_mcp_tools().await {
501            Ok(list) => list,
502            Err(err) => {
503                warn!(
504                    "Failed to list MCP tools while resolving alias '{}': {}",
505                    tool_name, err
506                );
507                return None;
508            }
509        };
510
511        for tool in tools {
512            if normalize_mcp_tool_identifier(&tool.name) == normalized {
513                return Some(tool.name);
514            }
515        }
516
517        None
518    }
519
520    /// Refresh MCP tools (reconnect to providers and update tool lists)
521    pub async fn refresh_mcp_tools(&mut self) -> Result<()> {
522        if let Some(mcp_client) = &self.mcp_client {
523            debug!(
524                "Refreshing MCP tools for {} providers",
525                mcp_client.get_status().provider_count
526            );
527
528            let tools = mcp_client.list_mcp_tools().await?;
529            let mut provider_map: HashMap<String, Vec<String>> = HashMap::new();
530
531            for tool in tools {
532                provider_map
533                    .entry(tool.provider.clone())
534                    .or_default()
535                    .push(tool.name.clone());
536            }
537
538            for tools in provider_map.values_mut() {
539                tools.sort();
540                tools.dedup();
541            }
542
543            self.mcp_tool_index = provider_map;
544
545            if let Some(policy_manager) = self.tool_policy.as_mut() {
546                policy_manager.update_mcp_tools(&self.mcp_tool_index)?;
547                let allowlist = policy_manager.mcp_allowlist().clone();
548                mcp_client.update_allowlist(allowlist);
549            }
550
551            self.sync_policy_available_tools();
552            Ok(())
553        } else {
554            debug!("No MCP client configured, nothing to refresh");
555            Ok(())
556        }
557    }
558}
559
560impl ToolRegistry {
561    /// Prompt for permission before starting long-running tool executions to avoid spinner conflicts
562    pub fn preflight_tool_permission(&mut self, name: &str) -> Result<bool> {
563        match self.evaluate_tool_policy(name)? {
564            ToolPermissionDecision::Allow => Ok(true),
565            ToolPermissionDecision::Deny => Ok(false),
566            ToolPermissionDecision::Prompt => Ok(true),
567        }
568    }
569
570    pub fn evaluate_tool_policy(&mut self, name: &str) -> Result<ToolPermissionDecision> {
571        if let Some(tool_name) = name.strip_prefix("mcp_") {
572            return self.evaluate_mcp_tool_policy(name, tool_name);
573        }
574
575        if let Some(allowlist) = self.full_auto_allowlist.as_ref() {
576            if !allowlist.contains(name) {
577                return Ok(ToolPermissionDecision::Deny);
578            }
579
580            if let Some(policy_manager) = self.tool_policy.as_mut() {
581                match policy_manager.get_policy(name) {
582                    ToolPolicy::Deny => return Ok(ToolPermissionDecision::Deny),
583                    ToolPolicy::Allow | ToolPolicy::Prompt => {
584                        self.preapproved_tools.insert(name.to_string());
585                        return Ok(ToolPermissionDecision::Allow);
586                    }
587                }
588            }
589
590            self.preapproved_tools.insert(name.to_string());
591            return Ok(ToolPermissionDecision::Allow);
592        }
593
594        if let Some(policy_manager) = self.tool_policy.as_mut() {
595            match policy_manager.get_policy(name) {
596                ToolPolicy::Allow => {
597                    self.preapproved_tools.insert(name.to_string());
598                    Ok(ToolPermissionDecision::Allow)
599                }
600                ToolPolicy::Deny => Ok(ToolPermissionDecision::Deny),
601                ToolPolicy::Prompt => {
602                    if ToolPolicyManager::is_auto_allow_tool(name) {
603                        policy_manager.set_policy(name, ToolPolicy::Allow)?;
604                        self.preapproved_tools.insert(name.to_string());
605                        Ok(ToolPermissionDecision::Allow)
606                    } else {
607                        Ok(ToolPermissionDecision::Prompt)
608                    }
609                }
610            }
611        } else {
612            self.preapproved_tools.insert(name.to_string());
613            Ok(ToolPermissionDecision::Allow)
614        }
615    }
616
617    fn evaluate_mcp_tool_policy(
618        &mut self,
619        full_name: &str,
620        tool_name: &str,
621    ) -> Result<ToolPermissionDecision> {
622        let provider = match self.find_mcp_provider(tool_name) {
623            Some(provider) => provider,
624            None => {
625                // Unknown provider for this tool; default to prompt for safety
626                return Ok(ToolPermissionDecision::Prompt);
627            }
628        };
629
630        if let Some(allowlist) = self.full_auto_allowlist.as_ref() {
631            if !allowlist.contains(full_name) {
632                return Ok(ToolPermissionDecision::Deny);
633            }
634
635            if let Some(policy_manager) = self.tool_policy.as_mut() {
636                match policy_manager.get_mcp_tool_policy(&provider, tool_name) {
637                    ToolPolicy::Deny => return Ok(ToolPermissionDecision::Deny),
638                    ToolPolicy::Allow | ToolPolicy::Prompt => {
639                        self.preapproved_tools.insert(full_name.to_string());
640                        return Ok(ToolPermissionDecision::Allow);
641                    }
642                }
643            }
644
645            self.preapproved_tools.insert(full_name.to_string());
646            return Ok(ToolPermissionDecision::Allow);
647        }
648
649        if let Some(policy_manager) = self.tool_policy.as_mut() {
650            match policy_manager.get_mcp_tool_policy(&provider, tool_name) {
651                ToolPolicy::Allow => {
652                    self.preapproved_tools.insert(full_name.to_string());
653                    Ok(ToolPermissionDecision::Allow)
654                }
655                ToolPolicy::Deny => Ok(ToolPermissionDecision::Deny),
656                ToolPolicy::Prompt => Ok(ToolPermissionDecision::Prompt),
657            }
658        } else {
659            self.preapproved_tools.insert(full_name.to_string());
660            Ok(ToolPermissionDecision::Allow)
661        }
662    }
663
664    pub fn mark_tool_preapproved(&mut self, name: &str) {
665        self.preapproved_tools.insert(name.to_string());
666    }
667
668    pub fn persist_mcp_tool_policy(&mut self, name: &str, policy: ToolPolicy) -> Result<()> {
669        if !name.starts_with("mcp_") {
670            return Ok(());
671        }
672
673        let Some(tool_name) = name.strip_prefix("mcp_") else {
674            return Ok(());
675        };
676
677        let Some(provider) = self.find_mcp_provider(tool_name) else {
678            return Ok(());
679        };
680
681        if let Some(manager) = self.tool_policy.as_mut() {
682            manager.set_mcp_tool_policy(&provider, tool_name, policy)?;
683        }
684
685        Ok(())
686    }
687}
688
689fn normalize_mcp_tool_identifier(value: &str) -> String {
690    let mut normalized = String::new();
691    for ch in value.chars() {
692        if ch.is_ascii_alphanumeric() {
693            normalized.push(ch.to_ascii_lowercase());
694        }
695    }
696    normalized
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702    use async_trait::async_trait;
703    use serde_json::json;
704    use tempfile::TempDir;
705
706    const CUSTOM_TOOL_NAME: &str = "custom_test_tool";
707
708    struct CustomEchoTool;
709
710    #[async_trait]
711    impl Tool for CustomEchoTool {
712        async fn execute(&self, args: Value) -> Result<Value> {
713            Ok(json!({
714                "success": true,
715                "args": args,
716            }))
717        }
718
719        fn name(&self) -> &'static str {
720            CUSTOM_TOOL_NAME
721        }
722
723        fn description(&self) -> &'static str {
724            "Custom echo tool for testing"
725        }
726    }
727
728    #[tokio::test]
729    async fn registers_builtin_tools() -> Result<()> {
730        let temp_dir = TempDir::new()?;
731        let registry = ToolRegistry::new(temp_dir.path().to_path_buf());
732        let available = registry.available_tools();
733
734        assert!(available.contains(&tools::READ_FILE.to_string()));
735        assert!(available.contains(&tools::RUN_TERMINAL_CMD.to_string()));
736        assert!(available.contains(&tools::CURL.to_string()));
737        Ok(())
738    }
739
740    #[tokio::test]
741    async fn allows_registering_custom_tools() -> Result<()> {
742        let temp_dir = TempDir::new()?;
743        let mut registry = ToolRegistry::new(temp_dir.path().to_path_buf());
744
745        registry.register_tool(ToolRegistration::from_tool_instance(
746            CUSTOM_TOOL_NAME,
747            CapabilityLevel::CodeSearch,
748            CustomEchoTool,
749        ))?;
750
751        registry.sync_policy_available_tools();
752
753        registry.allow_all_tools().ok();
754
755        let available = registry.available_tools();
756        assert!(available.contains(&CUSTOM_TOOL_NAME.to_string()));
757
758        let response = registry
759            .execute_tool(CUSTOM_TOOL_NAME, json!({"input": "value"}))
760            .await?;
761        assert!(response["success"].as_bool().unwrap_or(false));
762        Ok(())
763    }
764
765    #[tokio::test]
766    async fn full_auto_allowlist_enforced() -> Result<()> {
767        let temp_dir = TempDir::new()?;
768        let mut registry = ToolRegistry::new(temp_dir.path().to_path_buf());
769
770        registry.enable_full_auto_mode(&vec![tools::READ_FILE.to_string()]);
771
772        assert!(registry.preflight_tool_permission(tools::READ_FILE)?);
773        assert!(!registry.preflight_tool_permission(tools::RUN_TERMINAL_CMD)?);
774
775        Ok(())
776    }
777
778    #[test]
779    fn normalizes_mcp_tool_identifiers() {
780        assert_eq!(
781            normalize_mcp_tool_identifier("sequential-thinking"),
782            "sequentialthinking"
783        );
784        assert_eq!(
785            normalize_mcp_tool_identifier("Context7.Lookup"),
786            "context7lookup"
787        );
788        assert_eq!(normalize_mcp_tool_identifier("alpha_beta"), "alphabeta");
789    }
790}