vtcode_core/
tool_policy.rs

1//! Tool policy management system
2//!
3//! This module manages user preferences for tool usage, storing choices in
4//! ~/.vtcode/tool-policy.json to minimize repeated prompts while maintaining
5//! user control overwhich tools the agent can use.
6
7use anyhow::{Context, Result};
8use console::{Color as ConsoleColor, Style as ConsoleStyle, style};
9use dialoguer::{Confirm, theme::ColorfulTheme};
10use indexmap::IndexMap;
11use is_terminal::IsTerminal;
12use serde::{Deserialize, Serialize};
13use std::fs;
14use std::path::{Path, PathBuf};
15
16use crate::ui::theme;
17use crate::utils::ansi::{AnsiRenderer, MessageStyle};
18
19use crate::config::constants::tools;
20use crate::config::core::tools::{ToolPolicy as ConfigToolPolicy, ToolsConfig};
21
22const AUTO_ALLOW_TOOLS: &[&str] = &["run_terminal_cmd", "bash"];
23const DEFAULT_CURL_MAX_RESPONSE_BYTES: usize = 64 * 1024;
24
25/// Tool execution policy
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum ToolPolicy {
29    /// Allow tool execution without prompting
30    Allow,
31    /// Prompt user for confirmation each time
32    Prompt,
33    /// Never allow tool execution
34    Deny,
35}
36
37impl Default for ToolPolicy {
38    fn default() -> Self {
39        ToolPolicy::Prompt
40    }
41}
42
43/// Tool policy configuration stored in ~/.vtcode/tool-policy.json
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ToolPolicyConfig {
46    /// Configuration version for future compatibility
47    pub version: u32,
48    /// Available tools at time of last update
49    pub available_tools: Vec<String>,
50    /// Policy for each tool
51    pub policies: IndexMap<String, ToolPolicy>,
52    /// Optional per-tool constraints to scope permissions and enforce safety
53    #[serde(default)]
54    pub constraints: IndexMap<String, ToolConstraints>,
55}
56
57impl Default for ToolPolicyConfig {
58    fn default() -> Self {
59        Self {
60            version: 1,
61            available_tools: Vec::new(),
62            policies: IndexMap::new(),
63            constraints: IndexMap::new(),
64        }
65    }
66}
67
68/// Alternative tool policy configuration format (user's format)
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct AlternativeToolPolicyConfig {
71    /// Configuration version for future compatibility
72    pub version: u32,
73    /// Default policy settings
74    pub default: AlternativeDefaultPolicy,
75    /// Tool-specific policies
76    pub tools: IndexMap<String, AlternativeToolPolicy>,
77    /// Optional per-tool constraints (ignored if absent)
78    #[serde(default)]
79    pub constraints: IndexMap<String, ToolConstraints>,
80}
81
82/// Default policy in alternative format
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct AlternativeDefaultPolicy {
85    /// Whether to allow by default
86    pub allow: bool,
87    /// Rate limit per run
88    pub rate_limit_per_run: u32,
89    /// Max concurrent executions
90    pub max_concurrent: u32,
91    /// Allow filesystem writes
92    pub fs_write: bool,
93    /// Allow network access
94    pub network: bool,
95}
96
97/// Tool policy in alternative format
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct AlternativeToolPolicy {
100    /// Whether to allow this tool
101    pub allow: bool,
102    /// Allow filesystem writes (optional)
103    #[serde(default)]
104    pub fs_write: bool,
105    /// Allow network access (optional)
106    #[serde(default)]
107    pub network: bool,
108    /// Arguments policy (optional)
109    #[serde(default)]
110    pub args_policy: Option<AlternativeArgsPolicy>,
111}
112
113/// Arguments policy in alternative format
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct AlternativeArgsPolicy {
116    /// Substrings to deny
117    pub deny_substrings: Vec<String>,
118}
119
120/// Tool policy manager
121#[derive(Clone)]
122pub struct ToolPolicyManager {
123    config_path: PathBuf,
124    config: ToolPolicyConfig,
125}
126
127impl ToolPolicyManager {
128    /// Create a new tool policy manager
129    pub fn new() -> Result<Self> {
130        let config_path = Self::get_config_path()?;
131        let config = Self::load_or_create_config(&config_path)?;
132
133        Ok(Self {
134            config_path,
135            config,
136        })
137    }
138
139    /// Create a new tool policy manager with workspace-specific config
140    pub fn new_with_workspace(workspace_root: &PathBuf) -> Result<Self> {
141        let config_path = Self::get_workspace_config_path(workspace_root)?;
142        let config = Self::load_or_create_config(&config_path)?;
143
144        Ok(Self {
145            config_path,
146            config,
147        })
148    }
149
150    /// Get the path to the tool policy configuration file
151    fn get_config_path() -> Result<PathBuf> {
152        let home_dir = dirs::home_dir().context("Could not determine home directory")?;
153
154        let vtcode_dir = home_dir.join(".vtcode");
155        if !vtcode_dir.exists() {
156            fs::create_dir_all(&vtcode_dir).context("Failed to create ~/.vtcode directory")?;
157        }
158
159        Ok(vtcode_dir.join("tool-policy.json"))
160    }
161
162    /// Get the path to the workspace-specific tool policy configuration file
163    fn get_workspace_config_path(workspace_root: &PathBuf) -> Result<PathBuf> {
164        let workspace_vtcode_dir = workspace_root.join(".vtcode");
165
166        if !workspace_vtcode_dir.exists() {
167            fs::create_dir_all(&workspace_vtcode_dir).with_context(|| {
168                format!(
169                    "Failed to create workspace policy directory at {}",
170                    workspace_vtcode_dir.display()
171                )
172            })?;
173        }
174
175        Ok(workspace_vtcode_dir.join("tool-policy.json"))
176    }
177
178    /// Load existing config or create new one with all tools as "prompt"
179    fn load_or_create_config(config_path: &PathBuf) -> Result<ToolPolicyConfig> {
180        if config_path.exists() {
181            let content =
182                fs::read_to_string(config_path).context("Failed to read tool policy config")?;
183
184            // Try to parse as alternative format first
185            if let Ok(alt_config) = serde_json::from_str::<AlternativeToolPolicyConfig>(&content) {
186                // Convert alternative format to standard format
187                return Ok(Self::convert_from_alternative(alt_config));
188            }
189
190            // Fall back to standard format with graceful recovery on parse errors
191            match serde_json::from_str(&content) {
192                Ok(mut config) => {
193                    Self::apply_auto_allow_defaults(&mut config);
194                    Self::ensure_network_constraints(&mut config);
195                    Ok(config)
196                }
197                Err(parse_err) => {
198                    eprintln!(
199                        "Warning: Invalid tool policy config at {} ({}). Resetting to defaults.",
200                        config_path.display(),
201                        parse_err
202                    );
203                    Self::reset_to_default(config_path)
204                }
205            }
206        } else {
207            // Create new config with empty tools list
208            let mut config = ToolPolicyConfig::default();
209            Self::apply_auto_allow_defaults(&mut config);
210            Self::ensure_network_constraints(&mut config);
211            Ok(config)
212        }
213    }
214
215    fn apply_auto_allow_defaults(config: &mut ToolPolicyConfig) {
216        for tool in AUTO_ALLOW_TOOLS {
217            config
218                .policies
219                .entry((*tool).to_string())
220                .and_modify(|policy| *policy = ToolPolicy::Allow)
221                .or_insert(ToolPolicy::Allow);
222            if !config.available_tools.contains(&tool.to_string()) {
223                config.available_tools.push(tool.to_string());
224            }
225        }
226        Self::ensure_network_constraints(config);
227    }
228
229    fn ensure_network_constraints(config: &mut ToolPolicyConfig) {
230        let entry = config
231            .constraints
232            .entry(tools::CURL.to_string())
233            .or_insert_with(ToolConstraints::default);
234
235        if entry.max_response_bytes.is_none() {
236            entry.max_response_bytes = Some(DEFAULT_CURL_MAX_RESPONSE_BYTES);
237        }
238        if entry.allowed_url_schemes.is_none() {
239            entry.allowed_url_schemes = Some(vec!["https".to_string()]);
240        }
241        if entry.denied_url_hosts.is_none() {
242            entry.denied_url_hosts = Some(vec![
243                "localhost".to_string(),
244                "127.0.0.1".to_string(),
245                "0.0.0.0".to_string(),
246                "::1".to_string(),
247                ".localhost".to_string(),
248                ".local".to_string(),
249                ".internal".to_string(),
250                ".lan".to_string(),
251            ]);
252        }
253    }
254
255    fn reset_to_default(config_path: &PathBuf) -> Result<ToolPolicyConfig> {
256        let backup_path = config_path.with_extension("json.bak");
257
258        if let Err(err) = fs::rename(config_path, &backup_path) {
259            eprintln!(
260                "Warning: Unable to back up invalid tool policy config ({}). {}",
261                config_path.display(),
262                err
263            );
264        } else {
265            eprintln!(
266                "Backed up invalid tool policy config to {}",
267                backup_path.display()
268            );
269        }
270
271        let default_config = ToolPolicyConfig::default();
272        Self::write_config(config_path.as_path(), &default_config)?;
273        Ok(default_config)
274    }
275
276    fn write_config(path: &Path, config: &ToolPolicyConfig) -> Result<()> {
277        if let Some(parent) = path.parent() {
278            if !parent.exists() {
279                fs::create_dir_all(parent).with_context(|| {
280                    format!(
281                        "Failed to create directory for tool policy config at {}",
282                        parent.display()
283                    )
284                })?;
285            }
286        }
287
288        let serialized = serde_json::to_string_pretty(config)
289            .context("Failed to serialize tool policy config")?;
290
291        fs::write(path, serialized)
292            .with_context(|| format!("Failed to write tool policy config: {}", path.display()))
293    }
294
295    /// Convert alternative format to standard format
296    fn convert_from_alternative(alt_config: AlternativeToolPolicyConfig) -> ToolPolicyConfig {
297        let mut policies = IndexMap::new();
298
299        // Convert tool policies
300        for (tool_name, alt_policy) in alt_config.tools {
301            let policy = if alt_policy.allow {
302                ToolPolicy::Allow
303            } else {
304                ToolPolicy::Deny
305            };
306            policies.insert(tool_name, policy);
307        }
308
309        let mut config = ToolPolicyConfig {
310            version: alt_config.version,
311            available_tools: policies.keys().cloned().collect(),
312            policies,
313            constraints: alt_config.constraints,
314        };
315        Self::apply_auto_allow_defaults(&mut config);
316        config
317    }
318
319    fn apply_config_policy(&mut self, tool_name: &str, policy: ConfigToolPolicy) {
320        let runtime_policy = match policy {
321            ConfigToolPolicy::Allow => ToolPolicy::Allow,
322            ConfigToolPolicy::Prompt => ToolPolicy::Prompt,
323            ConfigToolPolicy::Deny => ToolPolicy::Deny,
324        };
325
326        self.config
327            .policies
328            .insert(tool_name.to_string(), runtime_policy);
329    }
330
331    fn resolve_config_policy(tools_config: &ToolsConfig, tool_name: &str) -> ConfigToolPolicy {
332        if let Some(policy) = tools_config.policies.get(tool_name) {
333            return policy.clone();
334        }
335
336        match tool_name {
337            tools::LIST_FILES => tools_config
338                .policies
339                .get("list_dir")
340                .or_else(|| tools_config.policies.get("list_directory"))
341                .cloned(),
342            _ => None,
343        }
344        .unwrap_or_else(|| tools_config.default_policy.clone())
345    }
346
347    /// Apply policies defined in vtcode.toml to the runtime policy manager
348    pub fn apply_tools_config(&mut self, tools_config: &ToolsConfig) -> Result<()> {
349        if self.config.available_tools.is_empty() {
350            return Ok(());
351        }
352
353        for tool in self.config.available_tools.clone() {
354            let config_policy = Self::resolve_config_policy(tools_config, &tool);
355            self.apply_config_policy(&tool, config_policy);
356        }
357
358        Self::apply_auto_allow_defaults(&mut self.config);
359        self.save_config()
360    }
361
362    /// Update the tool list and save configuration
363    pub fn update_available_tools(&mut self, tools: Vec<String>) -> Result<()> {
364        let current_tools: std::collections::HashSet<_> =
365            self.config.policies.keys().cloned().collect();
366        let new_tools: std::collections::HashSet<_> = tools.iter().cloned().collect();
367
368        // Add new tools with appropriate defaults
369        for tool in tools.iter().filter(|tool| !current_tools.contains(*tool)) {
370            let default_policy = if AUTO_ALLOW_TOOLS.contains(&tool.as_str()) {
371                ToolPolicy::Allow
372            } else {
373                ToolPolicy::Prompt
374            };
375            self.config.policies.insert(tool.clone(), default_policy);
376        }
377
378        // Remove deleted tools - use itertools to find tools to remove
379        let tools_to_remove: Vec<_> = self
380            .config
381            .policies
382            .keys()
383            .filter(|tool| !new_tools.contains(*tool))
384            .cloned()
385            .collect();
386
387        for tool in tools_to_remove {
388            self.config.policies.shift_remove(&tool);
389        }
390
391        // Update available tools list
392        self.config.available_tools = tools;
393
394        Self::ensure_network_constraints(&mut self.config);
395
396        self.save_config()
397    }
398
399    /// Get policy for a specific tool
400    pub fn get_policy(&self, tool_name: &str) -> ToolPolicy {
401        self.config
402            .policies
403            .get(tool_name)
404            .cloned()
405            .unwrap_or(ToolPolicy::Prompt)
406    }
407
408    /// Get optional constraints for a specific tool
409    pub fn get_constraints(&self, tool_name: &str) -> Option<&ToolConstraints> {
410        self.config.constraints.get(tool_name)
411    }
412
413    /// Check if tool should be executed based on policy
414    pub fn should_execute_tool(&mut self, tool_name: &str) -> Result<bool> {
415        match self.get_policy(tool_name) {
416            ToolPolicy::Allow => Ok(true),
417            ToolPolicy::Deny => Ok(false),
418            ToolPolicy::Prompt => {
419                if AUTO_ALLOW_TOOLS.contains(&tool_name) {
420                    self.set_policy(tool_name, ToolPolicy::Allow)?;
421                    return Ok(true);
422                }
423                let should_execute = self.prompt_user_for_tool(tool_name)?;
424                Ok(should_execute)
425            }
426        }
427    }
428
429    pub fn is_auto_allow_tool(tool_name: &str) -> bool {
430        AUTO_ALLOW_TOOLS.contains(&tool_name)
431    }
432
433    /// Prompt user for tool execution permission
434    fn prompt_user_for_tool(&mut self, tool_name: &str) -> Result<bool> {
435        let interactive = std::io::stdin().is_terminal() && std::io::stdout().is_terminal();
436        let mut renderer = AnsiRenderer::stdout();
437        let banner_style = theme::banner_style();
438
439        if !interactive {
440            let message = format!(
441                "Non-interactive environment detected. Auto-approving '{}' tool.",
442                tool_name
443            );
444            renderer.line_with_style(banner_style, &message)?;
445            return Ok(true);
446        }
447
448        let header = format!("Tool Permission Request: {}", tool_name);
449        renderer.line_with_style(banner_style, &header)?;
450        renderer.line_with_style(
451            banner_style,
452            &format!("The agent wants to use the '{}' tool.", tool_name),
453        )?;
454        renderer.line_with_style(banner_style, "")?;
455        renderer.line_with_style(
456            banner_style,
457            "This decision applies to the current request only.",
458        )?;
459        renderer.line_with_style(
460            banner_style,
461            "Update the policy file or use CLI flags to change the default.",
462        )?;
463        renderer.line_with_style(banner_style, "")?;
464
465        if AUTO_ALLOW_TOOLS.contains(&tool_name) {
466            renderer.line_with_style(
467                banner_style,
468                &format!(
469                    "Auto-approving '{}' tool (default trusted tool).",
470                    tool_name
471                ),
472            )?;
473            return Ok(true);
474        }
475
476        let rgb = theme::banner_color();
477        let to_ansi_256 = |value: u8| -> u8 {
478            if value < 48 {
479                0
480            } else if value < 114 {
481                1
482            } else {
483                ((value - 35) / 40).min(5)
484            }
485        };
486        let rgb_to_index = |r: u8, g: u8, b: u8| -> u8 {
487            let r_idx = to_ansi_256(r);
488            let g_idx = to_ansi_256(g);
489            let b_idx = to_ansi_256(b);
490            16 + 36 * r_idx + 6 * g_idx + b_idx
491        };
492        let color_index = rgb_to_index(rgb.0, rgb.1, rgb.2);
493        let dialog_color = ConsoleColor::Color256(color_index);
494        let tinted_style = ConsoleStyle::new().for_stderr().fg(dialog_color);
495
496        let mut dialog_theme = ColorfulTheme::default();
497        dialog_theme.prompt_style = tinted_style;
498        dialog_theme.prompt_prefix = style("—".to_string()).for_stderr().fg(dialog_color);
499        dialog_theme.prompt_suffix = style("—".to_string()).for_stderr().fg(dialog_color);
500        dialog_theme.hint_style = ConsoleStyle::new().for_stderr().fg(dialog_color);
501        dialog_theme.defaults_style = dialog_theme.hint_style.clone();
502        dialog_theme.success_prefix = style("✓".to_string()).for_stderr().fg(dialog_color);
503        dialog_theme.success_suffix = style("·".to_string()).for_stderr().fg(dialog_color);
504        dialog_theme.error_prefix = style("✗".to_string()).for_stderr().fg(dialog_color);
505        dialog_theme.error_style = ConsoleStyle::new().for_stderr().fg(dialog_color);
506        dialog_theme.values_style = ConsoleStyle::new().for_stderr().fg(dialog_color);
507
508        let prompt_text = format!("Allow the agent to use '{}'?", tool_name);
509
510        match Confirm::with_theme(&dialog_theme)
511            .with_prompt(prompt_text)
512            .default(false)
513            .interact()
514        {
515            Ok(confirmed) => {
516                let message = if confirmed {
517                    format!("✓ Approved: '{}' tool will run now", tool_name)
518                } else {
519                    format!("✗ Denied: '{}' tool will not run", tool_name)
520                };
521                let style = if confirmed {
522                    MessageStyle::Tool
523                } else {
524                    MessageStyle::Error
525                };
526                renderer.line(style, &message)?;
527                Ok(confirmed)
528            }
529            Err(e) => {
530                renderer.line(
531                    MessageStyle::Error,
532                    &format!("Failed to read confirmation: {}", e),
533                )?;
534                Ok(false)
535            }
536        }
537    }
538
539    /// Set policy for a specific tool
540    pub fn set_policy(&mut self, tool_name: &str, policy: ToolPolicy) -> Result<()> {
541        self.config.policies.insert(tool_name.to_string(), policy);
542        self.save_config()
543    }
544
545    /// Reset all tools to prompt
546    pub fn reset_all_to_prompt(&mut self) -> Result<()> {
547        for policy in self.config.policies.values_mut() {
548            *policy = ToolPolicy::Prompt;
549        }
550        self.save_config()
551    }
552
553    /// Allow all tools
554    pub fn allow_all_tools(&mut self) -> Result<()> {
555        for policy in self.config.policies.values_mut() {
556            *policy = ToolPolicy::Allow;
557        }
558        self.save_config()
559    }
560
561    /// Deny all tools
562    pub fn deny_all_tools(&mut self) -> Result<()> {
563        for policy in self.config.policies.values_mut() {
564            *policy = ToolPolicy::Deny;
565        }
566        self.save_config()
567    }
568
569    /// Get summary of current policies
570    pub fn get_policy_summary(&self) -> IndexMap<String, ToolPolicy> {
571        self.config.policies.clone()
572    }
573
574    /// Save configuration to file
575    fn save_config(&self) -> Result<()> {
576        Self::write_config(&self.config_path, &self.config)
577    }
578
579    /// Print current policy status
580    pub fn print_status(&self) {
581        println!("{}", style("Tool Policy Status").cyan().bold());
582        println!("Config file: {}", self.config_path.display());
583        println!();
584
585        if self.config.policies.is_empty() {
586            println!("No tools configured yet.");
587            return;
588        }
589
590        let mut allow_count = 0;
591        let mut prompt_count = 0;
592        let mut deny_count = 0;
593
594        for (tool, policy) in &self.config.policies {
595            let (status, color_name) = match policy {
596                ToolPolicy::Allow => {
597                    allow_count += 1;
598                    ("ALLOW", "green")
599                }
600                ToolPolicy::Prompt => {
601                    prompt_count += 1;
602                    ("PROMPT", "yellow")
603                }
604                ToolPolicy::Deny => {
605                    deny_count += 1;
606                    ("DENY", "red")
607                }
608            };
609
610            let status_styled = match color_name {
611                "green" => style(status).green(),
612                "yellow" => style(status).yellow(),
613                "red" => style(status).red(),
614                _ => style(status),
615            };
616
617            println!(
618                "  {} {}",
619                style(format!("{:15}", tool)).cyan(),
620                status_styled
621            );
622        }
623
624        println!();
625        println!(
626            "Summary: {} allowed, {} prompt, {} denied",
627            style(allow_count).green(),
628            style(prompt_count).yellow(),
629            style(deny_count).red()
630        );
631    }
632
633    /// Expose path of the underlying policy configuration file
634    pub fn config_path(&self) -> &Path {
635        &self.config_path
636    }
637}
638
639/// Scoped, optional constraints for a tool to align with safe defaults
640#[derive(Debug, Clone, Default, Serialize, Deserialize)]
641pub struct ToolConstraints {
642    /// Whitelisted modes for tools that support modes (e.g., 'terminal')
643    #[serde(default)]
644    pub allowed_modes: Option<Vec<String>>,
645    /// Cap on results for list/search-like tools
646    #[serde(default)]
647    pub max_results_per_call: Option<usize>,
648    /// Cap on items scanned for file listing
649    #[serde(default)]
650    pub max_items_per_call: Option<usize>,
651    /// Default response format if unspecified by caller
652    #[serde(default)]
653    pub default_response_format: Option<String>,
654    /// Cap maximum bytes when reading files
655    #[serde(default)]
656    pub max_bytes_per_read: Option<usize>,
657    /// Cap maximum bytes when fetching over the network
658    #[serde(default)]
659    pub max_response_bytes: Option<usize>,
660    /// Allowed URL schemes for network tools
661    #[serde(default)]
662    pub allowed_url_schemes: Option<Vec<String>>,
663    /// Denied URL hosts or suffixes for network tools
664    #[serde(default)]
665    pub denied_url_hosts: Option<Vec<String>>,
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671    use crate::config::constants::tools;
672    use tempfile::tempdir;
673
674    #[test]
675    fn test_tool_policy_config_serialization() {
676        let mut config = ToolPolicyConfig::default();
677        config.available_tools = vec![tools::READ_FILE.to_string(), tools::WRITE_FILE.to_string()];
678        config
679            .policies
680            .insert(tools::READ_FILE.to_string(), ToolPolicy::Allow);
681        config
682            .policies
683            .insert(tools::WRITE_FILE.to_string(), ToolPolicy::Prompt);
684
685        let json = serde_json::to_string_pretty(&config).unwrap();
686        let deserialized: ToolPolicyConfig = serde_json::from_str(&json).unwrap();
687
688        assert_eq!(config.available_tools, deserialized.available_tools);
689        assert_eq!(config.policies, deserialized.policies);
690    }
691
692    #[test]
693    fn test_policy_updates() {
694        let dir = tempdir().unwrap();
695        let config_path = dir.path().join("tool-policy.json");
696
697        let mut config = ToolPolicyConfig::default();
698        config.available_tools = vec!["tool1".to_string()];
699        config
700            .policies
701            .insert("tool1".to_string(), ToolPolicy::Prompt);
702
703        // Save initial config
704        let content = serde_json::to_string_pretty(&config).unwrap();
705        fs::write(&config_path, content).unwrap();
706
707        // Load and update
708        let mut loaded_config = ToolPolicyManager::load_or_create_config(&config_path).unwrap();
709
710        // Add new tool
711        let new_tools = vec!["tool1".to_string(), "tool2".to_string()];
712        let current_tools: std::collections::HashSet<_> =
713            loaded_config.available_tools.iter().collect();
714
715        for tool in &new_tools {
716            if !current_tools.contains(tool) {
717                loaded_config
718                    .policies
719                    .insert(tool.clone(), ToolPolicy::Prompt);
720            }
721        }
722
723        loaded_config.available_tools = new_tools;
724
725        assert_eq!(loaded_config.policies.len(), 2);
726        assert_eq!(
727            loaded_config.policies.get("tool2"),
728            Some(&ToolPolicy::Prompt)
729        );
730    }
731}