From be3eb41118fa4f92a2a2a455a1079e202e1a7089 Mon Sep 17 00:00:00 2001 From: Rennie Song Date: Fri, 3 Jul 2026 05:10:03 +0000 Subject: [PATCH] allow extension-managed Codex Apps auth --- codex-rs/codex-mcp/src/lib.rs | 1 + codex-rs/codex-mcp/src/mcp/mod.rs | 17 ++++- codex-rs/codex-mcp/src/mcp/mod_tests.rs | 82 +++++++++++++++++++++++ codex-rs/core/src/session/review.rs | 1 + codex-rs/core/src/session/turn_context.rs | 9 ++- 5 files changed, 108 insertions(+), 2 deletions(-) diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index eda7ac2399..29f005166c 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -43,6 +43,7 @@ pub use mcp::codex_apps_mcp_server_config; pub use mcp::configured_mcp_servers; pub use mcp::effective_mcp_servers; pub use mcp::effective_mcp_servers_from_configured; +pub use mcp::extension_managed_codex_apps_enabled; pub use mcp::host_owned_codex_apps_enabled; pub use mcp::hosted_plugin_runtime_mcp_server_config; pub use mcp::tool_plugin_provenance; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index ffb797f26e..7e181278dc 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -43,6 +43,7 @@ use rmcp::model::ReadResourceResult; use serde_json::Value; use tokio_util::sync::CancellationToken; +use crate::McpServerSource; use crate::ResolvedMcpCatalog; use crate::codex_apps_cache::CodexAppsToolsCache; use crate::codex_apps_cache::codex_apps_tools_cache_key; @@ -238,8 +239,22 @@ impl ToolPluginProvenance { } } +pub fn extension_managed_codex_apps_enabled(config: &McpConfig) -> bool { + // Trusted host extensions may replace the ChatGPT-backed server with one + // that supplies its own request authentication. + config.apps_enabled + && config + .mcp_server_catalog + .server(CODEX_APPS_MCP_SERVER_NAME) + .is_some_and(|server| { + matches!(server.source(), McpServerSource::Extension { .. }) + && matches!(&server.config().auth, &McpServerAuth::OAuth) + }) +} + pub fn host_owned_codex_apps_enabled(config: &McpConfig, auth: Option<&CodexAuth>) -> bool { - config.apps_enabled && auth.is_some_and(CodexAuth::uses_codex_backend) + extension_managed_codex_apps_enabled(config) + || (config.apps_enabled && auth.is_some_and(CodexAuth::uses_codex_backend)) } pub fn configured_mcp_servers(config: &McpConfig) -> HashMap { diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index 7d166cbab8..458f0debd6 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -285,6 +285,88 @@ fn codex_apps_server_config_forwards_configured_product_sku_header() { } } +#[test] +fn effective_mcp_servers_preserve_extension_owned_oauth_codex_apps_without_chatgpt_auth() { + let mut config = test_mcp_config(PathBuf::new()); + config.apps_enabled = true; + + let compatibility_config = + codex_apps_mcp_server_config(&config.chatgpt_base_url, /*apps_mcp_product_sku*/ None); + let mut extension_config = compatibility_config.clone(); + extension_config.auth = McpServerAuth::OAuth; + let McpServerTransportConfig::StreamableHttp { http_headers, .. } = + &mut extension_config.transport + else { + panic!("expected streamable http transport for codex apps"); + }; + *http_headers = Some(HashMap::from([( + "X-OpenAI-Actor-Authorization".to_string(), + "actor-biscuit".to_string(), + )])); + let expected = extension_config.clone(); + + let mut catalog = ResolvedMcpCatalog::builder(); + catalog.register(McpServerRegistration::from_compatibility( + CODEX_APPS_MCP_SERVER_NAME.to_string(), + "legacy_codex_apps".to_string(), + compatibility_config, + )); + catalog.register(McpServerRegistration::from_extension( + CODEX_APPS_MCP_SERVER_NAME.to_string(), + "host_managed_apps".to_string(), + /*contribution_order*/ 0, + extension_config, + )); + config.mcp_server_catalog = catalog.build(); + + let effective = effective_mcp_servers(&config, /*auth*/ None); + + assert_eq!( + effective + .get(CODEX_APPS_MCP_SERVER_NAME) + .and_then(EffectiveMcpServer::configured_config), + Some(&expected) + ); +} + +#[test] +fn effective_mcp_servers_hide_chatgpt_codex_apps_without_chatgpt_auth() { + let mut config = test_mcp_config(PathBuf::new()); + config.apps_enabled = true; + + let mut catalog = ResolvedMcpCatalog::builder(); + catalog.register(McpServerRegistration::from_compatibility( + CODEX_APPS_MCP_SERVER_NAME.to_string(), + "legacy_codex_apps".to_string(), + codex_apps_mcp_server_config(&config.chatgpt_base_url, /*apps_mcp_product_sku*/ None), + )); + config.mcp_server_catalog = catalog.build(); + + let effective = effective_mcp_servers(&config, /*auth*/ None); + + assert!(!effective.contains_key(CODEX_APPS_MCP_SERVER_NAME)); +} + +#[test] +fn effective_mcp_servers_hide_config_owned_oauth_codex_apps_without_chatgpt_auth() { + let mut config = test_mcp_config(PathBuf::new()); + config.apps_enabled = true; + let mut server_config = + codex_apps_mcp_server_config(&config.chatgpt_base_url, /*apps_mcp_product_sku*/ None); + server_config.auth = McpServerAuth::OAuth; + + let mut catalog = ResolvedMcpCatalog::builder(); + catalog.register(McpServerRegistration::from_config( + CODEX_APPS_MCP_SERVER_NAME.to_string(), + server_config, + )); + config.mcp_server_catalog = catalog.build(); + + let effective = effective_mcp_servers(&config, /*auth*/ None); + + assert!(!effective.contains_key(CODEX_APPS_MCP_SERVER_NAME)); +} + #[tokio::test] async fn effective_mcp_servers_preserve_runtime_servers() { let codex_home = tempfile::tempdir().expect("tempdir"); diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 1587290d33..eb4b0f2bd6 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -111,6 +111,7 @@ pub(super) async fn spawn_review_thread( realtime_active: parent_turn_context.realtime_active, config: per_turn_config, auth_manager: auth_manager_for_context, + extension_managed_apps_enabled: parent_turn_context.extension_managed_apps_enabled, model_info: model_info.clone(), session_telemetry: session_telemetry_for_context, provider: provider_for_context, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index b9bf42073b..08847177d6 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -105,6 +105,7 @@ pub struct TurnContext { pub(crate) realtime_active: bool, pub config: Arc, pub(crate) auth_manager: Option>, + pub(crate) extension_managed_apps_enabled: bool, pub(crate) model_info: ModelInfo, pub(crate) session_telemetry: SessionTelemetry, pub(crate) provider: SharedModelProvider, @@ -201,7 +202,7 @@ impl TurnContext { .is_some_and(AuthManager::current_auth_uses_codex_backend); self.config .features - .apps_enabled_for_auth(uses_codex_backend) + .apps_enabled_for_auth(uses_codex_backend || self.extension_managed_apps_enabled) && self.config.orchestrator_mcp_enabled } @@ -253,6 +254,7 @@ impl TurnContext { realtime_active: self.realtime_active, config: Arc::new(config), auth_manager: self.auth_manager.clone(), + extension_managed_apps_enabled: self.extension_managed_apps_enabled, model_info: model_info.clone(), session_telemetry: self .session_telemetry @@ -534,6 +536,7 @@ impl Session { realtime_active: false, config: per_turn_config, auth_manager: auth_manager_for_context, + extension_managed_apps_enabled: false, model_info, session_telemetry: session_telemetry_for_context, provider: provider_for_context, @@ -763,6 +766,10 @@ impl Session { sub_id, skills_snapshot, ); + turn_context.extension_managed_apps_enabled = + codex_mcp::extension_managed_codex_apps_enabled( + self.services.latest_mcp_runtime().config(), + ); turn_context.realtime_active = self.conversation.running_state().await.is_some(); if let Some(final_schema) = final_output_json_schema {