mod custom_provider_config;
mod hooks;
mod paths;
pub(crate) mod release_notes;
mod settings;
mod settings_storage;
use crate::thinking::ThinkingLevel;
pub(crate) use settings::DEFAULT_TUI_SUBAGENT_CARD_ROWS;
pub(crate) use settings::{HeaderPanel, PanelLayoutSettings, set_panel_layout};
use std::{collections::BTreeMap, env, fmt, io::IsTerminal};
use crate::auth::{
AuthState, ProviderCredential, codex_credential_from_store, read_auth,
resolve_provider_credential,
};
pub use custom_provider_config::{
CustomProviderConfig, CustomProviderFastMode, CustomProviderHeaderValue,
CustomReasoningProtocol,
};
pub(crate) use custom_provider_config::{
derive_custom_provider_id, looks_like_secret_value, make_custom_provider_config,
normalize_custom_provider_base_url, normalized_extra_models, validate_custom_provider_id,
validate_optional_env_var_name,
};
pub use hooks::{
HookDefinition, HookFailurePolicy, HookPayloadMode, HookSettings, InjectedContentSettings,
InjectedContentStyle,
};
pub use paths::McPaths;
#[cfg(test)]
pub(crate) use settings::ensure_settings_schema_files;
#[cfg(test)]
pub(crate) use settings::load_config_with_settings;
pub(crate) use settings::set_selected_model;
pub use settings::{
AnthropicCacheTtl, AstGrepToolSettings, AutoCompactionSettings, BashProtectionFailurePolicy,
BashProtectionLevel, BashProtectionSettings, BashToolSettings, CodeModeToolSettings,
CompactionSettings, CompactionTimingSettings, CompletionVerificationFailurePolicy,
CompletionVerificationSettings, FastSettings, FindToolSettings, GrepToolSettings,
HashEditToolSettings, HerdrSettings, HumanizeProtectionFailurePolicy,
HumanizeProtectionSettings, InstructionsSettings, IntegrationsSettings, JevSettings,
ListFilesToolSettings, LspServerConfig, LspServersSettings, LspSettings, McpHttpServerConfig,
McpOAuthConfig, McpServerConfig, McpServersSettings, McpStdioServerConfig, ModelsSettings,
OpenAiCodexSettings, OpenAiResponsesSettings, PromptInjectionFailurePolicy,
PromptInjectionProtectionLevel, PromptInjectionProtectionSettings, ProviderStreamSettings,
ReadToolSettings, SelectedModelSettings, SessionTitleSettings, Settings, SideAgentSettings,
SkillSuggestionSettings, SkillsSettings, SubagentsSettings, SubagentsToolSettings,
SummarizerSettings, TextVerbosity, ToolSettings, TuiSettings, ViewImageToolSettings,
ViewImageVisionModelSettings, WriteToolSettings,
};
pub(crate) use settings::{AppearanceSettings, AutoCompactionLimit, set_appearance_theme};
pub(crate) use settings::{
CompactionConfig, SessionTitleConfig, clamp_subagent_max_depth, validate_mcp_http_url_field,
validate_mcp_server_name, validate_view_image_identifier, validate_view_image_max_image_bytes,
};
pub(crate) use settings::{
DEFAULT_MCP_TIMEOUT_SECONDS, DEFAULT_SESSION_RETENTION_DAYS, SettingsListKind, SettingsScope,
};
pub(crate) use settings::{DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES, MAX_VIEW_IMAGE_MAX_IMAGE_BYTES};
pub(crate) use settings::{
disabled_model_ids_from_settings, disabled_skill_names_from_settings,
disabled_subagent_profile_names_from_settings, disabled_tool_names_from_settings,
load_startup_config_with_settings,
};
pub(crate) use settings::{
disabled_names_for_modal_scope, fast_mode_enabled, read_settings, remove_custom_provider,
set_fast_mode, set_mcp_server_enabled, set_model_disabled_for_scope,
set_selected_primary_agent, set_skill_disabled_for_scope, set_subagent_profile_disabled,
set_tool_disabled, toggle_fast_mode, upsert_custom_provider,
};
pub(crate) use settings::{load_settings_editor, save_settings_editor, set_code_mode_tool_enabled};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
pub(crate) enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct CliConfigOverrides {
pub(crate) provider: Option<String>,
pub(crate) model: Option<String>,
pub(crate) api_key: Option<String>,
pub(crate) color: Option<ColorChoice>,
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct EffectiveConfig {
pub(crate) provider: Option<String>,
pub(crate) model: Option<String>,
pub(crate) no_color: bool,
pub(crate) file_autocomplete_respects_gitignore: bool,
pub(crate) custom_providers: BTreeMap<String, CustomProviderConfig>,
pub(crate) thinking_level: ThinkingLevel,
pub(crate) auth: Option<ProviderCredential>,
pub(crate) paths: McPaths,
}
impl fmt::Debug for EffectiveConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EffectiveConfig")
.field("provider", &self.provider)
.field("model", &self.model)
.field("no_color", &self.no_color)
.field(
"file_autocomplete_respects_gitignore",
&self.file_autocomplete_respects_gitignore,
)
.field("custom_providers", &self.custom_providers)
.field("auth", &self.auth)
.field("thinking_level", &self.thinking_level)
.field("paths", &self.paths)
.finish()
}
}
impl EffectiveConfig {
pub(crate) fn selected_provider_model(settings: &Settings) -> (Option<String>, Option<String>) {
(
env::var("MC_PROVIDER")
.ok()
.or_else(|| settings.selected_model.provider.clone()),
env::var("MC_MODEL")
.ok()
.or_else(|| settings.selected_model.model.clone()),
)
}
pub(crate) fn from_loaded_settings(
paths: McPaths,
cli: CliConfigOverrides,
settings: Settings,
) -> anyhow::Result<Self> {
let (color_enabled, _) = resolve_output_style(&settings, cli.color);
let (provider, model) = Self::selected_provider_model(&settings);
let provider = cli.provider.or(provider);
let model = cli.model.or(model);
let provider_id = provider
.as_deref()
.unwrap_or(crate::providers::OPENAI_CODEX_PROVIDER);
reject_retired_provider_selection(provider_id, model.as_deref().unwrap_or(""))?;
let auth_file = read_auth(&paths)?;
let auth = resolve_provider_credential(
provider_id,
&auth_file,
cli.api_key,
&settings.custom_providers,
)?;
Ok(Self {
provider,
model,
no_color: !color_enabled,
file_autocomplete_respects_gitignore: settings.file_autocomplete_respects_gitignore,
custom_providers: settings.custom_providers,
thinking_level: settings.selected_model.thinking_level.unwrap_or_default(),
auth,
paths,
})
}
pub(crate) fn auth_state(&self) -> AuthState {
AuthState::for_provider_with_custom(
self.provider_id(),
self.auth.as_ref(),
&self.custom_providers,
)
}
pub(crate) fn require_auth(&self) -> Result<ProviderCredential, ConfigError> {
self.auth_state()
.credential()
.cloned()
.ok_or_else(|| self.missing_auth_error())
}
pub(crate) fn resolve_provider_auth_for_runtime(&self) -> anyhow::Result<ProviderCredential> {
self.resolve_provider_auth_for_runtime_with(codex_credential_from_store)
}
fn resolve_provider_auth_for_runtime_with(
&self,
prepare_codex_auth: impl FnOnce(&McPaths) -> anyhow::Result<ProviderCredential>,
) -> anyhow::Result<ProviderCredential> {
if self.provider_id() == crate::providers::ANTHROPIC_PROVIDER {
return resolve_provider_credential(
self.provider_id(),
&read_auth(&self.paths)?,
None,
&self.custom_providers,
)?
.ok_or_else(|| self.missing_auth_error().into());
}
if self.provider_id() == crate::providers::CLAUDE_SUBSCRIPTION_PROVIDER {
return resolve_provider_credential(
self.provider_id(),
&read_auth(&self.paths)?,
None,
&self.custom_providers,
)?
.ok_or_else(|| self.missing_auth_error().into());
}
if self.provider_id() != crate::providers::OPENAI_CODEX_PROVIDER {
return self.require_auth().map_err(Into::into);
}
prepare_codex_auth(&self.paths)
}
pub(crate) fn missing_auth_error(&self) -> ConfigError {
ConfigError::missing_auth_for_custom_providers(
self.provider_id(),
&self.custom_providers,
&self.paths.auth_file,
)
}
pub(crate) fn provider_id(&self) -> &str {
self.provider
.as_deref()
.unwrap_or(crate::providers::OPENAI_CODEX_PROVIDER)
}
}
pub(crate) fn reject_retired_provider_selection(provider: &str, model: &str) -> anyhow::Result<()> {
if provider != "claude-code" {
return Ok(());
}
let model = if model.trim().is_empty() {
"<model>"
} else {
model
};
anyhow::bail!(
"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"
);
}
pub(crate) fn load_effective_provider_selection(
paths: &McPaths,
provider: &str,
model: &str,
) -> anyhow::Result<EffectiveConfig> {
load_effective_provider_selection_with_settings(paths, provider, model, &read_settings(paths)?)
}
pub(crate) fn load_effective_provider_selection_with_settings(
paths: &McPaths,
provider: &str,
model: &str,
settings: &Settings,
) -> anyhow::Result<EffectiveConfig> {
reject_retired_provider_selection(provider, model)?;
let auth_file = read_auth(paths)?;
let auth = resolve_provider_credential(provider, &auth_file, None, &settings.custom_providers)?;
let (color_enabled, _) = resolve_output_style(settings, None);
Ok(EffectiveConfig {
provider: Some(provider.to_string()),
model: Some(model.to_string()),
no_color: !color_enabled,
file_autocomplete_respects_gitignore: settings.file_autocomplete_respects_gitignore,
custom_providers: settings.custom_providers.clone(),
thinking_level: settings.selected_model.thinking_level.unwrap_or_default(),
auth,
paths: paths.clone(),
})
}
impl ConfigError {
pub(crate) fn missing_auth_for_custom_providers(
provider: &str,
custom_providers: &BTreeMap<String, CustomProviderConfig>,
auth_file: &std::path::Path,
) -> Self {
missing_auth_error(provider, custom_providers.get(provider), auth_file)
}
}
fn missing_auth_error(
provider: &str,
custom: Option<&CustomProviderConfig>,
auth_file: &std::path::Path,
) -> ConfigError {
let auth_path = auth_file.display();
let message = if let Some(custom) = custom {
match &custom.api_key_env_var {
Some(env_var) => format!(
"missing auth: custom provider '{provider}' is configured but environment variable {env_var} is missing or empty"
),
None => format!(
"missing auth: custom provider '{provider}' is configured for no-auth but could not be prepared"
),
}
} else if provider == crate::providers::CLAUDE_SUBSCRIPTION_PROVIDER {
"Claude subscription unavailable: on Unix, install Claude Code and run `claude auth login`; unset ANTHROPIC_API_KEY and other native backend overrides; API keys are not used".to_string()
} else if provider == crate::providers::OPENAI_CODEX_PROVIDER {
format!(
"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}"
)
} else if provider == crate::providers::ANTHROPIC_PROVIDER {
format!(
"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"
)
} else {
format!(
"missing auth for provider '{provider}'; configure provider-keyed auth in {auth_path}"
)
};
ConfigError::MissingAuth {
provider: provider.to_string(),
message,
}
}
fn resolve_output_style(settings: &Settings, cli_color: Option<ColorChoice>) -> (bool, bool) {
let stdout_is_tty = std::io::stdout().is_terminal();
resolve_output_style_for_stdout(settings, cli_color, stdout_is_tty)
}
pub(crate) fn resolve_output_style_for_stdout(
settings: &Settings,
cli_color: Option<ColorChoice>,
stdout_is_tty: bool,
) -> (bool, bool) {
let policy = crate::appearance::resolve_color_policy_from_env(
settings.no_color,
cli_color,
stdout_is_tty,
env::var_os("NO_COLOR").is_some(),
env::var("COLORTERM").ok().as_deref(),
env::var("TERM").ok().as_deref(),
);
(policy.color_enabled, policy.unicode_enabled)
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub(crate) enum ConfigError {
#[error("{message}")]
MissingAuth { provider: String, message: String },
}
pub(crate) fn load_context_budget(
config: &EffectiveConfig,
) -> anyhow::Result<crate::context::ContextBudget> {
let settings = read_settings(&config.paths)?;
let mut budget = settings.context.unwrap_or_default();
let provider = config.provider_id();
let model = config
.model
.as_deref()
.unwrap_or_else(|| crate::providers::default_model_for_provider(provider));
if let Some(context_window) =
crate::model_catalog::cached_model_context_window(&config.paths, provider, model)
{
budget.max_tokens = context_window;
}
budget.apply_model_limits(provider, model);
settings
.compaction
.apply_conversation_limit(&mut budget, provider);
Ok(budget)
}
#[cfg(test)]
pub(crate) use settings::write_settings;