From 88d1370d95b6ec58a0d723dedd1dd2f9b73d8393 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 17 Feb 2026 11:36:51 -0800 Subject: [PATCH 1/5] Reject built-in provider overrides --- codex-rs/core/config.schema.json | 2 +- codex-rs/core/src/config/config_tests.rs | 66 +++++++++++++++++++++++ codex-rs/core/src/config/mod.rs | 42 +++++++++++++-- codex-rs/core/src/config/service_tests.rs | 28 ++++++++++ 4 files changed, 134 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 2aa45fafdd82..bc407863f035 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2223,7 +2223,7 @@ "$ref": "#/definitions/ModelProviderInfo" }, "default": {}, - "description": "User-defined provider entries that extend/override the built-in list.", + "description": "User-defined provider entries that extend the built-in list. Built-in IDs cannot be overridden.", "type": "object" }, "model_reasoning_effort": { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index ec13247966a5..f1f4d0445013 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -159,6 +159,49 @@ consolidation_model = "gpt-5" ); } +#[test] +fn test_toml_parsing_rejects_reserved_model_provider_override_openai() { + let cfg = r#" +[model_providers.openai] +name = "OpenAI Custom" +"#; + + let err = toml::from_str::(cfg).expect_err("should reject reserved provider"); + let message = err.to_string(); + assert!(message.contains("reserved built-in provider IDs")); + assert!(message.contains("`openai`")); +} + +#[test] +fn test_toml_parsing_rejects_multiple_reserved_model_provider_overrides() { + let cfg = r#" +[model_providers.ollama] +name = "Ollama Override" + +[model_providers.openai] +name = "OpenAI Override" +"#; + + let err = toml::from_str::(cfg).expect_err("should reject reserved providers"); + let message = err.to_string(); + assert!(message.contains("`openai`")); + assert!(message.contains("`ollama`")); +} + +#[test] +fn test_toml_parsing_allows_non_reserved_model_provider() { + let cfg = r#" +[model_providers.openai-custom] +name = "OpenAI Custom" +base_url = "https://example.com/v1" +env_key = "OPENAI_API_KEY" +"#; + + let parsed = + toml::from_str::(cfg).expect("non-reserved provider should deserialize"); + assert!(parsed.model_providers.contains_key("openai-custom")); +} + #[test] fn parses_bundled_skills_config() { let cfg: ConfigToml = toml::from_str( @@ -4929,6 +4972,29 @@ fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error() -> Ok(()) } +#[test] +fn test_load_config_rejects_programmatic_reserved_model_provider_override() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let mut cfg = ConfigToml::default(); + cfg.model_providers.insert( + "openai".to_string(), + ModelProviderInfo::create_openai_provider(None), + ); + + let result = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides::default(), + codex_home.path().to_path_buf(), + ); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); + assert!(error.to_string().contains("reserved built-in provider IDs")); + assert!(error.to_string().contains("`openai`")); + + Ok(()) +} + #[test] fn test_untrusted_project_gets_workspace_write_sandbox() -> anyhow::Result<()> { let config_with_untrusted = r#" diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 18f9e24d148e..de50c810a4d6 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -139,6 +139,8 @@ pub(crate) const DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS: Option = None; pub const CONFIG_TOML_FILE: &str = "config.toml"; const OPENAI_BASE_URL_ENV_VAR: &str = "OPENAI_BASE_URL"; +const RESERVED_MODEL_PROVIDER_IDS: [&str; 3] = + ["openai", OLLAMA_OSS_PROVIDER_ID, LMSTUDIO_OSS_PROVIDER_ID]; fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option { let raw = std::env::var(codex_state::SQLITE_HOME_ENV).ok()?; @@ -367,7 +369,7 @@ pub struct Config { /// to 127.0.0.1 (using `mcp_oauth_callback_port` when provided). pub mcp_oauth_callback_url: Option, - /// Combined provider map (defaults merged with user-defined overrides). + /// Combined provider map (defaults plus user-defined providers). pub model_providers: HashMap, /// Maximum number of bytes to include from an AGENTS.md project doc file. @@ -1262,8 +1264,9 @@ pub struct ConfigToml { /// to 127.0.0.1 (using `mcp_oauth_callback_port` when provided). pub mcp_oauth_callback_url: Option, - /// User-defined provider entries that extend/override the built-in list. - #[serde(default)] + /// User-defined provider entries that extend the built-in list. Built-in + /// IDs cannot be overridden. + #[serde(default, deserialize_with = "deserialize_model_providers")] pub model_providers: HashMap, /// Maximum number of bytes to include from an AGENTS.md project doc file. @@ -1890,6 +1893,37 @@ pub struct ConfigOverrides { pub additional_writable_roots: Vec, } +fn validate_reserved_model_provider_ids( + model_providers: &HashMap, +) -> Result<(), String> { + let mut conflicts = model_providers + .keys() + .filter(|key| RESERVED_MODEL_PROVIDER_IDS.contains(&key.as_str())) + .map(|key| format!("`{key}`")) + .collect::>(); + conflicts.sort_unstable(); + if conflicts.is_empty() { + Ok(()) + } else { + Err(format!( + "model_providers contains reserved built-in provider IDs: {}. \ +Built-in providers cannot be overridden. Rename your custom provider (for example, `openai-custom`).", + conflicts.join(", ") + )) + } +} + +fn deserialize_model_providers<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let model_providers = HashMap::::deserialize(deserializer)?; + validate_reserved_model_provider_ids(&model_providers).map_err(serde::de::Error::custom)?; + Ok(model_providers) +} + /// Resolves the OSS provider from CLI override, profile config, or global config. /// Returns `None` if no provider is configured at any level. pub fn resolve_oss_provider( @@ -2011,6 +2045,8 @@ impl Config { codex_home: PathBuf, config_layer_stack: ConfigLayerStack, ) -> std::io::Result { + validate_reserved_model_provider_ids(&cfg.model_providers) + .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?; // Ensure that every field of ConfigRequirements is applied to the final // Config. let ConfigRequirements { diff --git a/codex-rs/core/src/config/service_tests.rs b/codex-rs/core/src/config/service_tests.rs index a23537e13998..bc3006a9a9c5 100644 --- a/codex-rs/core/src/config/service_tests.rs +++ b/codex-rs/core/src/config/service_tests.rs @@ -386,6 +386,34 @@ async fn invalid_user_value_rejected_even_if_overridden_by_managed() { assert_eq!(contents.trim(), "model = \"user\""); } +#[tokio::test] +async fn reserved_builtin_provider_override_rejected() { + let tmp = tempdir().expect("tempdir"); + std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n").unwrap(); + + let service = ConfigService::new_with_defaults(tmp.path().to_path_buf()); + let error = service + .write_value(ConfigValueWriteParams { + file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()), + key_path: "model_providers.openai.name".to_string(), + value: serde_json::json!("OpenAI Override"), + merge_strategy: MergeStrategy::Replace, + expected_version: None, + }) + .await + .expect_err("should reject reserved provider override"); + + assert_eq!( + error.write_error_code(), + Some(ConfigWriteErrorCode::ConfigValidationError) + ); + assert!(error.to_string().contains("reserved built-in provider IDs")); + assert!(error.to_string().contains("`openai`")); + + let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"); + assert_eq!(contents, "model = \"user\"\n"); +} + #[tokio::test] async fn write_value_rejects_feature_requirement_conflict() { let tmp = tempdir().expect("tempdir"); From bdc44811fb58318eb75471412627ed9b7e097c94 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 13 Mar 2026 20:36:38 -0600 Subject: [PATCH 2/5] codex: address PR review feedback (#12024) --- codex-rs/core/src/config/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index de50c810a4d6..a0d541a15d41 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -47,6 +47,7 @@ use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR; use crate::model_provider_info::OLLAMA_OSS_PROVIDER_ID; +use crate::model_provider_info::OPENAI_PROVIDER_ID; use crate::model_provider_info::built_in_model_providers; use crate::path_utils::normalize_for_native_workdir; use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME; @@ -139,8 +140,11 @@ pub(crate) const DEFAULT_AGENT_JOB_MAX_RUNTIME_SECONDS: Option = None; pub const CONFIG_TOML_FILE: &str = "config.toml"; const OPENAI_BASE_URL_ENV_VAR: &str = "OPENAI_BASE_URL"; -const RESERVED_MODEL_PROVIDER_IDS: [&str; 3] = - ["openai", OLLAMA_OSS_PROVIDER_ID, LMSTUDIO_OSS_PROVIDER_ID]; +const RESERVED_MODEL_PROVIDER_IDS: [&str; 3] = [ + OPENAI_PROVIDER_ID, + OLLAMA_OSS_PROVIDER_ID, + LMSTUDIO_OSS_PROVIDER_ID, +]; fn resolve_sqlite_home_env(resolved_cwd: &Path) -> Option { let raw = std::env::var(codex_state::SQLITE_HOME_ENV).ok()?; From 26373defb1821bd52195fe7fb78a636a6947fdab Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 13 Mar 2026 20:37:54 -0600 Subject: [PATCH 3/5] codex: address PR review feedback (#12024) --- codex-rs/core/src/config/config_tests.rs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index f1f4d0445013..fb06eb75c8d0 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4972,29 +4972,6 @@ fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error() -> Ok(()) } -#[test] -fn test_load_config_rejects_programmatic_reserved_model_provider_override() -> std::io::Result<()> { - let codex_home = TempDir::new()?; - let mut cfg = ConfigToml::default(); - cfg.model_providers.insert( - "openai".to_string(), - ModelProviderInfo::create_openai_provider(None), - ); - - let result = Config::load_from_base_config_with_overrides( - cfg, - ConfigOverrides::default(), - codex_home.path().to_path_buf(), - ); - assert!(result.is_err()); - let error = result.unwrap_err(); - assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); - assert!(error.to_string().contains("reserved built-in provider IDs")); - assert!(error.to_string().contains("`openai`")); - - Ok(()) -} - #[test] fn test_untrusted_project_gets_workspace_write_sandbox() -> anyhow::Result<()> { let config_with_untrusted = r#" From 8c6e7562c2fac68cb44da287893951d090590072 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 13 Mar 2026 20:39:57 -0600 Subject: [PATCH 4/5] codex: address PR review feedback (#12024) --- codex-rs/core/src/config/config_tests.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index fb06eb75c8d0..7734fb8950ce 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -188,20 +188,6 @@ name = "OpenAI Override" assert!(message.contains("`ollama`")); } -#[test] -fn test_toml_parsing_allows_non_reserved_model_provider() { - let cfg = r#" -[model_providers.openai-custom] -name = "OpenAI Custom" -base_url = "https://example.com/v1" -env_key = "OPENAI_API_KEY" -"#; - - let parsed = - toml::from_str::(cfg).expect("non-reserved provider should deserialize"); - assert!(parsed.model_providers.contains_key("openai-custom")); -} - #[test] fn parses_bundled_skills_config() { let cfg: ConfigToml = toml::from_str( From 187e6de8d641d995cee3da7bbdbc58c7d18df675 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 13 Mar 2026 20:44:17 -0600 Subject: [PATCH 5/5] codex: address PR review feedback (#12024) --- codex-rs/core/src/config/config_tests.rs | 29 ------------------------ 1 file changed, 29 deletions(-) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 7734fb8950ce..ec13247966a5 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -159,35 +159,6 @@ consolidation_model = "gpt-5" ); } -#[test] -fn test_toml_parsing_rejects_reserved_model_provider_override_openai() { - let cfg = r#" -[model_providers.openai] -name = "OpenAI Custom" -"#; - - let err = toml::from_str::(cfg).expect_err("should reject reserved provider"); - let message = err.to_string(); - assert!(message.contains("reserved built-in provider IDs")); - assert!(message.contains("`openai`")); -} - -#[test] -fn test_toml_parsing_rejects_multiple_reserved_model_provider_overrides() { - let cfg = r#" -[model_providers.ollama] -name = "Ollama Override" - -[model_providers.openai] -name = "OpenAI Override" -"#; - - let err = toml::from_str::(cfg).expect_err("should reject reserved providers"); - let message = err.to_string(); - assert!(message.contains("`openai`")); - assert!(message.contains("`ollama`")); -} - #[test] fn parses_bundled_skills_config() { let cfg: ConfigToml = toml::from_str(