use codex_arg0::Arg0DispatchPaths; use codex_cloud_config::cloud_config_bundle_loader; use codex_config::CloudConfigBundleLoader; use codex_config::ConfigLayerStack; use codex_config::LoaderOverrides; use codex_config::ThreadConfigLoader; use codex_config::loader::load_config_layers_state; use codex_config::loader::load_managed_requirements_state; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; use codex_exec_server::LOCAL_FS; use codex_features::feature_for_key; use codex_login::AuthManager; use codex_login::default_client::set_default_client_residency_requirement; use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID; use codex_model_provider_info::AMAZON_BEDROCK_RUNTIME_PROVIDER_ID; use codex_model_provider_info::built_in_model_providers; use codex_model_provider_info::merge_configured_model_providers; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_json_to_toml::json_to_toml; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; use toml::Value as TomlValue; use tracing::instrument; use tracing::warn; #[derive(Debug, thiserror::Error)] #[error( "Your organization's required model provider settings changed. Restart Codex to apply them; this request was not sent" )] pub(crate) struct ModelProviderRequirementsChanged; /// Shared app-server entry point for loading effective Codex configuration. #[derive(Clone)] pub(crate) struct ConfigManager { codex_home: PathBuf, cli_overrides: Arc>>, runtime_feature_enablement: Arc>>, loader_overrides: LoaderOverrides, strict_config: bool, cloud_config_bundle: Arc>, arg0_paths: Arg0DispatchPaths, thread_config_loader: Arc, } impl ConfigManager { pub(crate) fn new( codex_home: PathBuf, cli_overrides: Vec<(String, TomlValue)>, loader_overrides: LoaderOverrides, strict_config: bool, cloud_config_bundle: CloudConfigBundleLoader, arg0_paths: Arg0DispatchPaths, thread_config_loader: Arc, ) -> Self { Self { codex_home, cli_overrides: Arc::new(RwLock::new(cli_overrides)), runtime_feature_enablement: Arc::new(RwLock::new(BTreeMap::new())), loader_overrides, strict_config, cloud_config_bundle: Arc::new(RwLock::new(cloud_config_bundle)), arg0_paths, thread_config_loader, } } pub(crate) fn codex_home(&self) -> &Path { self.codex_home.as_path() } pub(crate) fn user_config_path(&self) -> std::io::Result { self.loader_overrides.user_config_path(self.codex_home()) } pub(crate) fn current_cli_overrides(&self) -> Vec<(String, TomlValue)> { self.cli_overrides .read() .map(|guard| guard.clone()) .unwrap_or_default() } pub(crate) fn current_cloud_config_bundle(&self) -> CloudConfigBundleLoader { self.cloud_config_bundle .read() .map(|guard| guard.clone()) .unwrap_or_default() } pub(crate) fn extend_runtime_feature_enablement(&self, enablement: I) -> Result<(), ()> where I: IntoIterator, { let mut runtime_feature_enablement = self.runtime_feature_enablement.write().map_err(|_| ())?; runtime_feature_enablement.extend(enablement); Ok(()) } pub(crate) fn replace_cloud_config_bundle_loader( &self, auth_manager: Arc, chatgpt_base_url: String, http_client_factory: codex_http_client::HttpClientFactory, ) { let loader = cloud_config_bundle_loader( auth_manager, chatgpt_base_url, self.codex_home.clone(), http_client_factory, ); if let Ok(mut guard) = self.cloud_config_bundle.write() { *guard = loader; } else { warn!("failed to update cloud config bundle loader"); } } pub(crate) fn clear_cloud_config_bundle_loader(&self) { if let Ok(mut guard) = self.cloud_config_bundle.write() { *guard = CloudConfigBundleLoader::default(); } else { warn!("failed to clear cloud config bundle loader"); } } pub(crate) async fn sync_default_client_residency_requirement(&self) { match self.load_latest_config(/*fallback_cwd*/ None).await { Ok(config) => { set_default_client_residency_requirement(config.enforce_residency.value()); } Err(err) => warn!( error = %err, "failed to sync default client residency requirement after auth refresh" ), } } pub(crate) async fn load_latest_config( &self, fallback_cwd: Option, ) -> std::io::Result { self.load_with_cli_overrides( &self.current_cli_overrides(), /*request_overrides*/ None, ConfigOverrides::default(), fallback_cwd, ) .await } /// Loads system, user, and runtime settings without discovering a project /// from the app-server process's working directory. pub(crate) async fn load_non_project_config(&self) -> std::io::Result { let mut manager = self.clone(); manager.loader_overrides.ignore_project_config = true; manager.load_latest_config(/*fallback_cwd*/ None).await } pub(crate) async fn load_latest_config_with_session_layers( &self, session_layers: &ConfigLayerStack, cwd: &Path, ) -> std::io::Result { let refreshed_config = self.load_latest_config(Some(cwd.to_path_buf())).await?; let mut config = Config::rebuild_with_session_layers( session_layers, cwd.to_path_buf(), &refreshed_config, ) .await?; self.apply_runtime_feature_enablement(&mut config); self.apply_arg0_paths(&mut config); Ok(config) } /// Checks the retained session route against current managed provider requirements. pub(crate) async fn check_thread_model_provider( &self, current: &Config, ) -> std::io::Result<()> { // Existing threads retain their session route; only managed // requirements can invalidate it. let requirements = load_managed_requirements_state( LOCAL_FS.as_ref(), &self.codex_home, codex_config::ConfigLoadOptions { loader_overrides: self.loader_overrides.clone(), strict_config: self.strict_config, cloud_config_bundle: self.current_cloud_config_bundle(), }, ) .await?; let selection_changed = requirements .model_provider .as_ref() .is_some_and(|provider_id| provider_id != ¤t.model_provider_id); let required_provider = requirements .model_providers .as_ref() .and_then(|providers| providers.get(¤t.model_provider_id)) .cloned(); let required_provider = match required_provider { Some(provider) if matches!( current.model_provider_id.as_str(), AMAZON_BEDROCK_PROVIDER_ID | AMAZON_BEDROCK_RUNTIME_PROVIDER_ID ) => { // Bedrock requirements contain overrides of the built-in provider. merge_configured_model_providers( built_in_model_providers(/*openai_base_url*/ None), HashMap::from([(current.model_provider_id.clone(), provider)]), ) .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidData, message))? .remove(¤t.model_provider_id) } Some(provider) => Some(provider), None => None, }; let definition_changed = required_provider .as_ref() .is_some_and(|provider| provider != ¤t.model_provider); if selection_changed || definition_changed { return Err(std::io::Error::new( std::io::ErrorKind::PermissionDenied, ModelProviderRequirementsChanged, )); } Ok(()) } pub(crate) async fn load_default_config(&self) -> std::io::Result { let mut loader_overrides = self.loader_overrides.clone(); loader_overrides.ignore_user_config = true; let mut config = ConfigBuilder::default() .codex_home(self.codex_home.clone()) .cli_overrides(self.current_cli_overrides()) .loader_overrides(loader_overrides) .fallback_cwd(Some(self.codex_home.clone())) .cloud_config_bundle(CloudConfigBundleLoader::default()) .build() .await?; self.apply_runtime_feature_enablement(&mut config); self.apply_arg0_paths(&mut config); Ok(config) } pub(crate) async fn load_with_overrides( &self, request_overrides: Option>, typesafe_overrides: ConfigOverrides, ) -> std::io::Result { self.load_with_cli_overrides( &self.current_cli_overrides(), request_overrides, typesafe_overrides, /*fallback_cwd*/ None, ) .await } pub(crate) async fn load_for_cwd( &self, request_overrides: Option>, typesafe_overrides: ConfigOverrides, cwd: Option, ) -> std::io::Result { self.load_with_cli_overrides( &self.current_cli_overrides(), request_overrides, typesafe_overrides, cwd, ) .await } #[instrument(level = "trace", skip_all)] pub(crate) async fn load_with_cli_overrides( &self, cli_overrides: &[(String, TomlValue)], request_overrides: Option>, mut typesafe_overrides: ConfigOverrides, fallback_cwd: Option, ) -> std::io::Result { let mut request_overrides = request_overrides.unwrap_or_default(); if let Some(value) = request_overrides.remove("bypass_hook_trust") { typesafe_overrides.bypass_hook_trust = Some(value.as_bool().ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::InvalidData, "`bypass_hook_trust` override must be a boolean", ) })?); } let merged_cli_overrides = cli_overrides .iter() .cloned() .chain( request_overrides .into_iter() .map(|(key, value)| (key, json_to_toml(value))), ) .collect::>(); let mut config = codex_core::config::ConfigBuilder::default() .codex_home(self.codex_home.clone()) .cli_overrides(merged_cli_overrides) .loader_overrides(self.loader_overrides.clone()) .strict_config(self.strict_config) .harness_overrides(typesafe_overrides) .fallback_cwd(fallback_cwd) .cloud_config_bundle(self.current_cloud_config_bundle()) .thread_config_loader(Arc::clone(&self.thread_config_loader)) .build() .await?; self.apply_runtime_feature_enablement(&mut config); self.apply_arg0_paths(&mut config); Ok(config) } pub(crate) async fn load_config_layers_for_cwd( &self, cwd: AbsolutePathBuf, ) -> std::io::Result { self.load_config_layers(Some(cwd)).await } pub(crate) async fn load_config_layers( &self, cwd: Option, ) -> std::io::Result { load_config_layers_state( LOCAL_FS.as_ref(), &self.codex_home, cwd, &self.current_cli_overrides(), codex_config::ConfigLoadOptions { loader_overrides: self.loader_overrides.clone(), strict_config: self.strict_config, cloud_config_bundle: self.current_cloud_config_bundle(), }, self.thread_config_loader.as_ref(), ) .await } fn apply_runtime_feature_enablement(&self, config: &mut Config) { apply_runtime_feature_enablement(config, &self.current_runtime_feature_enablement()); } fn current_runtime_feature_enablement(&self) -> BTreeMap { self.runtime_feature_enablement .read() .map(|guard| guard.clone()) .unwrap_or_default() } fn apply_arg0_paths(&self, config: &mut Config) { config.codex_self_exe = self.arg0_paths.codex_self_exe.clone(); config.codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone(); config.main_execve_wrapper_exe = self.arg0_paths.main_execve_wrapper_exe.clone(); } #[cfg(test)] pub(crate) fn new_for_tests( codex_home: PathBuf, cli_overrides: Vec<(String, TomlValue)>, loader_overrides: LoaderOverrides, cloud_config_bundle: CloudConfigBundleLoader, ) -> Self { Self::new( codex_home, cli_overrides, loader_overrides, /*strict_config*/ false, cloud_config_bundle, Arg0DispatchPaths::default(), Arc::new(codex_config::NoopThreadConfigLoader), ) } #[cfg(test)] pub(crate) fn without_managed_config_for_tests(codex_home: PathBuf) -> Self { Self::new_for_tests( codex_home, Vec::new(), LoaderOverrides::without_managed_config_for_tests(), CloudConfigBundleLoader::default(), ) } } #[cfg(test)] #[path = "config_manager_provider_tests.rs"] mod provider_tests; pub(crate) fn protected_feature_keys(config_layer_stack: &ConfigLayerStack) -> BTreeSet { let mut protected_features = config_layer_stack .effective_config() .get("features") .and_then(toml::Value::as_table) .map(|features| features.keys().cloned().collect::>()) .unwrap_or_default(); if let Some(feature_requirements) = config_layer_stack .requirements_toml() .feature_requirements .as_ref() { protected_features.extend(feature_requirements.entries.keys().cloned()); } protected_features } pub(crate) fn apply_runtime_feature_enablement( config: &mut Config, runtime_feature_enablement: &BTreeMap, ) { let protected_features = protected_feature_keys(&config.config_layer_stack); for (name, enabled) in runtime_feature_enablement { if protected_features.contains(name) { continue; } let Some(feature) = feature_for_key(name) else { continue; }; if let Err(err) = config.features.set_enabled(feature, *enabled) { warn!( feature = name, error = %err, "failed to apply runtime feature enablement" ); } } }