Skip to main content

vtcode_config/loader/
config.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::str::FromStr;
5
6use crate::acp::AgentClientProtocolConfig;
7use crate::codex::{FileOpener, HistoryConfig, TuiConfig};
8use crate::constants::defaults::DEFAULT_PRIMARY_AGENT_NAME;
9use crate::context::ContextFeaturesConfig;
10use crate::core::{
11    AgentConfig, AgentPermissionsConfig, AnthropicConfig, AuthConfig, AutomationConfig, CommandsConfig,
12    CustomProviderConfig, DesktopConfig, DotfileProtectionConfig, ModelConfig, OpenAIConfig, OpenRouterConfig,
13    PermissionsConfig, PromptCachingConfig, ProviderOverrideConfig, SandboxConfig, SecurityConfig, SkillsConfig,
14    ToolsConfig,
15};
16use crate::debug::DebugConfig;
17use crate::hooks::HooksConfig;
18use crate::mcp::McpClientConfig;
19use crate::models::{MiMoAuthMethod, Provider};
20use crate::optimization::OptimizationConfig;
21use crate::output_styles::OutputStyleConfig;
22use crate::root::{ChatConfig, PtyConfig, UiConfig};
23use crate::subagents::SubagentRuntimeLimits;
24use crate::telemetry::TelemetryConfig;
25use crate::timeouts::TimeoutsConfig;
26use crate::webmcp::WebmcpConfig;
27
28use crate::loader::syntax_highlighting::SyntaxHighlightingConfig;
29
30/// Provider-specific configuration
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32#[derive(Debug, Clone, Deserialize, Serialize, Default)]
33pub struct ProviderConfig {
34    /// OpenAI provider configuration
35    #[serde(default)]
36    pub openai: OpenAIConfig,
37
38    /// Anthropic provider configuration
39    #[serde(default)]
40    pub anthropic: AnthropicConfig,
41
42    /// OpenRouter provider configuration
43    #[serde(default)]
44    pub openrouter: OpenRouterConfig,
45
46    /// Xiaomi MiMo auth method: "payg" or "token-plan"
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub mimo_auth_method: Option<MiMoAuthMethod>,
49}
50
51/// Codex-compatible top-level feature flags.
52///
53/// Maps to `[features]` in `vtcode.toml`.
54/// When `memories` is true, VT Code can carry useful context from earlier
55/// threads into future work via the persistent memory subsystem.
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
58pub struct FeaturesConfig {
59    /// Master toggle for the memories subsystem.
60    /// When true, VT Code extracts durable context from completed threads
61    /// and injects it into future sessions.
62    #[serde(default)]
63    pub memories: bool,
64}
65
66/// Workspace-level configuration controls.
67///
68/// Maps to `[workspace]` in `vtcode.toml`.
69/// When `use_root_config` is true, only the workspace root `vtcode.toml`
70/// is used as the active config layer (system, user, project, and
71/// dot-dir layers are discarded).
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
74pub struct WorkspaceConfig {
75    /// When true, force the workspace root `vtcode.toml` as the sole
76    /// active config layer, discarding system, user, project, and
77    /// dot-dir layers.
78    #[serde(default)]
79    pub(crate) use_root_config: bool,
80
81    /// Include workspace context in messages.
82    #[serde(default = "default_true")]
83    pub(crate) include_context: bool,
84
85    /// Maximum size of workspace context to include (in bytes).
86    #[serde(default)]
87    pub(crate) max_context_size: Option<usize>,
88}
89
90impl Default for WorkspaceConfig {
91    fn default() -> Self {
92        Self {
93            use_root_config: false,
94            include_context: true,
95            max_context_size: None,
96        }
97    }
98}
99
100fn default_true() -> bool {
101    true
102}
103
104/// Main configuration structure for VT Code
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106#[derive(Debug, Clone, Deserialize, Serialize)]
107pub struct VTCodeConfig {
108    /// Primary agent selected at startup when no session override is active.
109    #[serde(default = "default_primary_agent")]
110    pub default_primary_agent: String,
111
112    /// Codex-compatible top-level feature flags (`[features]` table).
113    #[serde(default)]
114    pub features: FeaturesConfig,
115
116    /// Codex-compatible clickable citation URI scheme.
117    #[serde(default)]
118    pub file_opener: FileOpener,
119
120    /// External notification command invoked for supported events.
121    #[serde(default)]
122    pub notify: Vec<String>,
123
124    /// Codex-compatible local history persistence controls.
125    #[serde(default)]
126    pub history: HistoryConfig,
127
128    /// Codex-compatible TUI settings.
129    #[serde(default)]
130    pub tui: TuiConfig,
131
132    /// Native macOS desktop app settings.
133    #[serde(default)]
134    pub desktop: DesktopConfig,
135
136    /// Agent-wide settings
137    #[serde(default)]
138    pub agent: AgentConfig,
139
140    /// Authentication configuration for OAuth flows
141    #[serde(default)]
142    pub auth: AuthConfig,
143
144    /// Tool execution policies
145    #[serde(default)]
146    pub tools: ToolsConfig,
147
148    /// Unix command permissions
149    #[serde(default)]
150    pub commands: CommandsConfig,
151
152    /// Permission system settings (resolution, audit logging, caching)
153    #[serde(default)]
154    pub permissions: PermissionsConfig,
155
156    /// Runtime-only agent permission policy supplied by derived child agents.
157    #[serde(skip)]
158    #[cfg_attr(feature = "schema", schemars(skip))]
159    pub runtime_agent_permissions: Option<AgentPermissionsConfig>,
160
161    /// Runtime-only snapshot of lifecycle hook commands sourced from
162    /// workspace-controlled configuration layers, populated by `ConfigManager`
163    /// at load time. Workspace-controlled hooks require explicit user approval
164    /// before execution; see [`WorkspaceLifecycleHooks`](crate::hooks::WorkspaceLifecycleHooks).
165    #[serde(skip)]
166    #[cfg_attr(feature = "schema", schemars(skip))]
167    pub workspace_lifecycle_hooks: Option<crate::hooks::WorkspaceLifecycleHooks>,
168
169    /// Security settings
170    #[serde(default)]
171    pub security: SecurityConfig,
172
173    /// Sandbox settings for command execution isolation
174    #[serde(default)]
175    pub sandbox: SandboxConfig,
176
177    /// UI settings
178    #[serde(default)]
179    pub ui: UiConfig,
180
181    /// Chat settings
182    #[serde(default)]
183    pub chat: ChatConfig,
184
185    /// PTY settings
186    #[serde(default)]
187    pub pty: PtyConfig,
188
189    /// Debug and tracing settings
190    #[serde(default)]
191    pub debug: DebugConfig,
192
193    /// Context features (e.g., Decision Ledger)
194    #[serde(default)]
195    pub context: ContextFeaturesConfig,
196
197    /// Telemetry configuration (logging, trajectory)
198    #[serde(default)]
199    pub telemetry: TelemetryConfig,
200
201    /// Performance optimization settings
202    #[serde(default)]
203    pub optimization: OptimizationConfig,
204
205    /// Syntax highlighting configuration
206    #[serde(default)]
207    pub syntax_highlighting: SyntaxHighlightingConfig,
208
209    /// Timeout ceilings and UI warning thresholds
210    #[serde(default)]
211    pub timeouts: TimeoutsConfig,
212
213    /// Automation configuration
214    #[serde(default)]
215    pub automation: AutomationConfig,
216
217    /// Subagent runtime configuration
218    #[serde(default)]
219    pub subagents: SubagentRuntimeLimits,
220
221    /// Prompt cache configuration (local + provider integration)
222    #[serde(default)]
223    pub prompt_cache: PromptCachingConfig,
224
225    /// Model Context Protocol configuration
226    #[serde(default)]
227    pub mcp: McpClientConfig,
228
229    /// Authenticated browser editor bridge configuration
230    #[serde(default)]
231    pub webmcp: WebmcpConfig,
232
233    /// Agent Client Protocol configuration
234    #[serde(default)]
235    pub acp: AgentClientProtocolConfig,
236
237    /// Lifecycle hooks configuration
238    #[serde(default)]
239    pub hooks: HooksConfig,
240
241    /// Model-specific behavior configuration
242    #[serde(default)]
243    pub model: ModelConfig,
244
245    /// Provider-specific configuration
246    #[serde(default)]
247    pub provider: ProviderConfig,
248
249    /// Skills system configuration (Agent Skills spec)
250    #[serde(default)]
251    pub skills: SkillsConfig,
252
253    /// Extra OpenAI-compatible endpoints for the model picker.
254    /// Define in user or system config only.
255    #[serde(default)]
256    pub custom_providers: Vec<CustomProviderConfig>,
257
258    /// Built-in provider overrides for model lists and endpoint configuration.
259    ///
260    /// Maps provider key (e.g., "opencode-zen", "opencode-go") to override
261    /// config that extends the provider's hardcoded model list with custom
262    /// models, and optionally overrides the base URL or API key env var.
263    #[serde(default)]
264    pub provider_overrides: BTreeMap<String, ProviderOverrideConfig>,
265
266    /// Restrict which providers may be used.
267    ///
268    /// When non-empty, only providers listed here are visible in the model
269    /// picker, selectable at first-run, and instantiable at runtime.  Empty
270    /// (the default) means all built-in and custom providers are available.
271    #[serde(default)]
272    pub providers_whitelist: Vec<String>,
273
274    /// Output style configuration
275    #[serde(default)]
276    pub output_style: OutputStyleConfig,
277
278    /// Dotfile protection configuration
279    #[serde(default)]
280    pub dotfile_protection: DotfileProtectionConfig,
281
282    /// Workspace-level configuration controls
283    #[serde(default)]
284    pub workspace: WorkspaceConfig,
285}
286
287impl Default for VTCodeConfig {
288    fn default() -> Self {
289        Self {
290            default_primary_agent: default_primary_agent(),
291            features: FeaturesConfig::default(),
292            file_opener: FileOpener::default(),
293            notify: Vec::new(),
294            history: HistoryConfig::default(),
295            tui: TuiConfig::default(),
296            desktop: DesktopConfig::default(),
297            agent: AgentConfig::default(),
298            auth: AuthConfig::default(),
299            tools: ToolsConfig::default(),
300            commands: CommandsConfig::default(),
301            permissions: PermissionsConfig::default(),
302            runtime_agent_permissions: None,
303            workspace_lifecycle_hooks: None,
304            security: SecurityConfig::default(),
305            sandbox: SandboxConfig::default(),
306            ui: UiConfig::default(),
307            chat: ChatConfig::default(),
308            pty: PtyConfig::default(),
309            debug: DebugConfig::default(),
310            context: ContextFeaturesConfig::default(),
311            telemetry: TelemetryConfig::default(),
312            optimization: OptimizationConfig::default(),
313            syntax_highlighting: SyntaxHighlightingConfig::default(),
314            timeouts: TimeoutsConfig::default(),
315            automation: AutomationConfig::default(),
316            subagents: SubagentRuntimeLimits::default(),
317            prompt_cache: PromptCachingConfig::default(),
318            mcp: McpClientConfig::default(),
319            webmcp: WebmcpConfig::default(),
320            acp: AgentClientProtocolConfig::default(),
321            hooks: HooksConfig::default(),
322            model: ModelConfig::default(),
323            provider: ProviderConfig::default(),
324            skills: SkillsConfig::default(),
325            custom_providers: Vec::new(),
326            provider_overrides: BTreeMap::new(),
327            providers_whitelist: Vec::new(),
328            output_style: OutputStyleConfig::default(),
329            dotfile_protection: DotfileProtectionConfig::default(),
330            workspace: WorkspaceConfig::default(),
331        }
332    }
333}
334
335impl VTCodeConfig {
336    pub fn validate(&self) -> Result<()> {
337        self.syntax_highlighting
338            .validate()
339            .context("Invalid syntax_highlighting configuration")?;
340
341        self.context.validate().context("Invalid context configuration")?;
342
343        self.hooks.validate().context("Invalid hooks configuration")?;
344
345        self.timeouts.validate().context("Invalid timeouts configuration")?;
346
347        self.prompt_cache.validate().context("Invalid prompt_cache configuration")?;
348
349        self.webmcp.validate().context("Invalid webmcp configuration")?;
350
351        self.agent
352            .validate_llm_params()
353            .map_err(anyhow::Error::msg)
354            .context("Invalid agent configuration")?;
355
356        self.ui
357            .keyboard_protocol
358            .validate()
359            .context("Invalid keyboard_protocol configuration")?;
360
361        self.pty.validate().context("Invalid pty configuration")?;
362
363        // Validate custom providers
364        let mut seen_names = std::collections::HashSet::new();
365        for cp in &self.custom_providers {
366            cp.validate()
367                .map_err(|msg| anyhow::anyhow!(msg))
368                .context("Invalid custom_providers configuration")?;
369            if !seen_names.insert(cp.name.to_lowercase()) {
370                anyhow::bail!("custom_providers: duplicate name `{}`", cp.name);
371            }
372        }
373
374        // Validate provider overrides
375        for (provider_name, override_config) in &self.provider_overrides {
376            override_config
377                .validate(provider_name)
378                .map_err(|msg| anyhow::anyhow!(msg))
379                .context("Invalid provider_overrides configuration")?;
380            // Validate that the provider key matches a known provider
381            if Provider::from_str(provider_name).is_err() {
382                anyhow::bail!(
383                    "provider_overrides: unknown provider `{provider_name}`; \
384                     must be one of: gemini, openai, anthropic, copilot, deepseek, meta, \
385                     openrouter, ollama, lmstudio, llamacpp, moonshot, zai, minimax, \
386                     mimo, mistral, huggingface, opencodezen, opencodego, qwen, \
387                     stepfun, evolink, poolside, nvidia, merge-gateway, vercel"
388                );
389            }
390        }
391
392        // Validate providers_whitelist entries
393        for entry in &self.providers_whitelist {
394            let known = Provider::from_str(entry).is_ok() || self.custom_providers.iter().any(|cp| cp.name == *entry);
395            if !known {
396                anyhow::bail!(
397                    "providers_whitelist: unknown provider `{entry}`; \
398                     must be a built-in provider or a name from custom_providers"
399                );
400            }
401        }
402
403        Ok(())
404    }
405
406    /// Returns true when the persistent memory subsystem is enabled.
407    ///
408    /// The top-level `features.memories` flag is the global master switch.
409    /// `agent.persistent_memory.enabled` then enables repository-scoped storage.
410    #[must_use]
411    pub fn persistent_memory_enabled(&self) -> bool {
412        self.features.memories && self.agent.persistent_memory.enabled
413    }
414
415    /// Returns true when the memories subsystem is enabled for injection.
416    ///
417    /// Memories are active when the subsystem is enabled and `memories.use_memories`
418    /// is true.
419    #[must_use]
420    pub fn memories_enabled(&self) -> bool {
421        self.persistent_memory_enabled() && self.agent.persistent_memory.memories.use_memories
422    }
423
424    /// Returns true when completed threads should be stored as memory inputs.
425    #[must_use]
426    pub fn should_generate_memories(&self) -> bool {
427        self.persistent_memory_enabled() && self.agent.persistent_memory.memories.generate_memories
428    }
429
430    /// Look up a custom provider by its stable key.
431    pub fn custom_provider(&self, name: &str) -> Option<&CustomProviderConfig> {
432        let lower = name.to_lowercase();
433        self.custom_providers.iter().find(|cp| cp.name.to_lowercase() == lower)
434    }
435
436    /// Return the explicitly configured credential key for a provider.
437    ///
438    /// Custom-provider identities take precedence over built-in provider
439    /// overrides. A missing result means callers should use the provider's
440    /// built-in default or the agent-wide fallback.
441    pub fn configured_api_key_env(&self, name: &str) -> Option<String> {
442        if let Some(custom_provider) = self.custom_provider(name) {
443            return Some(custom_provider.resolved_api_key_env());
444        }
445
446        self.provider_overrides
447            .iter()
448            .find(|(provider, _)| provider.eq_ignore_ascii_case(name))
449            .and_then(|(_, provider_override)| provider_override.api_key_env.clone())
450            .filter(|api_key_env| !api_key_env.trim().is_empty())
451    }
452
453    /// Get the display name for any provider key, falling back to the raw key
454    /// if no custom provider matches.
455    pub fn provider_display_name(&self, provider_key: &str) -> String {
456        if let Some(cp) = self.custom_provider(provider_key) {
457            cp.display_name.clone()
458        } else if provider_key.eq_ignore_ascii_case("codex") {
459            "Codex".to_string()
460        } else if let Ok(p) = FromStr::from_str(provider_key) {
461            let p: Provider = p;
462            p.label().to_string()
463        } else {
464            provider_key.to_string()
465        }
466    }
467}
468
469fn default_primary_agent() -> String {
470    DEFAULT_PRIMARY_AGENT_NAME.to_string()
471}