1mod custom_provider_config;
2mod hooks;
3mod paths;
4pub(crate) mod release_notes;
5mod settings;
6mod settings_storage;
7
8use crate::thinking::ThinkingLevel;
9pub(crate) use settings::DEFAULT_TUI_SUBAGENT_CARD_ROWS;
10pub(crate) use settings::{HeaderPanel, PanelLayoutSettings, set_panel_layout};
11use std::{collections::BTreeMap, env, fmt, io::IsTerminal};
12
13use crate::auth::{
14 AuthState, ProviderCredential, codex_credential_from_store, read_auth,
15 resolve_provider_credential,
16};
17pub use custom_provider_config::{
18 CustomProviderConfig, CustomProviderFastMode, CustomProviderHeaderValue,
19 CustomReasoningProtocol,
20};
21pub(crate) use custom_provider_config::{
22 derive_custom_provider_id, looks_like_secret_value, make_custom_provider_config,
23 normalize_custom_provider_base_url, normalized_extra_models, validate_custom_provider_id,
24 validate_optional_env_var_name,
25};
26pub use hooks::{
27 HookDefinition, HookFailurePolicy, HookPayloadMode, HookSettings, InjectedContentSettings,
28 InjectedContentStyle,
29};
30pub use paths::McPaths;
31#[cfg(test)]
32pub(crate) use settings::ensure_settings_schema_files;
33#[cfg(test)]
34pub(crate) use settings::load_config_with_settings;
35pub(crate) use settings::set_selected_model;
36pub use settings::{
37 AnthropicCacheTtl, AstGrepToolSettings, AutoCompactionSettings, BashProtectionFailurePolicy,
38 BashProtectionLevel, BashProtectionSettings, BashToolSettings, CodeModeToolSettings,
39 CompactionSettings, CompactionTimingSettings, CompletionVerificationFailurePolicy,
40 CompletionVerificationSettings, FastSettings, FindToolSettings, GrepToolSettings,
41 HashEditToolSettings, HerdrSettings, HumanizeProtectionFailurePolicy,
42 HumanizeProtectionSettings, InstructionsSettings, IntegrationsSettings, JevSettings,
43 ListFilesToolSettings, LspServerConfig, LspServersSettings, LspSettings, McpHttpServerConfig,
44 McpOAuthConfig, McpServerConfig, McpServersSettings, McpStdioServerConfig, ModelsSettings,
45 OpenAiCodexSettings, OpenAiResponsesSettings, PromptInjectionFailurePolicy,
46 PromptInjectionProtectionLevel, PromptInjectionProtectionSettings, ProviderStreamSettings,
47 ReadToolSettings, SelectedModelSettings, SessionTitleSettings, Settings, SideAgentSettings,
48 SkillSuggestionSettings, SkillsSettings, SubagentsSettings, SubagentsToolSettings,
49 SummarizerSettings, TextVerbosity, ToolSettings, TuiSettings, ViewImageToolSettings,
50 ViewImageVisionModelSettings, WriteToolSettings,
51};
52pub(crate) use settings::{AppearanceSettings, AutoCompactionLimit, set_appearance_theme};
53pub(crate) use settings::{
54 CompactionConfig, SessionTitleConfig, clamp_subagent_max_depth, validate_mcp_http_url_field,
55 validate_mcp_server_name, validate_view_image_identifier, validate_view_image_max_image_bytes,
56};
57pub(crate) use settings::{
58 DEFAULT_MCP_TIMEOUT_SECONDS, DEFAULT_SESSION_RETENTION_DAYS, SettingsListKind, SettingsScope,
59};
60pub(crate) use settings::{DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES, MAX_VIEW_IMAGE_MAX_IMAGE_BYTES};
61pub(crate) use settings::{
62 disabled_skill_names_from_settings, disabled_subagent_profile_names_from_settings,
63 disabled_tool_names_from_settings, enabled_model_ids_from_settings,
64 load_startup_config_with_settings,
65};
66pub(crate) use settings::{
67 fast_mode_enabled, list_names_for_modal_scope, read_settings, remove_custom_provider,
68 set_fast_mode, set_mcp_server_enabled, set_model_enabled_for_scope, set_selected_primary_agent,
69 set_skill_disabled_for_scope, set_subagent_profile_disabled, set_tool_disabled,
70 toggle_fast_mode, upsert_custom_provider,
71};
72pub(crate) use settings::{load_settings_editor, save_settings_editor, set_code_mode_tool_enabled};
73
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
75pub(crate) enum ColorChoice {
76 #[default]
77 Auto,
78 Always,
79 Never,
80}
81
82#[derive(Debug, Clone, Default, PartialEq, Eq)]
83pub(crate) struct CliConfigOverrides {
84 pub(crate) provider: Option<String>,
85 pub(crate) model: Option<String>,
86 pub(crate) api_key: Option<String>,
87 pub(crate) color: Option<ColorChoice>,
88}
89
90#[derive(Clone, PartialEq, Eq)]
91pub(crate) struct EffectiveConfig {
92 pub(crate) provider: Option<String>,
93 pub(crate) model: Option<String>,
94 pub(crate) no_color: bool,
95 pub(crate) file_autocomplete_respects_gitignore: bool,
96 pub(crate) custom_providers: BTreeMap<String, CustomProviderConfig>,
97 pub(crate) thinking_level: ThinkingLevel,
98 pub(crate) auth: Option<ProviderCredential>,
99 pub(crate) paths: McPaths,
100}
101
102impl fmt::Debug for EffectiveConfig {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.debug_struct("EffectiveConfig")
105 .field("provider", &self.provider)
106 .field("model", &self.model)
107 .field("no_color", &self.no_color)
108 .field(
109 "file_autocomplete_respects_gitignore",
110 &self.file_autocomplete_respects_gitignore,
111 )
112 .field("custom_providers", &self.custom_providers)
113 .field("auth", &self.auth)
114 .field("thinking_level", &self.thinking_level)
115 .field("paths", &self.paths)
116 .finish()
117 }
118}
119
120impl EffectiveConfig {
121 pub(crate) fn selected_provider_model(settings: &Settings) -> (Option<String>, Option<String>) {
122 (
123 env::var("MC_PROVIDER")
124 .ok()
125 .or_else(|| settings.selected_model.provider.clone()),
126 env::var("MC_MODEL")
127 .ok()
128 .or_else(|| settings.selected_model.model.clone()),
129 )
130 }
131
132 pub(crate) fn from_loaded_settings(
133 paths: McPaths,
134 cli: CliConfigOverrides,
135 settings: Settings,
136 ) -> anyhow::Result<Self> {
137 let (color_enabled, _) = resolve_output_style(&settings, cli.color);
138 let (provider, model) = Self::selected_provider_model(&settings);
139 let provider = cli.provider.or(provider);
140 let model = cli.model.or(model);
141 let provider_id = provider
142 .as_deref()
143 .unwrap_or(crate::providers::OPENAI_CODEX_PROVIDER);
144 reject_retired_provider_selection(provider_id, model.as_deref().unwrap_or(""))?;
145 let auth_file = read_auth(&paths)?;
146 let auth = resolve_provider_credential(
147 provider_id,
148 &auth_file,
149 cli.api_key,
150 &settings.custom_providers,
151 )?;
152 Ok(Self {
153 provider,
154 model,
155 no_color: !color_enabled,
156 file_autocomplete_respects_gitignore: settings.file_autocomplete_respects_gitignore,
157 custom_providers: settings.custom_providers,
158 thinking_level: settings.selected_model.thinking_level.unwrap_or_default(),
159 auth,
160 paths,
161 })
162 }
163
164 pub(crate) fn auth_state(&self) -> AuthState {
165 AuthState::for_provider_with_custom(
166 self.provider_id(),
167 self.auth.as_ref(),
168 &self.custom_providers,
169 )
170 }
171
172 pub(crate) fn require_auth(&self) -> Result<ProviderCredential, ConfigError> {
173 self.auth_state()
174 .credential()
175 .cloned()
176 .ok_or_else(|| self.missing_auth_error())
177 }
178
179 pub(crate) fn resolve_provider_auth_for_runtime(&self) -> anyhow::Result<ProviderCredential> {
180 self.resolve_provider_auth_for_runtime_with(codex_credential_from_store)
181 }
182
183 fn resolve_provider_auth_for_runtime_with(
184 &self,
185 prepare_codex_auth: impl FnOnce(&McPaths) -> anyhow::Result<ProviderCredential>,
186 ) -> anyhow::Result<ProviderCredential> {
187 if self.provider_id() == crate::providers::ANTHROPIC_PROVIDER {
188 return resolve_provider_credential(
190 self.provider_id(),
191 &read_auth(&self.paths)?,
192 None,
193 &self.custom_providers,
194 )?
195 .ok_or_else(|| self.missing_auth_error().into());
196 }
197
198 if self.provider_id() == crate::providers::CLAUDE_SUBSCRIPTION_PROVIDER {
199 return resolve_provider_credential(
200 self.provider_id(),
201 &read_auth(&self.paths)?,
202 None,
203 &self.custom_providers,
204 )?
205 .ok_or_else(|| self.missing_auth_error().into());
206 }
207 if self.provider_id() != crate::providers::OPENAI_CODEX_PROVIDER {
208 return self.require_auth().map_err(Into::into);
209 }
210
211 prepare_codex_auth(&self.paths)
214 }
215
216 pub(crate) fn missing_auth_error(&self) -> ConfigError {
217 ConfigError::missing_auth_for_custom_providers(
218 self.provider_id(),
219 &self.custom_providers,
220 &self.paths.auth_file,
221 )
222 }
223
224 pub(crate) fn provider_id(&self) -> &str {
225 self.provider
226 .as_deref()
227 .unwrap_or(crate::providers::OPENAI_CODEX_PROVIDER)
228 }
229}
230
231pub(crate) fn reject_retired_provider_selection(provider: &str, model: &str) -> anyhow::Result<()> {
232 if provider != "claude-code" {
233 return Ok(());
234 }
235 let model = if model.trim().is_empty() {
236 "<model>"
237 } else {
238 model
239 };
240 anyhow::bail!(
241 "stale provider selection 'claude-code/{model}': the Claude Code provider was removed; select 'anthropic/{model}' instead, then set ANTHROPIC_API_KEY or configure a provider-keyed 'anthropic' API key; Claude Code OAuth/subscription credentials are not reused"
242 );
243}
244
245pub(crate) fn load_effective_provider_selection(
246 paths: &McPaths,
247 provider: &str,
248 model: &str,
249) -> anyhow::Result<EffectiveConfig> {
250 load_effective_provider_selection_with_settings(paths, provider, model, &read_settings(paths)?)
251}
252
253pub(crate) fn load_effective_provider_selection_with_settings(
254 paths: &McPaths,
255 provider: &str,
256 model: &str,
257 settings: &Settings,
258) -> anyhow::Result<EffectiveConfig> {
259 reject_retired_provider_selection(provider, model)?;
260 let auth_file = read_auth(paths)?;
261 let auth = resolve_provider_credential(provider, &auth_file, None, &settings.custom_providers)?;
262 let (color_enabled, _) = resolve_output_style(settings, None);
263 Ok(EffectiveConfig {
264 provider: Some(provider.to_string()),
265 model: Some(model.to_string()),
266 no_color: !color_enabled,
267 file_autocomplete_respects_gitignore: settings.file_autocomplete_respects_gitignore,
268 custom_providers: settings.custom_providers.clone(),
269 thinking_level: settings.selected_model.thinking_level.unwrap_or_default(),
270 auth,
271 paths: paths.clone(),
272 })
273}
274
275impl ConfigError {
276 pub(crate) fn missing_auth_for_custom_providers(
277 provider: &str,
278 custom_providers: &BTreeMap<String, CustomProviderConfig>,
279 auth_file: &std::path::Path,
280 ) -> Self {
281 missing_auth_error(provider, custom_providers.get(provider), auth_file)
282 }
283}
284
285fn missing_auth_error(
286 provider: &str,
287 custom: Option<&CustomProviderConfig>,
288 auth_file: &std::path::Path,
289) -> ConfigError {
290 let auth_path = auth_file.display();
291 let message = if let Some(custom) = custom {
292 match &custom.api_key_env_var {
293 Some(env_var) => format!(
294 "missing auth: custom provider '{provider}' is configured but environment variable {env_var} is missing or empty"
295 ),
296 None => format!(
297 "missing auth: custom provider '{provider}' is configured for no-auth but could not be prepared"
298 ),
299 }
300 } else if provider == crate::providers::CLAUDE_SUBSCRIPTION_PROVIDER {
301 "Claude subscription unavailable: on Unix, install Claude Code and run `claude auth login`; API keys are not used".to_string()
302 } else if provider == crate::providers::OPENAI_CODEX_PROVIDER {
303 format!(
304 "missing auth: missing OAuth auth or expired OAuth credentials without refresh for provider 'openai-codex'; needs re-login with /login openai-codex; --api-key, MC_API_KEY, and OPENAI_API_KEY are unsupported for openai-codex; OAuth auth includes access token and accountId in {auth_path}"
305 )
306 } else if provider == crate::providers::ANTHROPIC_PROVIDER {
307 format!(
308 "missing auth: provider 'anthropic' requires an Anthropic API key; set ANTHROPIC_API_KEY or configure provider-keyed API-key auth in {auth_path}; OPENAI_API_KEY, MC_API_KEY, and --api-key are not used for Anthropic"
309 )
310 } else {
311 format!(
312 "missing auth for provider '{provider}'; configure provider-keyed auth in {auth_path}"
313 )
314 };
315 ConfigError::MissingAuth {
316 provider: provider.to_string(),
317 message,
318 }
319}
320
321fn resolve_output_style(settings: &Settings, cli_color: Option<ColorChoice>) -> (bool, bool) {
322 let stdout_is_tty = std::io::stdout().is_terminal();
323 resolve_output_style_for_stdout(settings, cli_color, stdout_is_tty)
324}
325
326pub(crate) fn resolve_output_style_for_stdout(
327 settings: &Settings,
328 cli_color: Option<ColorChoice>,
329 stdout_is_tty: bool,
330) -> (bool, bool) {
331 let policy = crate::appearance::resolve_color_policy_from_env(
332 settings.no_color,
333 cli_color,
334 stdout_is_tty,
335 env::var_os("NO_COLOR").is_some(),
336 env::var("COLORTERM").ok().as_deref(),
337 env::var("TERM").ok().as_deref(),
338 );
339 (policy.color_enabled, policy.unicode_enabled)
340}
341#[derive(Debug, thiserror::Error, PartialEq, Eq)]
342pub(crate) enum ConfigError {
343 #[error("{message}")]
344 MissingAuth { provider: String, message: String },
345}
346
347pub(crate) fn load_context_budget(
348 config: &EffectiveConfig,
349) -> anyhow::Result<crate::context::ContextBudget> {
350 let settings = read_settings(&config.paths)?;
351 let mut budget = settings.context.unwrap_or_default();
352 let provider = config.provider_id();
353 let model = config
354 .model
355 .as_deref()
356 .unwrap_or_else(|| crate::providers::default_model_for_provider(provider));
357 if let Some(context_window) =
358 crate::model_catalog::cached_model_context_window(&config.paths, provider, model)
359 {
360 budget.max_tokens = context_window;
361 }
362 budget.apply_model_limits(provider, model);
363 Ok(budget)
364}
365#[cfg(test)]
366pub(crate) use settings::write_settings;