From 7709bf32a34675e2a77ef5074d6d00faced3e5e8 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 2 Mar 2026 11:10:38 -0700 Subject: [PATCH] Fix project trust config parsing so CLI overrides work (#13090) Fixes #13076 This PR fixes a bug that causes command-line config overrides for MCP subtables to not be merged correctly. Summary - make project trust loading go through the dedicated struct so CLI overrides can update trusted project-local MCP transports --------- Co-authored-by: jif-oai --- codex-rs/core/src/config_loader/mod.rs | 16 ++++- codex-rs/core/src/config_loader/tests.rs | 85 ++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 5ce059a9f..63b3be48e 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -6,7 +6,6 @@ mod macos; mod tests; use crate::config::ConfigToml; -use crate::config::deserialize_config_toml_with_base; use crate::config_loader::layer_io::LoadedConfigLayers; use crate::git_info::resolve_root_git_project_for_trust; use codex_app_server_protocol::ConfigLayerSource; @@ -576,6 +575,11 @@ struct ProjectTrustContext { user_config_file: AbsolutePathBuf, } +#[derive(Deserialize)] +struct ProjectTrustConfigToml { + projects: Option>, +} + struct ProjectTrustDecision { trust_level: Option, trust_key: String, @@ -666,10 +670,16 @@ async fn project_trust_context( config_base_dir: &Path, user_config_file: &AbsolutePathBuf, ) -> io::Result { - let config_toml = deserialize_config_toml_with_base(merged_config.clone(), config_base_dir)?; + let project_trust_config: ProjectTrustConfigToml = { + let _guard = AbsolutePathBufGuard::new(config_base_dir); + merged_config + .clone() + .try_into() + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))? + }; let project_root = find_project_root(cwd, project_root_markers).await?; - let projects = config_toml.projects.unwrap_or_default(); + let projects = project_trust_config.projects.unwrap_or_default(); let project_root_key = project_root.as_path().to_string_lossy().to_string(); let repo_root = resolve_root_git_project_for_trust(cwd.as_path()); diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 05d5fa4c2..983acbe8d 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -1114,6 +1114,91 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result< Ok(()) } +#[tokio::test] +async fn cli_override_can_update_project_local_mcp_server_when_project_is_trusted() +-> std::io::Result<()> { + let tmp = tempdir()?; + let project_root = tmp.path().join("project"); + let nested = project_root.join("child"); + let dot_codex = project_root.join(".codex"); + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&nested).await?; + tokio::fs::create_dir_all(&dot_codex).await?; + tokio::fs::create_dir_all(&codex_home).await?; + tokio::fs::write(project_root.join(".git"), "gitdir: here").await?; + tokio::fs::write( + dot_codex.join(CONFIG_TOML_FILE), + r#" +[mcp_servers.sentry] +url = "https://mcp.sentry.dev/mcp" +enabled = false +"#, + ) + .await?; + make_config_for_test(&codex_home, &project_root, TrustLevel::Trusted, None).await?; + + let config = ConfigBuilder::default() + .codex_home(codex_home) + .cli_overrides(vec![( + "mcp_servers.sentry.enabled".to_string(), + TomlValue::Boolean(true), + )]) + .fallback_cwd(Some(nested)) + .build() + .await?; + + let server = config + .mcp_servers + .get() + .get("sentry") + .expect("trusted project MCP server should load"); + assert!(server.enabled); + + Ok(()) +} + +#[tokio::test] +async fn cli_override_for_disabled_project_local_mcp_server_returns_invalid_transport() +-> std::io::Result<()> { + let tmp = tempdir()?; + let project_root = tmp.path().join("project"); + let nested = project_root.join("child"); + let dot_codex = project_root.join(".codex"); + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&nested).await?; + tokio::fs::create_dir_all(&dot_codex).await?; + tokio::fs::create_dir_all(&codex_home).await?; + tokio::fs::write(project_root.join(".git"), "gitdir: here").await?; + tokio::fs::write( + dot_codex.join(CONFIG_TOML_FILE), + r#" +[mcp_servers.sentry] +url = "https://mcp.sentry.dev/mcp" +enabled = false +"#, + ) + .await?; + + let err = ConfigBuilder::default() + .codex_home(codex_home) + .cli_overrides(vec![( + "mcp_servers.sentry.enabled".to_string(), + TomlValue::Boolean(true), + )]) + .fallback_cwd(Some(nested)) + .build() + .await + .expect_err("untrusted project layer should not provide MCP transport"); + + assert!( + err.to_string().contains("invalid transport") + && err.to_string().contains("mcp_servers.sentry"), + "unexpected error: {err}" + ); + + Ok(()) +} + #[tokio::test] async fn invalid_project_config_ignored_when_untrusted_or_unknown() -> std::io::Result<()> { let tmp = tempdir()?;