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