Skip to main content

vtcode_config/core/
tools.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3
4use crate::constants::{defaults, tools};
5use crate::core::plugins::PluginRuntimeConfig;
6
7/// Tools configuration
8#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct ToolsConfig {
11    /// Default policy for tools not explicitly listed
12    #[serde(default = "default_tool_policy")]
13    pub default_policy: ToolPolicy,
14
15    /// Specific tool policies
16    #[serde(default)]
17    #[cfg_attr(
18        feature = "schema",
19        schemars(with = "std::collections::BTreeMap<String, ToolPolicy>")
20    )]
21    pub policies: IndexMap<String, ToolPolicy>,
22
23    /// Maximum inner tool-call loops per user turn
24    ///
25    /// Prevents infinite tool-calling cycles in interactive chat. This limits how
26    /// many back-and-forths the agent will perform executing tools and
27    /// re-asking the model before returning a final answer.
28    ///
29    #[serde(default = "default_max_tool_loops")]
30    pub max_tool_loops: usize,
31
32    /// Maximum number of times the same tool invocation can be retried with the
33    /// identical arguments within a single turn.
34    #[serde(default = "default_max_repeated_tool_calls")]
35    pub max_repeated_tool_calls: usize,
36
37    /// Maximum consecutive blocked tool calls allowed per turn before forcing a
38    /// turn break. This prevents long blocked-call churn from consuming CPU.
39    #[serde(default = "default_max_consecutive_blocked_tool_calls_per_turn")]
40    pub max_consecutive_blocked_tool_calls_per_turn: usize,
41
42    /// Optional per-second rate limit for tool calls to smooth bursty retries.
43    /// When unset, the runtime defaults apply.
44    #[serde(default = "default_max_tool_rate_per_second")]
45    pub max_tool_rate_per_second: Option<usize>,
46
47    /// Maximum sequential spool-chunk `read_file` calls allowed per turn before
48    /// nudging the agent to switch to targeted extraction/summarization.
49    #[serde(default = "default_max_sequential_spool_chunk_reads")]
50    pub max_sequential_spool_chunk_reads: usize,
51
52    /// Web Fetch tool security configuration
53    #[serde(default)]
54    pub web_fetch: WebFetchConfig,
55
56    /// Dynamic plugin runtime configuration
57    #[serde(default)]
58    pub plugins: PluginRuntimeConfig,
59
60    /// External editor integration settings used by `/edit` and keyboard shortcuts
61    #[serde(default)]
62    pub editor: EditorToolConfig,
63
64    /// Tool-specific loop thresholds (Adaptive Loop Detection)
65    /// Allows setting higher loop limits for read-only tools (e.g., ls, grep)
66    /// and lower limits for mutating tools.
67    #[serde(default)]
68    pub loop_thresholds: IndexMap<String, usize>,
69}
70
71/// External editor integration configuration
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct EditorToolConfig {
75    /// Enable external editor support for `/edit` and keyboard shortcuts
76    #[serde(default = "default_editor_enabled")]
77    pub enabled: bool,
78
79    /// Preferred editor command override (supports arguments, e.g. "code --wait")
80    #[serde(default)]
81    pub preferred_editor: String,
82
83    /// Suspend the TUI event loop while editor is running
84    #[serde(default = "default_editor_suspend_tui")]
85    pub suspend_tui: bool,
86}
87
88/// Web Fetch tool security configuration
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[derive(Debug, Clone, Deserialize, Serialize)]
91pub struct WebFetchConfig {
92    /// Security mode: "restricted" (blocklist) or "whitelist" (allowlist)
93    #[serde(default = "default_web_fetch_mode")]
94    pub mode: String,
95
96    /// Enable dynamic blocklist loading from external file
97    #[serde(default)]
98    pub dynamic_blocklist_enabled: bool,
99
100    /// Path to dynamic blocklist file
101    #[serde(default)]
102    pub dynamic_blocklist_path: String,
103
104    /// Enable dynamic whitelist loading from external file
105    #[serde(default)]
106    pub dynamic_whitelist_enabled: bool,
107
108    /// Path to dynamic whitelist file
109    #[serde(default)]
110    pub dynamic_whitelist_path: String,
111
112    /// Inline blocklist - Additional domains to block
113    #[serde(default)]
114    pub blocked_domains: Vec<String>,
115
116    /// Inline whitelist - Domains to allow in restricted mode
117    #[serde(default)]
118    pub allowed_domains: Vec<String>,
119
120    /// Additional blocked patterns
121    #[serde(default)]
122    pub blocked_patterns: Vec<String>,
123
124    /// Enable audit logging of URL validation decisions
125    #[serde(default)]
126    pub enable_audit_logging: bool,
127
128    /// Path to audit log file
129    #[serde(default)]
130    pub audit_log_path: String,
131
132    /// Strict HTTPS-only mode
133    #[serde(default = "default_strict_https")]
134    pub strict_https_only: bool,
135}
136
137impl Default for ToolsConfig {
138    fn default() -> Self {
139        let policies = DEFAULT_TOOL_POLICIES
140            .iter()
141            .map(|(tool, policy)| ((*tool).into(), *policy))
142            .collect::<IndexMap<_, _>>();
143        Self {
144            default_policy: default_tool_policy(),
145            policies,
146            max_tool_loops: default_max_tool_loops(),
147            max_repeated_tool_calls: default_max_repeated_tool_calls(),
148            max_consecutive_blocked_tool_calls_per_turn:
149                default_max_consecutive_blocked_tool_calls_per_turn(),
150            max_tool_rate_per_second: default_max_tool_rate_per_second(),
151            max_sequential_spool_chunk_reads: default_max_sequential_spool_chunk_reads(),
152            web_fetch: WebFetchConfig::default(),
153            plugins: PluginRuntimeConfig::default(),
154            editor: EditorToolConfig::default(),
155            loop_thresholds: IndexMap::new(),
156        }
157    }
158}
159
160const DEFAULT_BLOCKLIST_PATH: &str = "~/.vtcode/web_fetch_blocklist.json";
161const DEFAULT_WHITELIST_PATH: &str = "~/.vtcode/web_fetch_whitelist.json";
162const DEFAULT_AUDIT_LOG_PATH: &str = "~/.vtcode/web_fetch_audit.log";
163
164impl Default for WebFetchConfig {
165    fn default() -> Self {
166        Self {
167            mode: default_web_fetch_mode(),
168            dynamic_blocklist_enabled: false,
169            dynamic_blocklist_path: DEFAULT_BLOCKLIST_PATH.into(),
170            dynamic_whitelist_enabled: false,
171            dynamic_whitelist_path: DEFAULT_WHITELIST_PATH.into(),
172            blocked_domains: Vec::new(),
173            allowed_domains: Vec::new(),
174            blocked_patterns: Vec::new(),
175            enable_audit_logging: false,
176            audit_log_path: DEFAULT_AUDIT_LOG_PATH.into(),
177            strict_https_only: true,
178        }
179    }
180}
181
182impl Default for EditorToolConfig {
183    fn default() -> Self {
184        Self {
185            enabled: default_editor_enabled(),
186            preferred_editor: String::new(),
187            suspend_tui: default_editor_suspend_tui(),
188        }
189    }
190}
191
192/// Tool execution policy
193#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
194#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
195#[serde(rename_all = "lowercase")]
196pub enum ToolPolicy {
197    /// Allow execution without confirmation
198    Allow,
199    /// Prompt user for confirmation
200    Prompt,
201    /// Deny execution
202    Deny,
203}
204
205#[inline]
206const fn default_tool_policy() -> ToolPolicy {
207    ToolPolicy::Prompt
208}
209
210#[inline]
211const fn default_max_tool_loops() -> usize {
212    defaults::DEFAULT_MAX_TOOL_LOOPS
213}
214
215#[inline]
216const fn default_max_repeated_tool_calls() -> usize {
217    defaults::DEFAULT_MAX_REPEATED_TOOL_CALLS
218}
219
220#[inline]
221const fn default_max_consecutive_blocked_tool_calls_per_turn() -> usize {
222    defaults::DEFAULT_MAX_CONSECUTIVE_BLOCKED_TOOL_CALLS_PER_TURN
223}
224
225#[inline]
226const fn default_max_tool_rate_per_second() -> Option<usize> {
227    None
228}
229
230#[inline]
231const fn default_max_sequential_spool_chunk_reads() -> usize {
232    defaults::DEFAULT_MAX_SEQUENTIAL_SPOOL_CHUNK_READS_PER_TURN
233}
234
235#[inline]
236fn default_web_fetch_mode() -> String {
237    "restricted".into()
238}
239
240fn default_strict_https() -> bool {
241    true
242}
243
244#[inline]
245const fn default_editor_enabled() -> bool {
246    true
247}
248
249#[inline]
250const fn default_editor_suspend_tui() -> bool {
251    true
252}
253
254const DEFAULT_TOOL_POLICIES: &[(&str, ToolPolicy)] = &[
255    // Search operations (non-destructive)
256    (tools::UNIFIED_SEARCH, ToolPolicy::Allow),
257    // File operations (non-destructive)
258    (tools::READ_FILE, ToolPolicy::Allow),
259    // File operations (write/create)
260    (tools::WRITE_FILE, ToolPolicy::Allow),
261    (tools::EDIT_FILE, ToolPolicy::Allow),
262    (tools::CREATE_FILE, ToolPolicy::Allow),
263    // File operations (destructive - require confirmation)
264    (tools::DELETE_FILE, ToolPolicy::Prompt),
265    (tools::APPLY_PATCH, ToolPolicy::Prompt),
266    (tools::SEARCH_REPLACE, ToolPolicy::Prompt),
267    // PTY/Terminal operations
268    (tools::RUN_PTY_CMD, ToolPolicy::Prompt),
269    (tools::CREATE_PTY_SESSION, ToolPolicy::Allow),
270    (tools::READ_PTY_SESSION, ToolPolicy::Allow),
271    (tools::LIST_PTY_SESSIONS, ToolPolicy::Allow),
272    (tools::RESIZE_PTY_SESSION, ToolPolicy::Allow),
273    (tools::SEND_PTY_INPUT, ToolPolicy::Prompt),
274    (tools::CLOSE_PTY_SESSION, ToolPolicy::Allow),
275    // Code execution (requires confirmation)
276    (tools::EXECUTE_CODE, ToolPolicy::Prompt),
277    // Canonical execution interface
278    (tools::UNIFIED_EXEC, ToolPolicy::Prompt),
279];
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn editor_config_defaults_are_enabled() {
287        let config = ToolsConfig::default();
288        assert!(config.editor.enabled);
289        assert!(config.editor.preferred_editor.is_empty());
290        assert!(config.editor.suspend_tui);
291    }
292
293    #[test]
294    fn editor_config_deserializes_from_toml() {
295        let config: ToolsConfig = toml::from_str(
296            r#"
297default_policy = "prompt"
298
299[editor]
300enabled = false
301preferred_editor = "code --wait"
302suspend_tui = false
303"#,
304        )
305        .expect("tools config should parse");
306
307        assert!(!config.editor.enabled);
308        assert_eq!(config.editor.preferred_editor, "code --wait");
309        assert!(!config.editor.suspend_tui);
310    }
311}