mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
## Why Existing chats retain their provider configuration when the selected provider changes. Reject resume and input until the configured provider matches the chat, while keeping history readable and allowing recovery after configuration is restored. ## What changed - Validate saved providers before applying resume overrides, preserving the provider when only model or reasoning settings change. - Check the current provider selection, including profiles and launch overrides, before accepting input. Reject input when configuration cannot be loaded. - Return structured `ModelProviderError` data for provider mismatches, missing definitions, and policy conflicts, with generated JSON Schema, TypeScript, and Python types. ## Testing Add coverage for override bypass prevention, provider restoration, readable history after rejection, and blocked requests sending no inference traffic. Verify schema fields and Python round trips match the error wire format. GitOrigin-RevId: 051f97c3815c547d8411c71b770df30eae969f68
878 lines
33 KiB
Rust
878 lines
33 KiB
Rust
use codex_app_server_protocol::ModelProviderError;
|
|
use codex_arg0::Arg0DispatchPaths;
|
|
use codex_cloud_config::managed_config_bundle_loader;
|
|
use codex_config::CloudConfigBundleLoader;
|
|
use codex_config::ConfigLayerStack;
|
|
use codex_config::LoaderOverrides;
|
|
use codex_config::ThreadConfigLoader;
|
|
use codex_config::apply_default_gcp_auth;
|
|
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::GCP_PROVIDER_ID;
|
|
use codex_model_provider_info::OPENAI_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::Mutex;
|
|
use std::sync::RwLock;
|
|
use tokio_util::sync::CancellationToken;
|
|
use toml::Value as TomlValue;
|
|
use tracing::instrument;
|
|
use tracing::warn;
|
|
|
|
#[path = "application_network.rs"]
|
|
pub(crate) mod application_network;
|
|
|
|
/// Shared app-server entry point for loading effective Codex configuration.
|
|
#[derive(Clone)]
|
|
pub(crate) struct ConfigManager {
|
|
codex_home: PathBuf,
|
|
cli_overrides: Arc<RwLock<Vec<(String, TomlValue)>>>,
|
|
runtime_feature_enablement: Arc<RwLock<BTreeMap<String, bool>>>,
|
|
loader_overrides: LoaderOverrides,
|
|
strict_config: bool,
|
|
cloud_config_bundle: Arc<RwLock<CloudConfigBundleLoader>>,
|
|
arg0_paths: Arg0DispatchPaths,
|
|
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
|
network_policy: codex_http_client::NetworkPolicyController,
|
|
local_network_policy: codex_http_client::NetworkPolicyController,
|
|
network_policy_reload: Arc<tokio::sync::Semaphore>,
|
|
network_policy_snapshot: Arc<RwLock<Option<Arc<ApplicationPolicySnapshot>>>>,
|
|
/// Captured before config loading so a reload also revokes pending startups.
|
|
mcp_ema_cancellation: Arc<Mutex<EmaCancellationRoots>>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct EmaCancellationRoots {
|
|
startup: CancellationToken,
|
|
lifetime: CancellationToken,
|
|
}
|
|
|
|
/// Startup configuration, including whether loading had to fall back to defaults.
|
|
pub(crate) struct StartupConfig {
|
|
pub(crate) config: Config,
|
|
pub(crate) used_defaults: bool,
|
|
}
|
|
|
|
/// Local auth authority and the cloud-backed configuration loaded with it.
|
|
pub(crate) struct CloudStartupConfig {
|
|
pub(crate) bootstrap: Config,
|
|
pub(crate) config: std::io::Result<Config>,
|
|
}
|
|
|
|
/// Configuration and policy must finish loading against the same account and cloud snapshot.
|
|
pub(crate) struct ApplicationPolicyLoad {
|
|
cloud_config: CloudConfigBundleLoader,
|
|
snapshot: Arc<ApplicationPolicySnapshot>,
|
|
}
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
struct ApplicationPolicySnapshot {
|
|
revision: codex_http_client::NetworkPolicyRevision,
|
|
policy: codex_http_client::DestinationPolicy,
|
|
cloud: Option<codex_config::CloudConfigBundle>,
|
|
}
|
|
|
|
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<dyn ThreadConfigLoader>,
|
|
) -> Self {
|
|
let network_policy = codex_http_client::NetworkPolicyController::default();
|
|
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,
|
|
network_policy,
|
|
local_network_policy: Default::default(),
|
|
network_policy_reload: Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)),
|
|
network_policy_snapshot: Arc::default(),
|
|
mcp_ema_cancellation: Arc::default(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn codex_home(&self) -> &Path {
|
|
self.codex_home.as_path()
|
|
}
|
|
|
|
/// Explicit account, policy-owner, and enterprise-login changes revoke cached access.
|
|
pub(crate) fn revoke_mcp_ema(&self) {
|
|
let mut current = self
|
|
.mcp_ema_cancellation
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
current.startup.cancel();
|
|
current.lifetime.cancel();
|
|
*current = EmaCancellationRoots::default();
|
|
}
|
|
|
|
/// Published threads are validated individually by the existing refresh loop.
|
|
pub(crate) fn revoke_mcp_ema_startups(&self) {
|
|
let mut current = self
|
|
.mcp_ema_cancellation
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
current.startup.cancel();
|
|
current.startup = CancellationToken::new();
|
|
}
|
|
|
|
fn capture_mcp_ema_cancellation(&self) -> codex_config::McpEmaCancellation {
|
|
let current = self
|
|
.mcp_ema_cancellation
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
codex_config::McpEmaCancellation::pending(current.startup.clone(), current.lifetime.clone())
|
|
}
|
|
|
|
pub(crate) fn with_embedded_network_policy(
|
|
mut self,
|
|
policy: crate::in_process::EmbeddedNetworkPolicy,
|
|
) -> Self {
|
|
self.network_policy = policy.effective;
|
|
self.local_network_policy = policy.local;
|
|
self
|
|
}
|
|
|
|
pub(crate) fn user_config_path(&self) -> std::io::Result<AbsolutePathBuf> {
|
|
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_managed_config_loader(&self) -> CloudConfigBundleLoader {
|
|
self.cloud_config_bundle
|
|
.read()
|
|
.map(|guard| guard.clone())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn extend_runtime_feature_enablement<I>(&self, enablement: I) -> Result<(), ()>
|
|
where
|
|
I: IntoIterator<Item = (String, bool)>,
|
|
{
|
|
let mut runtime_feature_enablement =
|
|
self.runtime_feature_enablement.write().map_err(|_| ())?;
|
|
runtime_feature_enablement.extend(enablement);
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn update_managed_config_loader(
|
|
&self,
|
|
auth_manager: Arc<AuthManager>,
|
|
config: &Config,
|
|
) {
|
|
// Keep config-server settings and their refresh worker when only
|
|
// ChatGPT/API-key credentials change.
|
|
if self
|
|
.current_managed_config_loader()
|
|
.config_server()
|
|
.is_some()
|
|
{
|
|
self.revoke_mcp_ema();
|
|
return;
|
|
}
|
|
let loader = managed_config_bundle_loader(
|
|
auth_manager,
|
|
config.chatgpt_base_url.clone(),
|
|
self.codex_home.clone(),
|
|
config
|
|
.http_client_factory()
|
|
.with_network_policy(self.local_network_policy.policy()),
|
|
config.config_layer_stack.requirements_toml(),
|
|
);
|
|
if let Ok(mut guard) = self.cloud_config_bundle.write() {
|
|
guard.retire_ema_policy();
|
|
*guard = loader;
|
|
} else {
|
|
warn!("failed to update cloud config bundle loader");
|
|
}
|
|
self.revoke_mcp_ema();
|
|
}
|
|
|
|
pub(crate) fn clear_chatgpt_config_loader(&self) {
|
|
if let Ok(mut guard) = self.cloud_config_bundle.write() {
|
|
if guard.config_server().is_none() {
|
|
guard.retire_ema_policy();
|
|
*guard = CloudConfigBundleLoader::default();
|
|
}
|
|
} else {
|
|
warn!("failed to clear cloud config bundle loader");
|
|
}
|
|
self.revoke_mcp_ema();
|
|
}
|
|
|
|
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<PathBuf>,
|
|
) -> std::io::Result<Config> {
|
|
let Some(fallback_cwd) = fallback_cwd else {
|
|
return self.load_non_project_config().await;
|
|
};
|
|
self.load_with_cli_overrides(
|
|
&self.current_cli_overrides(),
|
|
/*request_overrides*/ None,
|
|
ConfigOverrides::default(),
|
|
Some(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<Config> {
|
|
let mcp_ema_cancellation = self.capture_mcp_ema_cancellation();
|
|
let policy_load = self.refresh_application_network_policy().await?;
|
|
let mut config = ConfigBuilder::default()
|
|
.codex_home(self.codex_home.clone())
|
|
.cli_overrides(self.current_cli_overrides())
|
|
.loader_overrides(self.loader_overrides.clone())
|
|
.strict_config(self.strict_config)
|
|
.fallback_cwd(Some(self.codex_home.clone()))
|
|
.cloud_config_bundle(policy_load.cloud_config.clone())
|
|
.thread_config_loader(Arc::clone(&self.thread_config_loader))
|
|
.without_project_context()
|
|
.build()
|
|
.await?;
|
|
self.check_application_policy_load(&policy_load)?;
|
|
self.apply_network_policy(&mut config);
|
|
self.apply_runtime_feature_enablement(&mut config);
|
|
self.apply_arg0_paths(&mut config);
|
|
config.config_layer_stack = config
|
|
.config_layer_stack
|
|
.with_mcp_ema_cancellation(mcp_ema_cancellation);
|
|
Ok(config)
|
|
}
|
|
|
|
pub(crate) async fn load_latest_config_with_session_layers(
|
|
&self,
|
|
session_layers: &ConfigLayerStack,
|
|
cwd: &Path,
|
|
) -> std::io::Result<Config> {
|
|
let mcp_ema_cancellation = self.capture_mcp_ema_cancellation();
|
|
let codex_home = AbsolutePathBuf::from_absolute_path(&self.codex_home)?;
|
|
let layers = self
|
|
.load_config_layers_for_cwd(AbsolutePathBuf::from_absolute_path(cwd)?)
|
|
.await?;
|
|
let mut config = Config::rebuild_with_session_layers(
|
|
session_layers,
|
|
cwd.to_path_buf(),
|
|
&layers,
|
|
codex_home,
|
|
)
|
|
.await?;
|
|
self.apply_network_policy(&mut config);
|
|
self.apply_runtime_feature_enablement(&mut config);
|
|
self.apply_arg0_paths(&mut config);
|
|
config.config_layer_stack = config
|
|
.config_layer_stack
|
|
.with_mcp_ema_cancellation(mcp_ema_cancellation);
|
|
Ok(config)
|
|
}
|
|
|
|
/// Refreshes global settings and managed requirements using already fetched session layers.
|
|
pub(crate) async fn load_retained_session_config(
|
|
&self,
|
|
session_layers: &ConfigLayerStack,
|
|
cwd: &Path,
|
|
) -> std::io::Result<Config> {
|
|
let mcp_ema_cancellation = self.capture_mcp_ema_cancellation();
|
|
let mut manager = self.clone();
|
|
manager.thread_config_loader = Arc::new(codex_config::NoopThreadConfigLoader);
|
|
let refreshed_layers = manager
|
|
.load_config_layers_for_cwd(AbsolutePathBuf::from_absolute_path(cwd)?)
|
|
.await?;
|
|
// Merge the retained provider definitions before resolving managed provider selection.
|
|
let mut config = Config::rebuild_with_session_layers(
|
|
session_layers,
|
|
cwd.to_path_buf(),
|
|
&refreshed_layers,
|
|
AbsolutePathBuf::from_absolute_path(&self.codex_home)?,
|
|
)
|
|
.await?;
|
|
self.apply_network_policy(&mut config);
|
|
self.apply_runtime_feature_enablement(&mut config);
|
|
self.apply_arg0_paths(&mut config);
|
|
config.config_layer_stack = config
|
|
.config_layer_stack
|
|
.with_mcp_ema_cancellation(mcp_ema_cancellation);
|
|
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<()> {
|
|
let policy_load = self.refresh_application_network_policy().await?;
|
|
// 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: policy_load.cloud_config.clone(),
|
|
},
|
|
)
|
|
.await?;
|
|
self.check_application_policy_load(&policy_load)?;
|
|
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
|
|
| GCP_PROVIDER_ID
|
|
| OPENAI_PROVIDER_ID
|
|
) =>
|
|
{
|
|
// Managed overrides must receive the same built-in defaults as startup config.
|
|
// OpenAI's inference URL belongs to the retained session, not the catalog override.
|
|
let mut provider = merge_configured_model_providers(
|
|
built_in_model_providers(current.model_provider.base_url.clone()),
|
|
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);
|
|
if current.model_provider_id == GCP_PROVIDER_ID
|
|
&& let Some(provider) = provider.as_mut()
|
|
{
|
|
apply_default_gcp_auth(provider, ¤t.cwd);
|
|
}
|
|
provider
|
|
}
|
|
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,
|
|
ModelProviderError::ModelProviderPolicyConflict {
|
|
model_provider: current.model_provider_id.clone(),
|
|
required_model_provider: requirements
|
|
.model_provider
|
|
.unwrap_or_else(|| current.model_provider_id.clone()),
|
|
},
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Load local policy before auth, then fetch the account's cloud configuration.
|
|
/// Callers choose their error/recovery policy before initializing any runtimes.
|
|
pub(crate) async fn load_managed_startup_config(
|
|
&self,
|
|
cwd: Option<PathBuf>,
|
|
) -> std::io::Result<CloudStartupConfig> {
|
|
let bootstrap = self.load_startup_config(cwd.clone()).await?.config;
|
|
let auth =
|
|
AuthManager::shared_from_config(&bootstrap, /*enable_codex_api_key_env*/ false)
|
|
.await
|
|
.map_err(std::io::Error::other)?;
|
|
self.update_managed_config_loader(auth, &bootstrap);
|
|
let config = self.load_latest_config(cwd).await;
|
|
Ok(CloudStartupConfig { bootstrap, config })
|
|
}
|
|
|
|
/// Admits resumed threads and input only on the currently selected provider.
|
|
pub(crate) async fn check_thread_model_provider_for_input(
|
|
&self,
|
|
current: &Config,
|
|
) -> std::io::Result<()> {
|
|
self.check_thread_model_provider(current).await?;
|
|
let configured = self
|
|
.load_latest_config(Some(current.cwd.to_path_buf()))
|
|
.await?;
|
|
if current.model_provider_id != configured.model_provider_id {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
ModelProviderError::ModelProviderMismatch {
|
|
model_provider: current.model_provider_id.clone(),
|
|
current_model_provider: configured.model_provider_id,
|
|
},
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Installs local policy before cloud access; failed policy loads block traffic while
|
|
/// non-strict startup remains available for configuration repair.
|
|
pub(crate) async fn load_startup_config(
|
|
&self,
|
|
fallback_cwd: Option<PathBuf>,
|
|
) -> std::io::Result<StartupConfig> {
|
|
match self.load_latest_config(fallback_cwd).await {
|
|
Ok(config) => Ok(StartupConfig {
|
|
config,
|
|
used_defaults: false,
|
|
}),
|
|
Err(error)
|
|
if self.strict_config
|
|
|| crate::is_unsupported_untrusted_approval_policy_error(&error) =>
|
|
{
|
|
Err(error)
|
|
}
|
|
Err(error) => {
|
|
warn!(%error, "configuration is unavailable; using default settings");
|
|
Ok(StartupConfig {
|
|
config: self.load_default_config().await?,
|
|
used_defaults: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn load_default_config(&self) -> std::io::Result<Config> {
|
|
let mcp_ema_cancellation = self.capture_mcp_ema_cancellation();
|
|
let mut loader_overrides = self.loader_overrides.clone();
|
|
loader_overrides.ignore_user_config = true;
|
|
loader_overrides.ignore_project_config = true;
|
|
loader_overrides.ignore_managed_requirements |=
|
|
self.refresh_local_network_policy().await.is_err();
|
|
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_network_policy(&mut config);
|
|
self.apply_runtime_feature_enablement(&mut config);
|
|
self.apply_arg0_paths(&mut config);
|
|
config.config_layer_stack = config
|
|
.config_layer_stack
|
|
.with_mcp_ema_cancellation(mcp_ema_cancellation);
|
|
Ok(config)
|
|
}
|
|
|
|
pub(crate) async fn load_with_overrides(
|
|
&self,
|
|
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
|
typesafe_overrides: ConfigOverrides,
|
|
) -> std::io::Result<Config> {
|
|
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<HashMap<String, serde_json::Value>>,
|
|
typesafe_overrides: ConfigOverrides,
|
|
cwd: Option<PathBuf>,
|
|
) -> std::io::Result<Config> {
|
|
self.load_with_cli_overrides(
|
|
&self.current_cli_overrides(),
|
|
request_overrides,
|
|
typesafe_overrides,
|
|
cwd,
|
|
)
|
|
.await
|
|
.and_then(|config| {
|
|
config.validate_windows_mxc_requirement()?;
|
|
Ok(config)
|
|
})
|
|
}
|
|
|
|
/// Validates saved and requested providers before resume overrides or policy can replace them.
|
|
pub(crate) async fn load_for_resume(
|
|
&self,
|
|
saved_provider: Option<&str>,
|
|
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
|
typesafe_overrides: ConfigOverrides,
|
|
cwd: Option<PathBuf>,
|
|
) -> std::io::Result<Config> {
|
|
let requested_provider = typesafe_overrides.model_provider.clone().or_else(|| {
|
|
request_overrides
|
|
.as_ref()
|
|
.and_then(|values| values.get("model_provider"))
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(str::to_owned)
|
|
});
|
|
// Resolve the selection before the saved provider is applied as a resume override.
|
|
let current_provider = self
|
|
.load_latest_config(typesafe_overrides.cwd.clone().or_else(|| cwd.clone()))
|
|
.await
|
|
.map(|config| config.model_provider_id);
|
|
self.load_with_validation(
|
|
&self.current_cli_overrides(),
|
|
request_overrides,
|
|
typesafe_overrides,
|
|
cwd,
|
|
|config, _overrides, layers| {
|
|
let requested_provider = requested_provider
|
|
.as_deref()
|
|
.or(config.model_provider.as_deref())
|
|
.unwrap_or("openai");
|
|
// Validate the saved route even when a client sends a provider override.
|
|
for provider in [
|
|
saved_provider.unwrap_or(requested_provider),
|
|
requested_provider,
|
|
] {
|
|
if let Some(required) = layers.required_model_provider()
|
|
&& required != provider
|
|
{
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::PermissionDenied,
|
|
ModelProviderError::ModelProviderPolicyConflict {
|
|
model_provider: provider.to_owned(),
|
|
required_model_provider: required.to_owned(),
|
|
},
|
|
));
|
|
}
|
|
if !config.model_providers.contains_key(provider)
|
|
&& !built_in_model_providers(config.openai_base_url.clone())
|
|
.contains_key(provider)
|
|
{
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
ModelProviderError::ModelProviderNotConfigured {
|
|
model_provider: provider.to_owned(),
|
|
},
|
|
));
|
|
}
|
|
}
|
|
let current_provider = current_provider?;
|
|
for provider in [
|
|
saved_provider.unwrap_or(requested_provider),
|
|
requested_provider,
|
|
] {
|
|
if provider != current_provider {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
ModelProviderError::ModelProviderMismatch {
|
|
model_provider: provider.to_owned(),
|
|
current_model_provider: current_provider,
|
|
},
|
|
));
|
|
}
|
|
}
|
|
Ok(())
|
|
},
|
|
)
|
|
.await
|
|
.and_then(|config| {
|
|
config.validate_windows_mxc_requirement()?;
|
|
Ok(config)
|
|
})
|
|
}
|
|
|
|
/// Reload sources using the task's session flags before materializing config.
|
|
pub(crate) async fn load_permission_config_for_thread(
|
|
&self,
|
|
thread_config: &Config,
|
|
cwd: AbsolutePathBuf,
|
|
permission_profile: String,
|
|
) -> std::io::Result<Config> {
|
|
let mut session_flags = TomlValue::Table(Default::default());
|
|
for layer in thread_config.config_layer_stack.layers_low_to_high() {
|
|
if matches!(layer.name, codex_config::ConfigLayerSource::SessionFlags) {
|
|
codex_config::merge_toml_values(&mut session_flags, &layer.config);
|
|
}
|
|
}
|
|
let overrides = session_flags
|
|
.as_table()
|
|
.ok_or_else(|| {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
"session flags must be a table",
|
|
)
|
|
})?
|
|
.iter()
|
|
.map(|(key, value)| (key.clone(), value.clone()))
|
|
.collect::<Vec<_>>();
|
|
self.load_with_cli_overrides(
|
|
&overrides,
|
|
/*request_overrides*/ None,
|
|
ConfigOverrides {
|
|
cwd: Some(cwd.to_path_buf()),
|
|
default_permissions: Some(permission_profile),
|
|
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
|
|
..Default::default()
|
|
},
|
|
Some(cwd.to_path_buf()),
|
|
)
|
|
.await
|
|
.and_then(|config| {
|
|
config.validate_windows_mxc_requirement()?;
|
|
Ok(config)
|
|
})
|
|
}
|
|
|
|
#[instrument(level = "trace", skip_all)]
|
|
pub(crate) async fn load_with_cli_overrides(
|
|
&self,
|
|
cli_overrides: &[(String, TomlValue)],
|
|
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
|
typesafe_overrides: ConfigOverrides,
|
|
fallback_cwd: Option<PathBuf>,
|
|
) -> std::io::Result<Config> {
|
|
self.load_with_validation(
|
|
cli_overrides,
|
|
request_overrides,
|
|
typesafe_overrides,
|
|
fallback_cwd,
|
|
|_, _, _| Ok(()),
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn load_with_validation(
|
|
&self,
|
|
cli_overrides: &[(String, TomlValue)],
|
|
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
|
mut typesafe_overrides: ConfigOverrides,
|
|
fallback_cwd: Option<PathBuf>,
|
|
validate: impl FnOnce(
|
|
&codex_config::config_toml::ConfigToml,
|
|
&ConfigOverrides,
|
|
&ConfigLayerStack,
|
|
) -> std::io::Result<()>,
|
|
) -> std::io::Result<Config> {
|
|
let mcp_ema_cancellation = self.capture_mcp_ema_cancellation();
|
|
let policy_load = self.refresh_application_network_policy().await?;
|
|
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::<Vec<_>>();
|
|
let result = 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(policy_load.cloud_config.clone())
|
|
.thread_config_loader(Arc::clone(&self.thread_config_loader))
|
|
.build_with_validation(validate)
|
|
.await;
|
|
let mut config = result?;
|
|
self.check_application_policy_load(&policy_load)?;
|
|
self.apply_network_policy(&mut config);
|
|
self.apply_runtime_feature_enablement(&mut config);
|
|
self.apply_arg0_paths(&mut config);
|
|
config.config_layer_stack = config
|
|
.config_layer_stack
|
|
.with_mcp_ema_cancellation(mcp_ema_cancellation);
|
|
Ok(config)
|
|
}
|
|
|
|
pub(crate) async fn load_config_layers_for_cwd(
|
|
&self,
|
|
cwd: AbsolutePathBuf,
|
|
) -> std::io::Result<ConfigLayerStack> {
|
|
self.load_config_layers(Some(cwd)).await
|
|
}
|
|
|
|
pub(crate) async fn load_config_layers(
|
|
&self,
|
|
cwd: Option<AbsolutePathBuf>,
|
|
) -> std::io::Result<ConfigLayerStack> {
|
|
let policy_load = self.refresh_application_network_policy().await?;
|
|
let 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: policy_load.cloud_config.clone(),
|
|
},
|
|
self.thread_config_loader.as_ref(),
|
|
)
|
|
.await;
|
|
let layers = result?;
|
|
self.check_application_policy_load(&policy_load)?;
|
|
Ok(layers)
|
|
}
|
|
|
|
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<String, bool> {
|
|
self.runtime_feature_enablement
|
|
.read()
|
|
.map(|guard| guard.clone())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn apply_network_policy(&self, config: &mut Config) {
|
|
config.application_network_policy = self.network_policy.policy();
|
|
config.application_auth_route_config = Some(
|
|
codex_login::AuthRouteConfig::from_http_client_factory(
|
|
config
|
|
.http_client_factory()
|
|
.with_network_policy(self.network_policy.policy()),
|
|
)
|
|
.with_local_bootstrap_factory(
|
|
config
|
|
.http_client_factory()
|
|
.with_network_policy(self.local_network_policy.policy()),
|
|
),
|
|
);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
#[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 = "application_network_tests.rs"]
|
|
mod application_network_tests;
|
|
|
|
#[cfg(test)]
|
|
#[path = "config_manager_provider_tests.rs"]
|
|
mod provider_tests;
|
|
|
|
pub(crate) fn protected_feature_keys(config_layer_stack: &ConfigLayerStack) -> BTreeSet<String> {
|
|
let mut protected_features = config_layer_stack
|
|
.effective_config()
|
|
.get("features")
|
|
.and_then(toml::Value::as_table)
|
|
.map(|features| features.keys().cloned().collect::<BTreeSet<_>>())
|
|
.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<String, bool>,
|
|
) {
|
|
config.runtime_feature_defaults = runtime_feature_enablement
|
|
.iter()
|
|
.filter_map(|(name, enabled)| feature_for_key(name).map(|feature| (feature, *enabled)))
|
|
.collect();
|
|
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"
|
|
);
|
|
}
|
|
}
|
|
}
|