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