From 3da65a31b34ac1240a224507a69252524c117d54 Mon Sep 17 00:00:00 2001 From: raquel Date: Tue, 7 Jul 2026 14:56:01 -0700 Subject: [PATCH 1/3] fix: forward originator to Codex Apps MCP --- codex-rs/codex-mcp/src/mcp/mod.rs | 10 +++++--- codex-rs/codex-mcp/src/mcp/mod_tests.rs | 34 ++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 87ea25dd9f16..a565c0c2ce96 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -29,6 +29,7 @@ use codex_config::types::AuthKeyringBackendKind; use codex_config::types::OAuthCredentialsStoreMode; use codex_connectors::ConnectorSnapshot; use codex_login::CodexAuth; +use codex_login::default_client::originator; use codex_model_provider::CHATGPT_CODEX_BASE_URL; use codex_protocol::mcp::McpServerInfo; use codex_protocol::mcp::Resource; @@ -521,15 +522,16 @@ fn mcp_server_config_for_url( apps_mcp_product_sku: Option<&str>, auth_mode: McpServerAuth, ) -> McpServerConfig { - let http_headers = apps_mcp_product_sku.map(|product_sku| { - HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())]) - }); + let mut http_headers = HashMap::from([("originator".to_string(), originator().value)]); + if let Some(product_sku) = apps_mcp_product_sku { + http_headers.insert("X-OpenAI-Product-Sku".to_string(), product_sku.to_string()); + } McpServerConfig { transport: McpServerTransportConfig::StreamableHttp { url, bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(), - http_headers, + http_headers: Some(http_headers), env_http_headers: None, }, auth: auth_mode, diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index 7d166cbab80f..17017340dd56 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -263,8 +263,10 @@ fn codex_apps_server_config_uses_legacy_codex_apps_path() { } #[test] -fn codex_apps_server_config_forwards_configured_product_sku_header() { - let config = codex_apps_mcp_server_config("https://chatgpt.com", Some("tpp")); +fn codex_apps_server_config_forwards_originator_header() { + let expected_originator = codex_login::default_client::originator().value; + let config = + codex_apps_mcp_server_config("https://chatgpt.com", /*apps_mcp_product_sku*/ None); match &config.transport { McpServerTransportConfig::StreamableHttp { @@ -275,8 +277,8 @@ fn codex_apps_server_config_forwards_configured_product_sku_header() { assert_eq!( http_headers, &Some(HashMap::from([( - "X-OpenAI-Product-Sku".to_string(), - "tpp".to_string(), + "originator".to_string(), + expected_originator, )])) ); assert!(env_http_headers.is_none()); @@ -285,6 +287,30 @@ fn codex_apps_server_config_forwards_configured_product_sku_header() { } } +#[test] +fn codex_apps_server_config_forwards_originator_and_configured_product_sku_headers() { + let expected_originator = codex_login::default_client::originator().value; + let config = codex_apps_mcp_server_config("https://chatgpt.com", Some("tpp")); + + match &config.transport { + McpServerTransportConfig::StreamableHttp { + http_headers, + env_http_headers, + .. + } => { + assert_eq!( + http_headers, + &Some(HashMap::from([ + ("originator".to_string(), expected_originator), + ("X-OpenAI-Product-Sku".to_string(), "tpp".to_string()), + ])) + ); + assert!(env_http_headers.is_none()); + } + other => panic!("expected streamable http transport, got {other:?}"), + } +} + #[tokio::test] async fn effective_mcp_servers_preserve_runtime_servers() { let codex_home = tempfile::tempdir().expect("tempdir"); From 41f09ce711555ef1d818aac1593ff6898f4ff493 Mon Sep 17 00:00:00 2001 From: raquel Date: Wed, 8 Jul 2026 10:06:56 -0700 Subject: [PATCH 2/3] fix: use thread originator for Apps MCP --- .../codex-mcp/src/connection_manager_tests.rs | 2 + codex-rs/codex-mcp/src/mcp/mod.rs | 12 ++++-- codex-rs/codex-mcp/src/mcp/mod_tests.rs | 41 +++++++++++++------ codex-rs/core/src/mcp.rs | 27 ++++++++---- codex-rs/core/src/mcp_tool_call_tests.rs | 1 + codex-rs/core/src/session/mcp.rs | 5 +++ codex-rs/core/src/session/session.rs | 2 + codex-rs/core/src/thread_manager_tests.rs | 26 +++++++++++- 8 files changed, 91 insertions(+), 25 deletions(-) diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 41fc19a594f5..541b2faab760 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -1418,6 +1418,7 @@ fn server_metadata_preserves_tool_approval_policy() { let mut config = crate::codex_apps_mcp_server_config( "https://docs.example", /*apps_mcp_product_sku*/ None, + /*originator*/ None, ); config.environment_id = "remote".to_string(); config.default_tools_approval_mode = Some(AppToolApproval::Prompt); @@ -1462,6 +1463,7 @@ fn host_owned_codex_apps_matches_reserved_name_with_server_metadata() { let server = EffectiveMcpServer::configured(crate::codex_apps_mcp_server_config( "https://chatgpt.com", /*apps_mcp_product_sku*/ None, + /*originator*/ None, )); manager.server_metadata.insert( CODEX_APPS_MCP_SERVER_NAME.to_string(), diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index a565c0c2ce96..3834cde4499f 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -29,7 +29,6 @@ use codex_config::types::AuthKeyringBackendKind; use codex_config::types::OAuthCredentialsStoreMode; use codex_connectors::ConnectorSnapshot; use codex_login::CodexAuth; -use codex_login::default_client::originator; use codex_model_provider::CHATGPT_CODEX_BASE_URL; use codex_protocol::mcp::McpServerInfo; use codex_protocol::mcp::Resource; @@ -491,10 +490,12 @@ fn codex_apps_mcp_url_for_base_url(base_url: &str) -> String { pub fn codex_apps_mcp_server_config( chatgpt_base_url: &str, apps_mcp_product_sku: Option<&str>, + originator: Option<&str>, ) -> McpServerConfig { mcp_server_config_for_url( codex_apps_mcp_url_for_base_url(chatgpt_base_url), apps_mcp_product_sku, + originator, McpServerAuth::ChatGpt, ) } @@ -513,6 +514,7 @@ pub fn hosted_plugin_runtime_mcp_server_config( mcp_server_config_for_url( format!("{base_url}/ps/mcp"), apps_mcp_product_sku, + /*originator*/ None, McpServerAuth::ChatGpt, ) } @@ -520,9 +522,13 @@ pub fn hosted_plugin_runtime_mcp_server_config( fn mcp_server_config_for_url( url: String, apps_mcp_product_sku: Option<&str>, + originator: Option<&str>, auth_mode: McpServerAuth, ) -> McpServerConfig { - let mut http_headers = HashMap::from([("originator".to_string(), originator().value)]); + let mut http_headers = HashMap::new(); + if let Some(originator) = originator { + http_headers.insert("originator".to_string(), originator.to_string()); + } if let Some(product_sku) = apps_mcp_product_sku { http_headers.insert("X-OpenAI-Product-Sku".to_string(), product_sku.to_string()); } @@ -531,7 +537,7 @@ fn mcp_server_config_for_url( transport: McpServerTransportConfig::StreamableHttp { url, bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(), - http_headers: Some(http_headers), + http_headers: (!http_headers.is_empty()).then_some(http_headers), env_http_headers: None, }, auth: auth_mode, diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index 17017340dd56..edc17ef457bf 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -130,7 +130,11 @@ fn tool_plugin_provenance_collects_app_and_mcp_sources() { "alpha".to_string(), McpPluginAttribution::new("alpha@test".to_string(), "alpha-plugin".to_string()), /*plugin_order*/ 0, - codex_apps_mcp_server_config("https://alpha.example", /*apps_mcp_product_sku*/ None), + codex_apps_mcp_server_config( + "https://alpha.example", + /*apps_mcp_product_sku*/ None, + /*originator*/ None, + ), )); config.mcp_server_catalog = catalog.build(); config.connector_snapshot = @@ -197,7 +201,11 @@ fn selected_mcp_attribution_does_not_join_an_unrelated_local_summary() { "Executor GitHub".to_string(), ), /*selection_order*/ 0, - codex_apps_mcp_server_config("https://github.example", /*apps_mcp_product_sku*/ None), + codex_apps_mcp_server_config( + "https://github.example", + /*apps_mcp_product_sku*/ None, + /*originator*/ None, + ), )); config.mcp_server_catalog = catalog.build(); config.connector_snapshot = @@ -252,8 +260,11 @@ fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() { #[test] fn codex_apps_server_config_uses_legacy_codex_apps_path() { - let config = - codex_apps_mcp_server_config("https://chatgpt.com", /*apps_mcp_product_sku*/ None); + let config = codex_apps_mcp_server_config( + "https://chatgpt.com", + /*apps_mcp_product_sku*/ None, + /*originator*/ None, + ); let url = match &config.transport { McpServerTransportConfig::StreamableHttp { url, .. } => url, _ => panic!("expected streamable http transport for codex apps"), @@ -263,10 +274,12 @@ fn codex_apps_server_config_uses_legacy_codex_apps_path() { } #[test] -fn codex_apps_server_config_forwards_originator_header() { - let expected_originator = codex_login::default_client::originator().value; - let config = - codex_apps_mcp_server_config("https://chatgpt.com", /*apps_mcp_product_sku*/ None); +fn codex_apps_server_config_forwards_thread_originator_header() { + let config = codex_apps_mcp_server_config( + "https://chatgpt.com", + /*apps_mcp_product_sku*/ None, + Some("thread_originator"), + ); match &config.transport { McpServerTransportConfig::StreamableHttp { @@ -278,7 +291,7 @@ fn codex_apps_server_config_forwards_originator_header() { http_headers, &Some(HashMap::from([( "originator".to_string(), - expected_originator, + "thread_originator".to_string(), )])) ); assert!(env_http_headers.is_none()); @@ -289,8 +302,11 @@ fn codex_apps_server_config_forwards_originator_header() { #[test] fn codex_apps_server_config_forwards_originator_and_configured_product_sku_headers() { - let expected_originator = codex_login::default_client::originator().value; - let config = codex_apps_mcp_server_config("https://chatgpt.com", Some("tpp")); + let config = codex_apps_mcp_server_config( + "https://chatgpt.com", + Some("tpp"), + Some("thread_originator"), + ); match &config.transport { McpServerTransportConfig::StreamableHttp { @@ -301,7 +317,7 @@ fn codex_apps_server_config_forwards_originator_and_configured_product_sku_heade assert_eq!( http_headers, &Some(HashMap::from([ - ("originator".to_string(), expected_originator), + ("originator".to_string(), "thread_originator".to_string()), ("X-OpenAI-Product-Sku".to_string(), "tpp".to_string()), ])) ); @@ -376,6 +392,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() { codex_apps_mcp_server_config( &config.chatgpt_base_url, config.apps_mcp_product_sku.as_deref(), + /*originator*/ None, ), )); config.mcp_server_catalog = catalog.build(); diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index 835c47ff5101..20b459d9828b 100644 --- a/codex-rs/core/src/mcp.rs +++ b/codex-rs/core/src/mcp.rs @@ -79,9 +79,12 @@ impl McpManager { /// Returns the MCP config after applying compatibility built-ins and /// runtime-only extension overlays. pub async fn runtime_config(&self, config: &Config) -> McpConfig { - self.runtime_config_with_context(McpServerContributionContext::global(config)) - .await - .config + self.runtime_config_with_context( + McpServerContributionContext::global(config), + /*originator*/ None, + ) + .await + .config } pub(crate) async fn runtime_config_for_step( @@ -89,20 +92,25 @@ impl McpManager { config: &Config, thread_init: &ExtensionDataInit, thread_store: &ExtensionData, + originator: &str, available_environment_ids: &[String], ) -> McpRuntimeProjection { - self.runtime_config_with_context(McpServerContributionContext::for_step( - config, - thread_init, - thread_store, - available_environment_ids, - )) + self.runtime_config_with_context( + McpServerContributionContext::for_step( + config, + thread_init, + thread_store, + available_environment_ids, + ), + Some(originator), + ) .await } async fn runtime_config_with_context( &self, context: McpServerContributionContext<'_, Config>, + originator: Option<&str>, ) -> McpRuntimeProjection { let config = context.config(); let mut selected_plugin_available = false; @@ -181,6 +189,7 @@ impl McpManager { codex_apps_mcp_server_config( &mcp_config.chatgpt_base_url, mcp_config.apps_mcp_product_sku.as_deref(), + originator, ), )); } else { diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 277caa482dd0..a2c662a5bb23 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1435,6 +1435,7 @@ async fn host_owned_codex_apps_manager( codex_mcp::EffectiveMcpServer::configured(codex_mcp::codex_apps_mcp_server_config( "https://chatgpt.com", /*apps_mcp_product_sku*/ None, + Some(&turn_context.originator), )), )]); let manager = codex_mcp::McpConnectionManager::new( diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 3f5e92eca7c5..3cc408f29179 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -78,6 +78,7 @@ impl ElicitationReviewer for GuardianMcpElicitationReviewer { impl Session { pub(crate) async fn runtime_mcp_config(&self, config: &Config) -> McpConfig { + let originator = self.originator().await; let environments = self.services.turn_environments.snapshot().await; let selected_capability_roots = self .resolve_selected_capability_roots_for_step(&environments) @@ -90,6 +91,7 @@ impl Session { config, &self.services.mcp_thread_init, &self.services.thread_extension_data, + &originator, &available_environment_ids, ) .await @@ -132,6 +134,7 @@ impl Session { &turn_context.config, &self.services.mcp_thread_init, &self.services.thread_extension_data, + &turn_context.originator, &available_environment_ids, ) .await; @@ -471,6 +474,7 @@ impl Session { &refresh_config, &self.services.mcp_thread_init, &self.services.thread_extension_data, + &turn_context.originator, &available_environment_ids, ) .await; @@ -539,6 +543,7 @@ impl Session { refresh_config, &self.services.mcp_thread_init, &self.services.thread_extension_data, + &turn_context.originator, &available_environment_ids, ) .await; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 08abb4637749..64c7b19c4490 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -667,6 +667,7 @@ impl Session { let mcp_manager_for_mcp = Arc::clone(&mcp_manager); let mcp_thread_init_for_startup = &mcp_thread_init; let thread_extension_data_for_mcp = &thread_extension_data; + let mcp_originator = session_configuration.originator.clone(); let mcp_runtime_cwd = session_configuration .environment_selections() .first() @@ -683,6 +684,7 @@ impl Session { &config_for_mcp, mcp_thread_init_for_startup, thread_extension_data_for_mcp, + &mcp_originator, /*available_environment_ids*/ &[], ) .await; diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 3c1d0146e7f4..b97a20725d80 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -553,6 +553,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() let mut server = codex_mcp::codex_apps_mcp_server_config( "https://selected.invalid", /*apps_mcp_product_sku*/ None, + /*originator*/ None, ); let CapabilityRootLocation::Environment { environment_id, .. } = &selected_root.location; @@ -574,6 +575,10 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() let mut config = test_config().await; config.codex_home = temp_dir.path().join("codex-home").abs(); config.cwd = config.codex_home.abs(); + config + .features + .enable(Feature::Apps) + .expect("test config should allow apps"); std::fs::create_dir_all(&config.codex_home).expect("create codex home"); let lifecycle_observed = Arc::new(std::sync::Mutex::new(Vec::new())); @@ -620,7 +625,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() session_source: None, thread_source: None, dynamic_tools: Vec::new(), - metrics_service_name: None, + metrics_service_name: Some("codex_work_desktop".to_string()), parent_trace: None, environments: Vec::new(), thread_extension_init: selected_root_init("selected-a", "env-a"), @@ -646,6 +651,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() .await .expect("start second thread"); let first_session = &first_thread.thread.codex.session; + let first_originator = first_session.originator().await; let first_resolved = first_session .services .mcp_manager @@ -653,10 +659,12 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() &config, &first_session.services.mcp_thread_init, &first_session.services.thread_extension_data, + &first_originator, /*available_environment_ids*/ &[], ) .await; let second_session = &second_thread.thread.codex.session; + let second_originator = second_session.originator().await; let second_resolved = second_session .services .mcp_manager @@ -664,6 +672,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() &config, &second_session.services.mcp_thread_init, &second_session.services.thread_extension_data, + &second_originator, /*available_environment_ids*/ &[], ) .await; @@ -706,6 +715,21 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors() selected_servers(&second_resolved.config), std::collections::BTreeMap::from([("selected-b".to_string(), "env-b".to_string())]) ); + let codex_apps_server = codex_mcp::configured_mcp_servers(&first_resolved.config) + .remove(codex_mcp::CODEX_APPS_MCP_SERVER_NAME) + .expect("Codex Apps server should be configured"); + let codex_apps_headers = match codex_apps_server.transport { + codex_config::McpServerTransportConfig::StreamableHttp { http_headers, .. } => http_headers, + codex_config::McpServerTransportConfig::Stdio { .. } => { + panic!("Codex Apps server should use streamable HTTP") + } + }; + assert_eq!( + codex_apps_headers + .expect("Codex Apps headers should be configured") + .get("originator"), + Some(&"codex_work_desktop".to_string()) + ); } #[tokio::test] From 5cbdc3b3976aae6710142da3f9dfdce02c9068e3 Mon Sep 17 00:00:00 2001 From: raquel Date: Wed, 8 Jul 2026 10:27:18 -0700 Subject: [PATCH 3/3] fix: forward originator through hosted Apps MCP --- codex-rs/codex-mcp/src/mcp/mod.rs | 3 +- codex-rs/core/src/mcp.rs | 1 + .../ext/extension-api/src/contributors/mcp.rs | 10 ++++ codex-rs/ext/mcp/src/lib.rs | 54 +++++++++++++++++++ codex-rs/ext/mcp/tests/executor_plugin_mcp.rs | 1 + 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 3834cde4499f..ce387affbc25 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -504,6 +504,7 @@ pub fn codex_apps_mcp_server_config( pub fn hosted_plugin_runtime_mcp_server_config( chatgpt_base_url: &str, apps_mcp_product_sku: Option<&str>, + originator: Option<&str>, ) -> McpServerConfig { let base_url = normalize_codex_apps_base_url(chatgpt_base_url); let base_url = if base_url.contains("/backend-api") || base_url.contains("/api/codex") { @@ -514,7 +515,7 @@ pub fn hosted_plugin_runtime_mcp_server_config( mcp_server_config_for_url( format!("{base_url}/ps/mcp"), apps_mcp_product_sku, - /*originator*/ None, + originator, McpServerAuth::ChatGpt, ) } diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index 20b459d9828b..87a450b3238b 100644 --- a/codex-rs/core/src/mcp.rs +++ b/codex-rs/core/src/mcp.rs @@ -100,6 +100,7 @@ impl McpManager { config, thread_init, thread_store, + originator, available_environment_ids, ), Some(originator), diff --git a/codex-rs/ext/extension-api/src/contributors/mcp.rs b/codex-rs/ext/extension-api/src/contributors/mcp.rs index 3fe648a1e946..8f7598e26438 100644 --- a/codex-rs/ext/extension-api/src/contributors/mcp.rs +++ b/codex-rs/ext/extension-api/src/contributors/mcp.rs @@ -15,6 +15,8 @@ pub struct McpServerContributionContext<'a, C> { thread_store: Option<&'a ExtensionData>, /// Stable host inputs for the active thread, when resolution is thread-scoped. thread_init: Option<&'a ExtensionDataInit>, + /// Effective request originator for the active thread, when resolution is thread-scoped. + originator: Option<&'a str>, /// Environment IDs whose selected roots may contribute to this exact step. available_environment_ids: Option<&'a [String]>, } @@ -34,6 +36,7 @@ impl<'a, C> McpServerContributionContext<'a, C> { config, thread_store: None, thread_init: None, + originator: None, available_environment_ids: None, } } @@ -43,12 +46,14 @@ impl<'a, C> McpServerContributionContext<'a, C> { config: &'a C, thread_init: &'a ExtensionDataInit, thread_store: &'a ExtensionData, + originator: &'a str, available_environment_ids: &'a [String], ) -> Self { Self { config, thread_store: Some(thread_store), thread_init: Some(thread_init), + originator: Some(originator), available_environment_ids: Some(available_environment_ids), } } @@ -68,6 +73,11 @@ impl<'a, C> McpServerContributionContext<'a, C> { self.thread_init } + /// Returns the effective request originator when resolving for a running thread. + pub fn originator(&self) -> Option<&'a str> { + self.originator + } + /// Returns the exact environment availability projection for a model step. /// /// `Some` means contributors must omit selected roots whose environment ID is absent from the diff --git a/codex-rs/ext/mcp/src/lib.rs b/codex-rs/ext/mcp/src/lib.rs index 9c22bad5a0a1..f915f3e39350 100644 --- a/codex-rs/ext/mcp/src/lib.rs +++ b/codex-rs/ext/mcp/src/lib.rs @@ -32,6 +32,7 @@ impl McpServerContributor for HostedPluginRuntimeExtension { config: Box::new(hosted_plugin_runtime_mcp_server_config( &config.chatgpt_base_url, config.apps_mcp_product_sku.as_deref(), + context.originator(), )), }] }) @@ -51,3 +52,56 @@ pub fn install_executor_plugins( executor_plugin::SelectedExecutorPluginMcpContributor::new(environment_manager), )); } + +#[cfg(test)] +mod tests { + use super::*; + use codex_config::McpServerTransportConfig; + use codex_core::config::ConfigBuilder; + use codex_extension_api::ExtensionData; + use codex_extension_api::ExtensionDataInit; + use pretty_assertions::assert_eq; + + #[tokio::test] + async fn hosted_plugin_runtime_forwards_thread_originator() + -> Result<(), Box> { + let codex_home = tempfile::tempdir()?; + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cli_overrides(vec![ + ("features.apps".to_string(), true.into()), + ("chatgpt_base_url".to_string(), "https://chatgpt.com".into()), + ]) + .build() + .await?; + let thread_init = ExtensionDataInit::new(); + let thread_store = ExtensionData::new("thread"); + + let contributions = HostedPluginRuntimeExtension + .contribute(McpServerContributionContext::for_step( + &config, + &thread_init, + &thread_store, + "codex_work_desktop", + /*available_environment_ids*/ &[], + )) + .await; + let [McpServerContribution::Set { config: server, .. }] = contributions.as_slice() else { + panic!("hosted plugin runtime should contribute one server"); + }; + let McpServerTransportConfig::StreamableHttp { http_headers, .. } = &server.transport + else { + panic!("hosted plugin runtime should use streamable HTTP"); + }; + + assert_eq!( + http_headers + .as_ref() + .and_then(|headers| headers.get("originator")), + Some(&"codex_work_desktop".to_string()) + ); + + Ok(()) + } +} diff --git a/codex-rs/ext/mcp/tests/executor_plugin_mcp.rs b/codex-rs/ext/mcp/tests/executor_plugin_mcp.rs index da5c1fb6ad43..afb6b0b923ac 100644 --- a/codex-rs/ext/mcp/tests/executor_plugin_mcp.rs +++ b/codex-rs/ext/mcp/tests/executor_plugin_mcp.rs @@ -202,6 +202,7 @@ async fn raw_selected_plugin_contributions( config, &thread_init, &thread_store, + "test_originator", &available_environment_ids, )) .await)