From 3fd5160cd6c78f2051bb54359d53f09207171733 Mon Sep 17 00:00:00 2001 From: Jesse Du Date: Mon, 21 Sep 2026 22:17:33 +0000 Subject: [PATCH] Honor the configured product SKU in remote plugin requests (#47116) ## Why Remote plugin requests always sent `OAI-Product-Sku: codex`, ignoring the existing `apps_mcp_product_sku` setting. ## What changed Pass `apps_mcp_product_sku` through plugin configuration and use it for authenticated remote plugin requests, including discovery, search, installation, and sharing. Keep `codex` as the default and include the SKU when comparing remote service configurations. ## Testing Extend app-server plugin listing and core agent-turn tests to check default and configured SKU headers. Update the CLI worktree test to wait for the user prompt's model request, skipping background title generation requests. GitOrigin-RevId: 508c9ef9aa3e41b87c92383c3088eb1eb9227fd8 --- .../src/request_processors/plugins.rs | 5 +- .../app-server/tests/suite/v2/plugin_list.rs | 24 ++++--- codex-rs/cli/tests/worktree.rs | 15 ++++- .../core-plugins/src/discoverable_tests.rs | 1 + codex-rs/core-plugins/src/manager.rs | 4 ++ codex-rs/core-plugins/src/manager_tests.rs | 5 ++ .../marketplace_policy/curated_skill_tests.rs | 1 + codex-rs/core-plugins/src/remote.rs | 66 +++++++++++++++---- .../src/remote/catalog_cache_tests.rs | 2 + .../src/remote/plugin_capabilities_tests.rs | 1 + .../remote/remote_installed_plugin_sync.rs | 1 + codex-rs/core-plugins/src/remote/search.rs | 6 +- codex-rs/core-plugins/src/remote/share.rs | 53 ++++++++++----- .../core-plugins/src/remote/share/tests.rs | 1 + codex-rs/core-plugins/src/remote_tests.rs | 1 + codex-rs/core-plugins/src/test_support.rs | 2 + codex-rs/core/src/config/config_tests.rs | 1 + codex-rs/core/src/config/mod.rs | 1 + codex-rs/core/tests/suite/plugins.rs | 16 +++-- 19 files changed, 157 insertions(+), 49 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index a59627b34b..e0d7f3c670 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -157,10 +157,7 @@ fn load_shared_plugin_ids_by_local_path( } fn remote_plugin_service_config(config: &Config) -> RemotePluginServiceConfig { - RemotePluginServiceConfig::new( - config.chatgpt_base_url.clone(), - config.http_client_factory(), - ) + config.plugins_config_input().remote_plugin_service_config() } fn share_context_for_source( diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 65b79799a6..dd200094ce 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -2088,12 +2088,14 @@ enum ProjectPluginConfiguration { Invalid, } -#[test_case(ProjectPluginConfiguration::None; "without project override")] -#[test_case(ProjectPluginConfiguration::Disabled; "project disables local plugins")] -#[test_case(ProjectPluginConfiguration::Invalid; "invalid project preserves remote plugins")] +#[test_case(ProjectPluginConfiguration::None, None; "without project override")] +#[test_case(ProjectPluginConfiguration::Disabled, None; "project disables local plugins")] +#[test_case(ProjectPluginConfiguration::Invalid, None; "invalid project preserves remote plugins")] +#[test_case(ProjectPluginConfiguration::None, Some("tpp"); "configured product sku")] #[tokio::test] async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled( project_configuration: ProjectPluginConfiguration, + product_sku: Option<&str>, ) -> Result<()> { let codex_home = TempDir::new()?; let server = MockServer::start().await; @@ -2208,13 +2210,14 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled( } }"#; + let expected_product_sku = product_sku.unwrap_or("codex"); Mock::given(method("GET")) .and(path("/backend-api/ps/plugins/list")) .and(query_param("scope", "GLOBAL")) .and(query_param("limit", "200")) .and(header("authorization", "Bearer chatgpt-token")) .and(header("chatgpt-account-id", "account-123")) - .and(header("oai-product-sku", "codex")) + .and(header("oai-product-sku", expected_product_sku)) .respond_with(ResponseTemplate::new(200).set_body_string(global_directory_body)) .mount(&server) .await; @@ -2224,7 +2227,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled( .and(query_param("limit", "200")) .and(header("authorization", "Bearer chatgpt-token")) .and(header("chatgpt-account-id", "account-123")) - .and(header("oai-product-sku", "codex")) + .and(header("oai-product-sku", expected_product_sku)) .respond_with(ResponseTemplate::new(200).set_body_string(empty_page_body)) .mount(&server) .await; @@ -2233,7 +2236,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled( .and(query_param("scope", "GLOBAL")) .and(header("authorization", "Bearer chatgpt-token")) .and(header("chatgpt-account-id", "account-123")) - .and(header("oai-product-sku", "codex")) + .and(header("oai-product-sku", expected_product_sku)) .respond_with(ResponseTemplate::new(200).set_body_string(global_installed_body)) .mount(&server) .await; @@ -2242,7 +2245,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled( .and(query_param("scope", "WORKSPACE")) .and(header("authorization", "Bearer chatgpt-token")) .and(header("chatgpt-account-id", "account-123")) - .and(header("oai-product-sku", "codex")) + .and(header("oai-product-sku", expected_product_sku)) .respond_with(ResponseTemplate::new(200).set_body_string(empty_page_body)) .mount(&server) .await; @@ -2255,8 +2258,11 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled( .mount(&server) .await; - let mut mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) + let mut builder = TestAppServer::builder().with_codex_home(codex_home.path()); + if let Some(product_sku) = product_sku { + builder = builder.with_args(&["-c", &format!("apps_mcp_product_sku=\"{product_sku}\"")]); + } + let mut mcp = builder .build_initialized_with_timeout(DEFAULT_TIMEOUT) .await?; diff --git a/codex-rs/cli/tests/worktree.rs b/codex-rs/cli/tests/worktree.rs index a15817ef4e..c0665b3438 100644 --- a/codex-rs/cli/tests/worktree.rs +++ b/codex-rs/cli/tests/worktree.rs @@ -293,6 +293,7 @@ trust_level = "trusted" ]; let mut owner = None; let mut previous: Vec = Vec::new(); + let prompt = "describe checkout"; for (fork, explicit_cd, analytics, auth_failure) in [ (false, false, false, false), (true, false, false, false), @@ -393,7 +394,7 @@ trust_level = "trusted" if analytics { args.extend(["-c".into(), "analytics.enabled=true".into()]); } - args.push("describe checkout".into()); + args.push(prompt.into()); if previous.is_empty() { let mut untrusted_args = args.clone(); untrusted_args.extend([ @@ -436,6 +437,18 @@ trust_level = "trusted" body = rx.recv() => { let (body, checkout, metadata) = body.context("model request")??; let body: Value = serde_json::from_slice(&body)?; + // Background title generation can reach the model before the user turn. + let is_user_turn = body["input"].as_array().is_some_and(|input| { + input.iter().any(|item| { + item["role"] == "user" + && item["content"].as_array().is_some_and(|content| { + content.iter().any(|part| part["text"] == prompt) + }) + }) + }); + if !is_user_turn { + continue; + } return Ok::<_, anyhow::Error>((body, checkout, metadata)); } bytes = stdout.recv() => { diff --git a/codex-rs/core-plugins/src/discoverable_tests.rs b/codex-rs/core-plugins/src/discoverable_tests.rs index 63538ce9ba..3c2085c9e1 100644 --- a/codex-rs/core-plugins/src/discoverable_tests.rs +++ b/codex-rs/core-plugins/src/discoverable_tests.rs @@ -956,6 +956,7 @@ plugins = true &RemotePluginServiceConfig::new( plugins.chatgpt_base_url.clone(), crate::test_support::test_http_client_factory(), + /*product_sku*/ None, ), Some(&auth), ) diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 8694817c4c..dbe52a569b 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -157,6 +157,7 @@ pub struct PluginsConfigInput { pub plugins_enabled: bool, pub remote_plugin_enabled: bool, pub chatgpt_base_url: String, + pub product_sku: Option, http_client_factory: HttpClientFactory, } @@ -168,6 +169,7 @@ impl PluginsConfigInput { remote_plugin_enabled: bool, chatgpt_base_url: String, http_client_factory: HttpClientFactory, + product_sku: Option, ) -> Self { Self { config_layer_stack, @@ -176,6 +178,7 @@ impl PluginsConfigInput { remote_plugin_enabled, chatgpt_base_url, http_client_factory, + product_sku, } } @@ -184,6 +187,7 @@ impl PluginsConfigInput { RemotePluginServiceConfig::new( self.chatgpt_base_url.clone(), self.http_client_factory.clone(), + self.product_sku.clone(), ) } } diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index fbd1501e76..663c053dd2 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -126,6 +126,7 @@ fn plugins_config_input_with_requirements( /*remote_plugin_enabled*/ false, String::new(), test_http_client_factory(), + /*product_sku*/ None, ) } @@ -166,6 +167,7 @@ fn curated_repo_sync_stays_deferred_for_remote_chatgpt_catalog() { /*remote_plugin_enabled*/ true, "https://chatgpt.com".to_string(), test_http_client_factory(), + /*product_sku*/ None, ); let manager = Arc::new(test_plugins_manager_with_options( tmp.path().to_path_buf(), @@ -2969,6 +2971,7 @@ async fn plugin_cache_reuses_effective_configurations() { /*remote_plugin_enabled*/ false, "https://chatgpt.com".to_string(), test_http_client_factory(), + /*product_sku*/ None, ) }; let manager = test_plugins_manager(codex_home.path().to_path_buf()); @@ -3233,6 +3236,7 @@ async fn plugins_for_config_discards_in_flight_load_after_account_change() { /*remote_plugin_enabled*/ true, String::new(), test_http_client_factory(), + /*product_sku*/ None, ); let auth_manager = test_auth_manager(Some(AuthMode::ChatgptAuthTokens)); let manager = Arc::new(test_plugins_manager_with_auth_manager( @@ -7182,6 +7186,7 @@ fn remote_installed_plugins_cache_refresh_coalesces_materializations() { service_config: RemotePluginServiceConfig::new( "https://example.com".to_string(), test_http_client_factory(), + /*product_sku*/ None, ), auth: None, notify: RemoteInstalledPluginsCacheRefreshNotify::IfCacheChanged, diff --git a/codex-rs/core-plugins/src/marketplace_policy/curated_skill_tests.rs b/codex-rs/core-plugins/src/marketplace_policy/curated_skill_tests.rs index 2dba808d1c..14de1550b0 100644 --- a/codex-rs/core-plugins/src/marketplace_policy/curated_skill_tests.rs +++ b/codex-rs/core-plugins/src/marketplace_policy/curated_skill_tests.rs @@ -87,6 +87,7 @@ async fn curated_git_requirements_control_plugin_skills() -> Result<()> { /*remote_plugin_enabled*/ false, "https://chatgpt.com/backend-api/".to_string(), HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), + /*product_sku*/ None, ); let auth = if name == "openai-curated" { CodexAuth::create_dummy_chatgpt_auth_for_testing() diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index ee70af99c0..5c267340d5 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -107,7 +107,7 @@ pub const REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_DISPLAY_NAME: &st const OPENAI_CURATED_REMOTE_COLLECTION_KEY: &str = "vertical"; const OAI_PRODUCT_SKU_HEADER: &str = "OAI-Product-Sku"; -const CODEX_PRODUCT_SKU: &str = "codex"; +pub(crate) const CODEX_PRODUCT_SKU: &str = "codex"; const REMOTE_PLUGIN_CATALOG_TIMEOUT: Duration = Duration::from_secs(30); const RECOMMENDED_PLUGINS_TIMEOUT: Duration = Duration::from_secs(5); const REMOTE_PLUGIN_LIST_PAGE_LIMIT: u32 = 200; @@ -147,6 +147,7 @@ const REMOTE_INSTALLED_MARKETPLACE_DISPLAY_ORDER: [(&str, &str); 6] = [ #[derive(Debug, Clone)] pub struct RemotePluginServiceConfig { pub chatgpt_base_url: String, + pub(crate) product_sku: String, pub(crate) http_clients: Arc, } @@ -155,7 +156,11 @@ impl RemotePluginServiceConfig { /// /// Keeping the factory mandatory ensures every catalog, mutation, upload, and bundle request /// follows the same outbound proxy policy. - pub fn new(chatgpt_base_url: String, http_client_factory: HttpClientFactory) -> Self { + pub fn new( + chatgpt_base_url: String, + http_client_factory: HttpClientFactory, + product_sku: Option, + ) -> Self { let http_clients = RouteAwareClientPool::with_chatgpt_cloudflare_cookies_without_request_logging( http_client_factory, @@ -163,6 +168,7 @@ impl RemotePluginServiceConfig { ); Self { chatgpt_base_url, + product_sku: product_sku.unwrap_or_else(|| CODEX_PRODUCT_SKU.to_string()), http_clients: Arc::new(http_clients), } } @@ -177,6 +183,7 @@ impl RemotePluginServiceConfig { impl PartialEq for RemotePluginServiceConfig { fn eq(&self, other: &Self) -> bool { self.chatgpt_base_url == other.chatgpt_base_url + && self.product_sku == other.product_sku && self.http_clients.outbound_proxy_policy() == other.http_clients.outbound_proxy_policy() } @@ -1032,8 +1039,12 @@ pub async fn fetch_recommended_plugins( .map_err(RemotePluginCatalogError::InvalidBaseUrl)?; url.query_pairs_mut().append_pair("scope", "GLOBAL"); let url = url.to_string(); - let request = authenticated_request(config.http_request(Method::GET, &url), auth) - .timeout(RECOMMENDED_PLUGINS_TIMEOUT); + let request = authenticated_request( + config.http_request(Method::GET, &url), + auth, + &config.product_sku, + ) + .timeout(RECOMMENDED_PLUGINS_TIMEOUT); let response: RecommendedPluginsResponse = send_and_decode(request, &url).await?; Ok(recommended_plugins_mode(response)) } @@ -1400,7 +1411,11 @@ pub async fn fetch_remote_plugin_skill_detail( } let url = remote_plugin_skill_detail_url(config, plugin_id, skill_name)?; - let request = authenticated_request(config.http_request(Method::GET, &url), auth); + let request = authenticated_request( + config.http_request(Method::GET, &url), + auth, + &config.product_sku, + ); let response: RemotePluginSkillDetailResponse = send_and_decode(request, &url).await?; if response.plugin_id != plugin_id { return Err(RemotePluginCatalogError::UnexpectedPluginId { @@ -1607,7 +1622,11 @@ async fn install_remote_plugin_inner( url.query_pairs_mut() .append_pair("includeAppsNeedingAuth", "true"); let url = url.to_string(); - let request = authenticated_request(config.http_request(Method::POST, &url), auth); + let request = authenticated_request( + config.http_request(Method::POST, &url), + auth, + &config.product_sku, + ); let request = if let Some(install_attempt_id) = install_attempt_id { request.json(&RemotePluginInstallRequest { install_attempt_id }) } else { @@ -1700,7 +1719,11 @@ pub async fn uninstall_remote_plugin( let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/uninstall"); - let request = authenticated_request(config.http_request(Method::POST, &url), auth); + let request = authenticated_request( + config.http_request(Method::POST, &url), + auth, + &config.product_sku, + ); let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; if response.id != remote_plugin_id { return Err(RemotePluginCatalogError::UnexpectedPluginId { @@ -2194,7 +2217,11 @@ async fn get_remote_plugin_list_page( url.query_pairs_mut().append_pair("pageToken", page_token); } let url = url.to_string(); - let request = authenticated_request(config.http_request(Method::GET, &url), auth); + let request = authenticated_request( + config.http_request(Method::GET, &url), + auth, + &config.product_sku, + ); send_and_decode(request, &url).await } @@ -2212,7 +2239,11 @@ async fn get_remote_shared_workspace_plugins_page( url.query_pairs_mut().append_pair("pageToken", page_token); } let url = url.to_string(); - let request = authenticated_request(config.http_request(Method::GET, &url), auth); + let request = authenticated_request( + config.http_request(Method::GET, &url), + auth, + &config.product_sku, + ); send_and_decode(request, &url).await } @@ -2244,7 +2275,11 @@ async fn get_remote_plugin_installed_page( url.query_pairs_mut().append_pair("pageToken", page_token); } let url = url.to_string(); - let request = authenticated_request(config.http_request(Method::GET, &url), auth); + let request = authenticated_request( + config.http_request(Method::GET, &url), + auth, + &config.product_sku, + ); send_and_decode(request, &url).await } @@ -2279,8 +2314,12 @@ async fn fetch_plugin_detail_with_timeout( .append_pair("includeDownloadUrls", "true"); } let url = url.to_string(); - let request = - authenticated_request(config.http_request(Method::GET, &url), auth).timeout(timeout); + let request = authenticated_request( + config.http_request(Method::GET, &url), + auth, + &config.product_sku, + ) + .timeout(timeout); send_and_decode(request, &url).await } @@ -2318,11 +2357,12 @@ fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePlu fn authenticated_request( request: RouteAwareRequestBuilder, auth: &CodexAuth, + product_sku: &str, ) -> RouteAwareRequestBuilder { request .timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT) .headers(codex_model_provider::auth_provider_from_auth(auth).to_auth_headers()) - .header(OAI_PRODUCT_SKU_HEADER, CODEX_PRODUCT_SKU) + .header(OAI_PRODUCT_SKU_HEADER, product_sku) } async fn send_and_decode Deserialize<'de>>( diff --git a/codex-rs/core-plugins/src/remote/catalog_cache_tests.rs b/codex-rs/core-plugins/src/remote/catalog_cache_tests.rs index 049355968b..8ccce5f3c0 100644 --- a/codex-rs/core-plugins/src/remote/catalog_cache_tests.rs +++ b/codex-rs/core-plugins/src/remote/catalog_cache_tests.rs @@ -51,6 +51,7 @@ fn global_catalog_cache_reuses_legacy_cache_file() { let config = RemotePluginServiceConfig::new( "https://chatgpt.com/backend-api".to_string(), crate::test_support::test_http_client_factory(), + /*product_sku*/ None, ); let auth = CodexAuth::Headers(AuthHeaders::new(http::HeaderMap::new())); let legacy_cache_path = codex_home @@ -96,6 +97,7 @@ fn header_auth_does_not_cache_private_catalogs_without_a_stable_identity() { let config = RemotePluginServiceConfig::new( "https://chatgpt.com/backend-api".to_string(), crate::test_support::test_http_client_factory(), + /*product_sku*/ None, ); let auth = CodexAuth::Headers(AuthHeaders::new(http::HeaderMap::new())); diff --git a/codex-rs/core-plugins/src/remote/plugin_capabilities_tests.rs b/codex-rs/core-plugins/src/remote/plugin_capabilities_tests.rs index 77ede1bb43..746ff9c75d 100644 --- a/codex-rs/core-plugins/src/remote/plugin_capabilities_tests.rs +++ b/codex-rs/core-plugins/src/remote/plugin_capabilities_tests.rs @@ -119,6 +119,7 @@ async fn capabilities_union_cached_versions_and_sync_reports_removal() -> anyhow let config = RemotePluginServiceConfig::new( format!("{}/backend-api", server.uri()), crate::test_support::test_http_client_factory(), + /*product_sku*/ None, ); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); Mock::given(method("GET")) diff --git a/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs b/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs index 380271b77f..92a2ba8697 100644 --- a/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs +++ b/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs @@ -570,6 +570,7 @@ mod tests { let config = RemotePluginServiceConfig::new( format!("{}/backend-api", server.uri()), crate::test_support::test_http_client_factory(), + /*product_sku*/ None, ); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); diff --git a/codex-rs/core-plugins/src/remote/search.rs b/codex-rs/core-plugins/src/remote/search.rs index 9d5d946b8c..f4a6021c28 100644 --- a/codex-rs/core-plugins/src/remote/search.rs +++ b/codex-rs/core-plugins/src/remote/search.rs @@ -60,7 +60,11 @@ pub async fn search_remote_plugins( } let url = url.to_string(); - let request = authenticated_request(config.http_request(Method::GET, &url), auth); + let request = authenticated_request( + config.http_request(Method::GET, &url), + auth, + &config.product_sku, + ); let response: RemotePluginListResponse = send_and_decode(request, &url_for_error) .await .map_err(|error| match error { diff --git a/codex-rs/core-plugins/src/remote/share.rs b/codex-rs/core-plugins/src/remote/share.rs index 3fa2859f07..713792d11e 100644 --- a/codex-rs/core-plugins/src/remote/share.rs +++ b/codex-rs/core-plugins/src/remote/share.rs @@ -285,7 +285,11 @@ pub async fn delete_remote_plugin_share( let auth = ensure_chatgpt_auth(auth)?; let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/public/plugins/workspace/{remote_plugin_id}"); - let request = authenticated_request(config.http_request(Method::DELETE, &url), auth); + let request = authenticated_request( + config.http_request(Method::DELETE, &url), + auth, + &config.product_sku, + ); send_and_expect_status(request, &url, &[StatusCode::NO_CONTENT]).await?; if let Err(err) = local_paths::remove_plugin_share_local_path(codex_home, remote_plugin_id) { warn!( @@ -318,12 +322,15 @@ pub async fn update_remote_plugin_share_targets( .unwrap_or_default(); let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/shares"); - let request = authenticated_request(config.http_request(Method::PUT, &url), auth).json( - &RemotePluginShareUpdateTargetsRequest { - discoverability, - targets, - }, - ); + let request = authenticated_request( + config.http_request(Method::PUT, &url), + auth, + &config.product_sku, + ) + .json(&RemotePluginShareUpdateTargetsRequest { + discoverability, + targets, + }); let response: RemotePluginShareUpdateTargetsResponse = send_and_decode(request, &url).await?; Ok(RemotePluginShareUpdateTargetsResult { principals: response.principals, @@ -390,7 +397,11 @@ async fn get_created_workspace_plugins_page( url.query_pairs_mut().append_pair("pageToken", page_token); } let url = url.to_string(); - let request = authenticated_request(config.http_request(Method::GET, &url), auth); + let request = authenticated_request( + config.http_request(Method::GET, &url), + auth, + &config.product_sku, + ); send_and_decode(request, &url).await } @@ -403,14 +414,17 @@ async fn create_workspace_plugin_upload( ) -> Result { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/public/plugins/workspace/upload-url"); - let request = authenticated_request(config.http_request(Method::POST, &url), auth).json( - &RemoteWorkspacePluginUploadUrlRequest { - filename, - mime_type: "application/gzip", - size_bytes, - plugin_id: remote_plugin_id, - }, - ); + let request = authenticated_request( + config.http_request(Method::POST, &url), + auth, + &config.product_sku, + ) + .json(&RemoteWorkspacePluginUploadUrlRequest { + filename, + mime_type: "application/gzip", + size_bytes, + plugin_id: remote_plugin_id, + }); send_and_decode(request, &url).await } @@ -456,7 +470,12 @@ async fn finalize_workspace_plugin_upload( } else { format!("{base_url}/public/plugins/workspace") }; - let request = authenticated_request(config.http_request(Method::POST, &url), auth).json(&body); + let request = authenticated_request( + config.http_request(Method::POST, &url), + auth, + &config.product_sku, + ) + .json(&body); send_and_decode(request, &url).await } diff --git a/codex-rs/core-plugins/src/remote/share/tests.rs b/codex-rs/core-plugins/src/remote/share/tests.rs index 13d4bdbfcf..fa4786280f 100644 --- a/codex-rs/core-plugins/src/remote/share/tests.rs +++ b/codex-rs/core-plugins/src/remote/share/tests.rs @@ -28,6 +28,7 @@ fn test_config(server: &MockServer) -> RemotePluginServiceConfig { RemotePluginServiceConfig::new( format!("{}/backend-api", server.uri()), crate::test_support::test_http_client_factory(), + /*product_sku*/ None, ) } diff --git a/codex-rs/core-plugins/src/remote_tests.rs b/codex-rs/core-plugins/src/remote_tests.rs index c1c0365d44..fa3503526a 100644 --- a/codex-rs/core-plugins/src/remote_tests.rs +++ b/codex-rs/core-plugins/src/remote_tests.rs @@ -276,6 +276,7 @@ fn catalog_cache_invalidation_clears_global_collections_and_preserves_other_scop let config = RemotePluginServiceConfig::new( "https://chatgpt.com/backend-api".to_string(), crate::test_support::test_http_client_factory(), + /*product_sku*/ None, ); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); for scope in [RemotePluginScope::Global, RemotePluginScope::Workspace] { diff --git a/codex-rs/core-plugins/src/test_support.rs b/codex-rs/core-plugins/src/test_support.rs index dc16dad05f..9bb63ae284 100644 --- a/codex-rs/core-plugins/src/test_support.rs +++ b/codex-rs/core-plugins/src/test_support.rs @@ -339,6 +339,7 @@ pub(crate) fn recording_remote_plugin_service_config( ( RemotePluginServiceConfig { chatgpt_base_url, + product_sku: crate::remote::CODEX_PRODUCT_SKU.to_string(), http_clients, }, selected_urls, @@ -497,6 +498,7 @@ pub(crate) async fn load_plugins_config(codex_home: &Path, cwd: &Path) -> Plugin ), "https://chatgpt.com/backend-api/".to_string(), test_http_client_factory(), + /*product_sku*/ None, ) } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 8a873cdf35..1cb352c372 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -2023,6 +2023,7 @@ respect_system_proxy = true codex_http_client::HttpClientFactory::new( codex_http_client::OutboundProxyPolicy::RespectSystemProxy, ), + /*product_sku*/ None, ) ); Ok(()) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 1a26e6a504..719aa7d2c4 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1676,6 +1676,7 @@ impl Config { self.features.enabled(Feature::RemotePlugin), self.chatgpt_base_url.clone(), self.http_client_factory(), + self.apps_mcp_product_sku.clone(), ) } diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 41c77a4a33..d80185621e 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -68,6 +68,7 @@ use test_case::test_case; use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; +use wiremock::matchers::header; use wiremock::matchers::method; use wiremock::matchers::path; @@ -999,11 +1000,13 @@ async fn legacy_plugin_skill_prompt_remains_complete() -> Result<()> { Ok(()) } -#[test_case(true, true, (true, false); "enabled remote supersedes bundled")] -#[test_case(false, true, (false, false); "disabled remote does not reactivate bundled")] -#[test_case(true, false, (false, true); "missing remote bundle preserves bundled")] +#[test_case(None, true, true, (true, false); "enabled remote supersedes bundled")] +#[test_case(Some("tpp"), true, true, (true, false); "configured work sku")] +#[test_case(None, false, true, (false, false); "disabled remote does not reactivate bundled")] +#[test_case(None, true, false, (false, true); "missing remote bundle preserves bundled")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sites_compatibility_guard_in_agent_turn( + product_sku: Option<&str>, remote_enabled: bool, cache_remote_sites: bool, expected_skills: (bool, bool), @@ -1017,6 +1020,7 @@ async fn sites_compatibility_guard_in_agent_turn( .await; Mock::given(method("GET")) .and(path("/ps/plugins/installed")) + .and(header("OAI-Product-Sku", product_sku.unwrap_or("codex"))) .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "plugins": [{ "id": "plugins~plugin_connector_1p_689987207de08191979cf68eca2941c6", @@ -1073,11 +1077,15 @@ enabled = true } let chatgpt_base_url = server.uri(); + let product_sku = product_sku.map(str::to_owned); let mut builder = test_codex() .with_home(codex_home) .with_extensions(skills_extensions()) .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_config(move |config| config.chatgpt_base_url = chatgpt_base_url); + .with_config(move |config| { + config.chatgpt_base_url = chatgpt_base_url; + config.apps_mcp_product_sku = product_sku; + }); let test = builder.build_with_auto_env(&server).await?; let auth = test.thread_manager.auth_manager().auth().await;