use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use crate::constants::{defaults, tools};
use crate::core::plugins::PluginRuntimeConfig;
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolsConfig {
#[serde(default = "default_tool_policy")]
pub default_policy: ToolPolicy,
#[serde(default)]
#[cfg_attr(
feature = "schema",
schemars(with = "std::collections::BTreeMap<String, ToolPolicy>")
)]
pub policies: IndexMap<String, ToolPolicy>,
#[serde(default = "default_max_tool_loops")]
pub max_tool_loops: usize,
#[serde(default = "default_max_repeated_tool_calls")]
pub max_repeated_tool_calls: usize,
#[serde(default = "default_max_consecutive_blocked_tool_calls_per_turn")]
pub max_consecutive_blocked_tool_calls_per_turn: usize,
#[serde(default = "default_max_tool_rate_per_second")]
pub max_tool_rate_per_second: Option<usize>,
#[serde(default = "default_max_sequential_spool_chunk_reads")]
pub max_sequential_spool_chunk_reads: usize,
#[serde(default)]
pub web_fetch: WebFetchConfig,
#[serde(default)]
pub plugins: PluginRuntimeConfig,
#[serde(default)]
pub editor: EditorToolConfig,
#[serde(default)]
pub loop_thresholds: IndexMap<String, usize>,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EditorToolConfig {
#[serde(default = "default_editor_enabled")]
pub enabled: bool,
#[serde(default)]
pub preferred_editor: String,
#[serde(default = "default_editor_suspend_tui")]
pub suspend_tui: bool,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebFetchConfig {
#[serde(default = "default_web_fetch_mode")]
pub mode: String,
#[serde(default)]
pub dynamic_blocklist_enabled: bool,
#[serde(default)]
pub dynamic_blocklist_path: String,
#[serde(default)]
pub dynamic_whitelist_enabled: bool,
#[serde(default)]
pub dynamic_whitelist_path: String,
#[serde(default)]
pub blocked_domains: Vec<String>,
#[serde(default)]
pub allowed_domains: Vec<String>,
#[serde(default)]
pub blocked_patterns: Vec<String>,
#[serde(default)]
pub enable_audit_logging: bool,
#[serde(default)]
pub audit_log_path: String,
#[serde(default = "default_strict_https")]
pub strict_https_only: bool,
}
impl Default for ToolsConfig {
fn default() -> Self {
let policies = DEFAULT_TOOL_POLICIES
.iter()
.map(|(tool, policy)| ((*tool).into(), *policy))
.collect::<IndexMap<_, _>>();
Self {
default_policy: default_tool_policy(),
policies,
max_tool_loops: default_max_tool_loops(),
max_repeated_tool_calls: default_max_repeated_tool_calls(),
max_consecutive_blocked_tool_calls_per_turn:
default_max_consecutive_blocked_tool_calls_per_turn(),
max_tool_rate_per_second: default_max_tool_rate_per_second(),
max_sequential_spool_chunk_reads: default_max_sequential_spool_chunk_reads(),
web_fetch: WebFetchConfig::default(),
plugins: PluginRuntimeConfig::default(),
editor: EditorToolConfig::default(),
loop_thresholds: IndexMap::new(),
}
}
}
const DEFAULT_BLOCKLIST_PATH: &str = "~/.vtcode/web_fetch_blocklist.json";
const DEFAULT_WHITELIST_PATH: &str = "~/.vtcode/web_fetch_whitelist.json";
const DEFAULT_AUDIT_LOG_PATH: &str = "~/.vtcode/web_fetch_audit.log";
impl Default for WebFetchConfig {
fn default() -> Self {
Self {
mode: default_web_fetch_mode(),
dynamic_blocklist_enabled: false,
dynamic_blocklist_path: DEFAULT_BLOCKLIST_PATH.into(),
dynamic_whitelist_enabled: false,
dynamic_whitelist_path: DEFAULT_WHITELIST_PATH.into(),
blocked_domains: Vec::new(),
allowed_domains: Vec::new(),
blocked_patterns: Vec::new(),
enable_audit_logging: false,
audit_log_path: DEFAULT_AUDIT_LOG_PATH.into(),
strict_https_only: true,
}
}
}
impl Default for EditorToolConfig {
fn default() -> Self {
Self {
enabled: default_editor_enabled(),
preferred_editor: String::new(),
suspend_tui: default_editor_suspend_tui(),
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ToolPolicy {
Allow,
Prompt,
Deny,
}
#[inline]
const fn default_tool_policy() -> ToolPolicy {
ToolPolicy::Prompt
}
#[inline]
const fn default_max_tool_loops() -> usize {
defaults::DEFAULT_MAX_TOOL_LOOPS
}
#[inline]
const fn default_max_repeated_tool_calls() -> usize {
defaults::DEFAULT_MAX_REPEATED_TOOL_CALLS
}
#[inline]
const fn default_max_consecutive_blocked_tool_calls_per_turn() -> usize {
defaults::DEFAULT_MAX_CONSECUTIVE_BLOCKED_TOOL_CALLS_PER_TURN
}
#[inline]
const fn default_max_tool_rate_per_second() -> Option<usize> {
None
}
#[inline]
const fn default_max_sequential_spool_chunk_reads() -> usize {
defaults::DEFAULT_MAX_SEQUENTIAL_SPOOL_CHUNK_READS_PER_TURN
}
#[inline]
fn default_web_fetch_mode() -> String {
"restricted".into()
}
fn default_strict_https() -> bool {
true
}
#[inline]
const fn default_editor_enabled() -> bool {
true
}
#[inline]
const fn default_editor_suspend_tui() -> bool {
true
}
const DEFAULT_TOOL_POLICIES: &[(&str, ToolPolicy)] = &[
(tools::UNIFIED_SEARCH, ToolPolicy::Allow),
(tools::READ_FILE, ToolPolicy::Allow),
(tools::WRITE_FILE, ToolPolicy::Allow),
(tools::EDIT_FILE, ToolPolicy::Allow),
(tools::CREATE_FILE, ToolPolicy::Allow),
(tools::DELETE_FILE, ToolPolicy::Prompt),
(tools::APPLY_PATCH, ToolPolicy::Prompt),
(tools::SEARCH_REPLACE, ToolPolicy::Prompt),
(tools::UNIFIED_EXEC, ToolPolicy::Prompt),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn editor_config_defaults_are_enabled() {
let config = ToolsConfig::default();
assert!(config.editor.enabled);
assert!(config.editor.preferred_editor.is_empty());
assert!(config.editor.suspend_tui);
}
#[test]
fn default_tool_policies_only_seed_canonical_exec_surface() {
let config = ToolsConfig::default();
assert_eq!(
config.policies.get(tools::UNIFIED_EXEC),
Some(&ToolPolicy::Prompt)
);
for legacy_tool in [
tools::RUN_PTY_CMD,
tools::READ_PTY_SESSION,
tools::LIST_PTY_SESSIONS,
tools::SEND_PTY_INPUT,
tools::CLOSE_PTY_SESSION,
tools::EXECUTE_CODE,
] {
assert!(!config.policies.contains_key(legacy_tool));
}
}
#[test]
fn editor_config_deserializes_from_toml() {
let config: ToolsConfig = toml::from_str(
r#"
default_policy = "prompt"
[editor]
enabled = false
preferred_editor = "code --wait"
suspend_tui = false
"#,
)
.expect("tools config should parse");
assert!(!config.editor.enabled);
assert_eq!(config.editor.preferred_editor, "code --wait");
assert!(!config.editor.suspend_tui);
}
}