From ec32866c379405a28b58c0064c857fb60ed3c735 Mon Sep 17 00:00:00 2001 From: alexsong-oai Date: Fri, 20 Mar 2026 18:42:40 -0700 Subject: [PATCH] Pass platform param to featured plugins (#15348) --- .../app-server/tests/suite/v2/plugin_list.rs | 5 ++ codex-rs/core/src/plugins/manager.rs | 3 +- codex-rs/core/src/plugins/manager_tests.rs | 69 +++++++++++++++++++ codex-rs/core/src/plugins/remote.rs | 6 ++ codex-rs/protocol/src/protocol.rs | 8 +++ 5 files changed, 90 insertions(+), 1 deletion(-) 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 a95871430..89b1090b8 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -25,6 +25,7 @@ use wiremock::ResponseTemplate; use wiremock::matchers::header; use wiremock::matchers::method; use wiremock::matchers::path; +use wiremock::matchers::query_param; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; @@ -678,6 +679,7 @@ async fn plugin_list_force_remote_sync_reconciles_curated_plugin_state() -> Resu .await; Mock::given(method("GET")) .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) .and(header("authorization", "Bearer chatgpt-token")) .and(header("chatgpt-account-id", "account-123")) .respond_with( @@ -784,6 +786,7 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> { .await; Mock::given(method("GET")) .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) .and(header("authorization", "Bearer chatgpt-token")) .and(header("chatgpt-account-id", "account-123")) .respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#)) @@ -850,6 +853,7 @@ async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Resul Mock::given(method("GET")) .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) .respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#)) .mount(&server) .await; @@ -888,6 +892,7 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> Mock::given(method("GET")) .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) .respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#)) .expect(1) .mount(&server) diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index 9987bbbb9..f02758d27 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -622,7 +622,8 @@ impl PluginsManager { if let Some(featured_plugin_ids) = self.cached_featured_plugin_ids(&cache_key) { return Ok(featured_plugin_ids); } - let featured_plugin_ids = fetch_remote_featured_plugin_ids(config, auth).await?; + let featured_plugin_ids = + fetch_remote_featured_plugin_ids(config, auth, self.restriction_product).await?; self.write_featured_plugin_ids_cache(cache_key, &featured_plugin_ids); Ok(featured_plugin_ids) } diff --git a/codex-rs/core/src/plugins/manager_tests.rs b/codex-rs/core/src/plugins/manager_tests.rs index c44343380..cd8541a85 100644 --- a/codex-rs/core/src/plugins/manager_tests.rs +++ b/codex-rs/core/src/plugins/manager_tests.rs @@ -23,6 +23,7 @@ use wiremock::ResponseTemplate; use wiremock::matchers::header; use wiremock::matchers::method; use wiremock::matchers::path; +use wiremock::matchers::query_param; fn write_plugin(root: &Path, dir_name: &str, manifest_name: &str) { let plugin_root = root.join(dir_name); @@ -1899,6 +1900,74 @@ plugins = true ); } +#[tokio::test] +async fn featured_plugin_ids_for_config_uses_restriction_product_query_param() { + let tmp = tempfile::tempdir().unwrap(); + write_file( + &tmp.path().join(CONFIG_TOML_FILE), + r#"[features] +plugins = true +"#, + ); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "chat")) + .and(header("authorization", "Bearer Access Token")) + .and(header("chatgpt-account-id", "account_id")) + .respond_with(ResponseTemplate::new(200).set_body_string(r#"["chat-plugin"]"#)) + .mount(&server) + .await; + + let mut config = load_config(tmp.path(), tmp.path()).await; + config.chatgpt_base_url = format!("{}/backend-api/", server.uri()); + let manager = PluginsManager::new_with_restriction_product( + tmp.path().to_path_buf(), + Some(Product::Chatgpt), + ); + + let featured_plugin_ids = manager + .featured_plugin_ids_for_config( + &config, + Some(&CodexAuth::create_dummy_chatgpt_auth_for_testing()), + ) + .await + .unwrap(); + + assert_eq!(featured_plugin_ids, vec!["chat-plugin".to_string()]); +} + +#[tokio::test] +async fn featured_plugin_ids_for_config_defaults_query_param_to_codex() { + let tmp = tempfile::tempdir().unwrap(); + write_file( + &tmp.path().join(CONFIG_TOML_FILE), + r#"[features] +plugins = true +"#, + ); + + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/backend-api/plugins/featured")) + .and(query_param("platform", "codex")) + .respond_with(ResponseTemplate::new(200).set_body_string(r#"["codex-plugin"]"#)) + .mount(&server) + .await; + + let mut config = load_config(tmp.path(), tmp.path()).await; + config.chatgpt_base_url = format!("{}/backend-api/", server.uri()); + let manager = PluginsManager::new_with_restriction_product(tmp.path().to_path_buf(), None); + + let featured_plugin_ids = manager + .featured_plugin_ids_for_config(&config, None) + .await + .unwrap(); + + assert_eq!(featured_plugin_ids, vec!["codex-plugin".to_string()]); +} + #[test] fn refresh_curated_plugin_cache_replaces_existing_local_version_with_sha() { let tmp = tempfile::tempdir().unwrap(); diff --git a/codex-rs/core/src/plugins/remote.rs b/codex-rs/core/src/plugins/remote.rs index 898767e35..756824b33 100644 --- a/codex-rs/core/src/plugins/remote.rs +++ b/codex-rs/core/src/plugins/remote.rs @@ -1,6 +1,7 @@ use crate::auth::CodexAuth; use crate::config::Config; use crate::default_client::build_reqwest_client; +use codex_protocol::protocol::Product; use serde::Deserialize; use std::time::Duration; use url::Url; @@ -162,12 +163,17 @@ pub(crate) async fn fetch_remote_plugin_status( pub async fn fetch_remote_featured_plugin_ids( config: &Config, auth: Option<&CodexAuth>, + product: Option, ) -> Result, RemotePluginFetchError> { let base_url = config.chatgpt_base_url.trim_end_matches('/'); let url = format!("{base_url}/plugins/featured"); let client = build_reqwest_client(); let mut request = client .get(&url) + .query(&[( + "platform", + product.unwrap_or(Product::Codex).to_app_platform(), + )]) .timeout(REMOTE_FEATURED_PLUGIN_FETCH_TIMEOUT); if let Some(auth) = auth.filter(|auth| auth.is_chatgpt_auth()) { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 808be6259..4f7f2616f 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2978,6 +2978,14 @@ pub enum Product { Atlas, } impl Product { + pub fn to_app_platform(self) -> &'static str { + match self { + Self::Chatgpt => "chat", + Self::Codex => "codex", + Self::Atlas => "atlas", + } + } + pub fn from_session_source_name(value: &str) -> Option { let normalized = value.trim().to_ascii_lowercase(); match normalized.as_str() {