mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
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
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -293,6 +293,7 @@ trust_level = "trusted"
|
||||
];
|
||||
let mut owner = None;
|
||||
let mut previous: Vec<String> = 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() => {
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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<String>,
|
||||
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<String>,
|
||||
) -> 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<dyn HttpClientSelector>,
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
) -> 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<T: for<'de> Deserialize<'de>>(
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<RemoteWorkspacePluginUploadUrlResponse, RemotePluginCatalogError> {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2023,6 +2023,7 @@ respect_system_proxy = true
|
||||
codex_http_client::HttpClientFactory::new(
|
||||
codex_http_client::OutboundProxyPolicy::RespectSystemProxy,
|
||||
),
|
||||
/*product_sku*/ None,
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user