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