1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3
4use crate::constants::{defaults, tools};
5use crate::core::plugins::PluginRuntimeConfig;
6
7#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct ToolsConfig {
11 #[serde(default = "default_tool_policy")]
13 pub default_policy: ToolPolicy,
14
15 #[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 #[serde(default = "default_max_tool_loops")]
30 pub max_tool_loops: usize,
31
32 #[serde(default = "default_max_repeated_tool_calls")]
35 pub max_repeated_tool_calls: usize,
36
37 #[serde(default = "default_max_consecutive_blocked_tool_calls_per_turn")]
40 pub max_consecutive_blocked_tool_calls_per_turn: usize,
41
42 #[serde(default = "default_max_tool_rate_per_second")]
45 pub max_tool_rate_per_second: Option<usize>,
46
47 #[serde(default = "default_max_sequential_spool_chunk_reads")]
50 pub max_sequential_spool_chunk_reads: usize,
51
52 #[serde(default)]
54 pub web_fetch: WebFetchConfig,
55
56 #[serde(default)]
58 pub plugins: PluginRuntimeConfig,
59
60 #[serde(default)]
62 pub editor: EditorToolConfig,
63
64 #[serde(default)]
68 pub loop_thresholds: IndexMap<String, usize>,
69}
70
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct EditorToolConfig {
75 #[serde(default = "default_editor_enabled")]
77 pub enabled: bool,
78
79 #[serde(default)]
81 pub preferred_editor: String,
82
83 #[serde(default = "default_editor_suspend_tui")]
85 pub suspend_tui: bool,
86}
87
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[derive(Debug, Clone, Deserialize, Serialize)]
91pub struct WebFetchConfig {
92 #[serde(default = "default_web_fetch_mode")]
94 pub mode: String,
95
96 #[serde(default)]
98 pub dynamic_blocklist_enabled: bool,
99
100 #[serde(default)]
102 pub dynamic_blocklist_path: String,
103
104 #[serde(default)]
106 pub dynamic_whitelist_enabled: bool,
107
108 #[serde(default)]
110 pub dynamic_whitelist_path: String,
111
112 #[serde(default)]
114 pub blocked_domains: Vec<String>,
115
116 #[serde(default)]
118 pub allowed_domains: Vec<String>,
119
120 #[serde(default)]
122 pub blocked_patterns: Vec<String>,
123
124 #[serde(default)]
126 pub enable_audit_logging: bool,
127
128 #[serde(default)]
130 pub audit_log_path: String,
131
132 #[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#[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,
199 Prompt,
201 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 (tools::UNIFIED_SEARCH, ToolPolicy::Allow),
257 (tools::READ_FILE, ToolPolicy::Allow),
259 (tools::WRITE_FILE, ToolPolicy::Allow),
261 (tools::EDIT_FILE, ToolPolicy::Allow),
262 (tools::CREATE_FILE, ToolPolicy::Allow),
263 (tools::DELETE_FILE, ToolPolicy::Prompt),
265 (tools::APPLY_PATCH, ToolPolicy::Prompt),
266 (tools::SEARCH_REPLACE, ToolPolicy::Prompt),
267 (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 (tools::EXECUTE_CODE, ToolPolicy::Prompt),
277 (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}