From 7db17865737fbdb2d7380ec025005422f8c77ca6 Mon Sep 17 00:00:00 2001 From: Steven Lee Date: Tue, 16 Jun 2026 19:53:50 +0000 Subject: [PATCH 1/5] [plugins] Route preview traffic to plugin service --- codex-rs/Cargo.lock | 1 + codex-rs/codex-mcp/src/mcp/mod.rs | 29 ++++++-- codex-rs/codex-mcp/src/mcp/mod_tests.rs | 59 ++++++++++++++- codex-rs/core-plugins/src/remote.rs | 41 +++++++++- codex-rs/core-plugins/src/remote/share.rs | 3 +- codex-rs/core-plugins/src/remote_tests.rs | 61 +++++++++++++++ codex-rs/utils/plugins/Cargo.toml | 1 + codex-rs/utils/plugins/src/lib.rs | 1 + .../plugins/src/plugin_service_routing.rs | 74 +++++++++++++++++++ .../src/plugin_service_routing_tests.rs | 44 +++++++++++ 10 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 codex-rs/utils/plugins/src/plugin_service_routing.rs create mode 100644 codex-rs/utils/plugins/src/plugin_service_routing_tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 65d581db6f7a..147c5fed87b9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4282,6 +4282,7 @@ dependencies = [ "codex-exec-server", "codex-utils-absolute-path", "codex-utils-path-uri", + "pretty_assertions", "serde", "serde_json", "tempfile", diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 4a765e6a62e4..6c036e4694eb 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -33,6 +33,8 @@ use codex_protocol::mcp::Tool; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::McpAuthStatus; +use codex_utils_plugins::plugin_service_routing::PLUGIN_SERVICE_PREVIEW_COOKIE; +use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; use rmcp::model::ElicitationCapability; use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceResult; @@ -455,6 +457,7 @@ pub fn codex_apps_mcp_server_config( mcp_server_config_for_url( codex_apps_mcp_url_for_base_url(chatgpt_base_url), apps_mcp_product_sku, + plugin_service_preview_enabled(), ) } @@ -469,13 +472,29 @@ pub fn hosted_plugin_runtime_mcp_server_config( } else { format!("{base_url}/api/codex") }; - mcp_server_config_for_url(format!("{base_url}/ps/mcp"), apps_mcp_product_sku) + mcp_server_config_for_url( + format!("{base_url}/ps/mcp"), + apps_mcp_product_sku, + plugin_service_preview_enabled(), + ) } -fn mcp_server_config_for_url(url: String, apps_mcp_product_sku: Option<&str>) -> McpServerConfig { - let http_headers = apps_mcp_product_sku.map(|product_sku| { - HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())]) - }); +fn mcp_server_config_for_url( + url: String, + apps_mcp_product_sku: Option<&str>, + preview_enabled: bool, +) -> McpServerConfig { + let mut http_headers = HashMap::new(); + if let Some(product_sku) = apps_mcp_product_sku { + http_headers.insert("X-OpenAI-Product-Sku".to_string(), product_sku.to_string()); + } + if preview_enabled { + http_headers.insert( + "Cookie".to_string(), + PLUGIN_SERVICE_PREVIEW_COOKIE.to_string(), + ); + } + let http_headers = (!http_headers.is_empty()).then_some(http_headers); McpServerConfig { transport: McpServerTransportConfig::StreamableHttp { diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index 31c31960c737..4cc79bff5f89 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -261,7 +261,11 @@ 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")); + let config = mcp_server_config_for_url( + codex_apps_mcp_url_for_base_url("https://chatgpt.com"), + Some("tpp"), + /*preview_enabled*/ false, + ); match &config.transport { McpServerTransportConfig::StreamableHttp { @@ -282,6 +286,59 @@ fn codex_apps_server_config_forwards_configured_product_sku_header() { } } +#[test] +fn built_in_plugin_service_mcp_config_only_adds_preview_cookie_when_enabled() { + let disabled = mcp_server_config_for_url( + "https://chatgpt.com/backend-api/ps/mcp".to_string(), + /*apps_mcp_product_sku*/ None, + /*preview_enabled*/ false, + ); + let enabled = mcp_server_config_for_url( + "https://chatgpt.com/backend-api/ps/mcp".to_string(), + /*apps_mcp_product_sku*/ None, + /*preview_enabled*/ true, + ); + + fn headers(config: &McpServerConfig) -> Option<&HashMap> { + match &config.transport { + McpServerTransportConfig::StreamableHttp { http_headers, .. } => http_headers.as_ref(), + other => panic!("expected streamable http transport, got {other:?}"), + } + } + assert_eq!(headers(&disabled), None); + assert_eq!( + headers(&enabled), + Some(&HashMap::from([( + "Cookie".to_string(), + "oai-chat-plugin-service-preview=true".to_string(), + )])), + ); + + let unrelated_server = McpServerConfig { + transport: McpServerTransportConfig::StreamableHttp { + url: "https://third-party.example/mcp".to_string(), + bearer_token_env_var: None, + http_headers: None, + env_http_headers: None, + }, + environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), + enabled: true, + required: false, + supports_parallel_tool_calls: false, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + default_tools_approval_mode: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + oauth: None, + oauth_resource: None, + tools: HashMap::new(), + }; + assert_eq!(headers(&unrelated_server), None); +} + #[tokio::test] async fn effective_mcp_servers_preserve_runtime_servers() { let codex_home = tempfile::tempdir().expect("tempdir"); diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 05ce110953e9..7e71622bf5a9 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -15,7 +15,11 @@ use codex_plugin::AppDeclaration; use codex_plugin::PluginId; use codex_plugin::app_connector_ids_from_declarations; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; +use codex_utils_plugins::plugin_service_routing::plugin_service_routing_cookie; use reqwest::RequestBuilder; +use reqwest::header::COOKIE; +use reqwest::header::HeaderValue; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; @@ -1840,8 +1844,7 @@ async fn send_and_decode Deserialize<'de>>( request: RequestBuilder, url: &str, ) -> Result { - let response = request - .send() + let response = send_plugin_service_request(request) .await .map_err(|source| RemotePluginCatalogError::Request { url: url.to_string(), @@ -1862,3 +1865,37 @@ async fn send_and_decode Deserialize<'de>>( source, }) } + +pub(super) async fn send_plugin_service_request( + request: RequestBuilder, +) -> Result { + send_plugin_service_request_with_preview(request, plugin_service_preview_enabled()).await +} + +async fn send_plugin_service_request_with_preview( + request: RequestBuilder, + preview_enabled: bool, +) -> Result { + let (client, request) = request.build_split(); + let mut request = request?; + let headers = request.headers_mut(); + let existing_cookie_headers = headers + .get_all(COOKIE) + .iter() + .map(|value| value.as_bytes().to_vec()) + .collect::>(); + let existing_cookie_headers = existing_cookie_headers + .iter() + .map(Vec::as_slice) + .collect::>(); + let routing_cookie = plugin_service_routing_cookie(&existing_cookie_headers, preview_enabled); + + headers.remove(COOKIE); + if let Some(routing_cookie) = routing_cookie + && let Ok(routing_cookie) = HeaderValue::from_bytes(&routing_cookie) + { + headers.insert(COOKIE, routing_cookie); + } + + client.execute(request).await +} diff --git a/codex-rs/core-plugins/src/remote/share.rs b/codex-rs/core-plugins/src/remote/share.rs index c400d27c47d4..961257d24591 100644 --- a/codex-rs/core-plugins/src/remote/share.rs +++ b/codex-rs/core-plugins/src/remote/share.rs @@ -493,8 +493,7 @@ async fn send_and_expect_status( url_for_error: &str, expected_statuses: &[StatusCode], ) -> Result<(), RemotePluginCatalogError> { - let response = request - .send() + let response = send_plugin_service_request(request) .await .map_err(|source| RemotePluginCatalogError::Request { url: url_for_error.to_string(), diff --git a/codex-rs/core-plugins/src/remote_tests.rs b/codex-rs/core-plugins/src/remote_tests.rs index f1d4478d7f46..bef7f543f57a 100644 --- a/codex-rs/core-plugins/src/remote_tests.rs +++ b/codex-rs/core-plugins/src/remote_tests.rs @@ -1,5 +1,9 @@ use super::*; use pretty_assertions::assert_eq; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; #[test] fn build_remote_marketplace_preserves_directory_order_and_appends_installed_only_plugins() { @@ -294,3 +298,60 @@ fn recommended_plugins_ignore_invalid_remote_plugin_ids() { } ); } + +#[tokio::test] +async fn plugin_service_request_does_not_add_preview_cookie_when_disabled() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + let response = send_plugin_service_request_with_preview( + reqwest::Client::new().get(server.uri()), + /*preview_enabled*/ false, + ) + .await + .expect("request should succeed"); + assert!(response.status().is_success()); + + let requests = server + .received_requests() + .await + .expect("request recording should be available"); + assert_eq!(requests.len(), 1); + assert_eq!(requests[0].headers.get("cookie"), None); +} + +#[tokio::test] +async fn plugin_service_request_sanitizes_and_preserves_caller_cookies() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + let response = send_plugin_service_request_with_preview( + reqwest::Client::new().get(server.uri()).header( + COOKIE, + "session=abc; oai-chat-plugin-service-preview=false; theme=dark", + ), + /*preview_enabled*/ true, + ) + .await + .expect("request should succeed"); + assert!(response.status().is_success()); + + let requests = server + .received_requests() + .await + .expect("request recording should be available"); + assert_eq!(requests.len(), 1); + assert_eq!( + requests[0] + .headers + .get("cookie") + .and_then(|value| value.to_str().ok()), + Some("session=abc; theme=dark; oai-chat-plugin-service-preview=true"), + ); +} diff --git a/codex-rs/utils/plugins/Cargo.toml b/codex-rs/utils/plugins/Cargo.toml index cdfb600c8c20..7ca29ecef2a5 100644 --- a/codex-rs/utils/plugins/Cargo.toml +++ b/codex-rs/utils/plugins/Cargo.toml @@ -21,5 +21,6 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } [dev-dependencies] +pretty_assertions = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/codex-rs/utils/plugins/src/lib.rs b/codex-rs/utils/plugins/src/lib.rs index 203b1f22e41d..58a641b171c6 100644 --- a/codex-rs/utils/plugins/src/lib.rs +++ b/codex-rs/utils/plugins/src/lib.rs @@ -6,6 +6,7 @@ use codex_utils_absolute_path::AbsolutePathBuf; pub mod mcp_connector; pub mod mention_syntax; pub mod plugin_namespace; +pub mod plugin_service_routing; pub use plugin_namespace::DISCOVERABLE_PLUGIN_MANIFEST_PATHS; pub use plugin_namespace::find_plugin_manifest_path; diff --git a/codex-rs/utils/plugins/src/plugin_service_routing.rs b/codex-rs/utils/plugins/src/plugin_service_routing.rs new file mode 100644 index 000000000000..fbb3e78b4973 --- /dev/null +++ b/codex-rs/utils/plugins/src/plugin_service_routing.rs @@ -0,0 +1,74 @@ +use std::ffi::OsStr; + +/// Process signal set by an eligible host to opt plugin-service requests into preview routing. +pub const CODEX_PLUGIN_SERVICE_PREVIEW_ENV_VAR: &str = "CODEX_PLUGIN_SERVICE_PREVIEW"; + +const PLUGIN_SERVICE_PREVIEW_ENABLED_VALUE: &str = "1"; +const PLUGIN_SERVICE_PREVIEW_COOKIE_NAME: &[u8] = b"oai-chat-plugin-service-preview"; +/// Routing cookie added to eligible plugin-service requests. +pub const PLUGIN_SERVICE_PREVIEW_COOKIE: &str = "oai-chat-plugin-service-preview=true"; + +/// Returns whether the host opted this process into plugin-service preview routing. +/// +/// The host owns employee eligibility. This signal is defense-in-depth routing, not an +/// authorization boundary; authentication and authorization remain the responsibility of the +/// existing request path and plugin-service. +pub fn plugin_service_preview_enabled() -> bool { + plugin_service_preview_enabled_from_value( + std::env::var_os(CODEX_PLUGIN_SERVICE_PREVIEW_ENV_VAR).as_deref(), + ) +} + +/// Rewrites plugin-service cookies so callers cannot override the process routing signal. +/// +/// Unrelated cookies are preserved. Any caller-provided preview cookie is removed before the +/// canonical routing cookie is added when preview routing is enabled. +pub fn plugin_service_routing_cookie( + existing_cookie_headers: &[&[u8]], + preview_enabled: bool, +) -> Option> { + let mut cookies = existing_cookie_headers + .iter() + .flat_map(|header| header.split(|byte| *byte == b';')) + .map(trim_cookie_whitespace) + .filter(|segment| !segment.is_empty()) + .filter(|segment| { + let name = segment + .iter() + .position(|byte| *byte == b'=') + .map_or(*segment, |separator| &segment[..separator]); + trim_cookie_whitespace(name) != PLUGIN_SERVICE_PREVIEW_COOKIE_NAME + }) + .map(<[u8]>::to_vec) + .collect::>(); + + if preview_enabled { + cookies.push(PLUGIN_SERVICE_PREVIEW_COOKIE.as_bytes().to_vec()); + } + + (!cookies.is_empty()).then(|| cookies.join(&b"; "[..])) +} + +fn plugin_service_preview_enabled_from_value(value: Option<&OsStr>) -> bool { + value == Some(OsStr::new(PLUGIN_SERVICE_PREVIEW_ENABLED_VALUE)) +} + +fn trim_cookie_whitespace(mut value: &[u8]) -> &[u8] { + while value + .first() + .is_some_and(|byte| matches!(byte, b' ' | b'\t')) + { + value = &value[1..]; + } + while value + .last() + .is_some_and(|byte| matches!(byte, b' ' | b'\t')) + { + value = &value[..value.len() - 1]; + } + value +} + +#[cfg(test)] +#[path = "plugin_service_routing_tests.rs"] +mod tests; diff --git a/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs b/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs new file mode 100644 index 000000000000..a2c3c3917c35 --- /dev/null +++ b/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs @@ -0,0 +1,44 @@ +use super::*; +use pretty_assertions::assert_eq; + +#[test] +fn preview_signal_requires_exact_enabled_value() { + for value in [ + None, + Some(""), + Some("0"), + Some("true"), + Some(" 1"), + Some("1 "), + ] { + assert!(!plugin_service_preview_enabled_from_value( + value.map(OsStr::new) + )); + } + assert!(plugin_service_preview_enabled_from_value(Some(OsStr::new( + "1" + )))); +} + +#[test] +fn routing_cookie_is_disabled_by_default_and_cannot_be_enabled_by_caller() { + assert_eq!(plugin_service_routing_cookie(&[], false), None); + assert_eq!( + plugin_service_routing_cookie(&[b"oai-chat-plugin-service-preview=true".as_slice()], false,), + None, + ); +} + +#[test] +fn routing_cookie_preserves_unrelated_cookies_and_replaces_caller_value() { + assert_eq!( + plugin_service_routing_cookie( + &[ + b"session=abc; oai-chat-plugin-service-preview=false".as_slice(), + b"theme=dark; oai-chat-plugin-service-preview=true".as_slice(), + ], + true, + ), + Some(b"session=abc; theme=dark; oai-chat-plugin-service-preview=true".to_vec()), + ); +} From e750db2ccbf8937dba9a11f34fe7a032bc46c0df Mon Sep 17 00:00:00 2001 From: Steven Lee Date: Tue, 16 Jun 2026 20:58:28 +0000 Subject: [PATCH 2/5] codex: address PR review feedback (#28582) --- codex-rs/Cargo.lock | 1 + codex-rs/chatgpt/Cargo.toml | 1 + codex-rs/chatgpt/src/chatgpt_client.rs | 51 ++++++++++++++++++- codex-rs/chatgpt/src/connectors.rs | 8 +-- .../src/chatgpt_cloudflare_cookies.rs | 12 +++++ codex-rs/codex-client/src/lib.rs | 1 + codex-rs/codex-mcp/src/codex_apps.rs | 4 ++ .../codex-mcp/src/connection_manager_tests.rs | 20 ++++++++ codex-rs/connectors/src/lib.rs | 20 ++++++++ codex-rs/core-plugins/src/remote.rs | 26 ++++++++-- .../core-plugins/src/remote/catalog_cache.rs | 8 +++ .../src/remote/catalog_cache_tests.rs | 21 ++++++++ codex-rs/core-plugins/src/remote_tests.rs | 17 +++++++ codex-rs/core/src/connectors.rs | 4 +- codex-rs/login/src/auth/default_client.rs | 1 + .../src/plugin_service_routing_tests.rs | 17 +++++-- 16 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 codex-rs/core-plugins/src/remote/catalog_cache_tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 147c5fed87b9..ef47818555de 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2300,6 +2300,7 @@ dependencies = [ "codex-plugin", "codex-utils-cargo-bin", "codex-utils-cli", + "codex-utils-plugins", "pretty_assertions", "serde", "serde_json", diff --git a/codex-rs/chatgpt/Cargo.toml b/codex-rs/chatgpt/Cargo.toml index c2cbb028303d..89a0279cd766 100644 --- a/codex-rs/chatgpt/Cargo.toml +++ b/codex-rs/chatgpt/Cargo.toml @@ -18,6 +18,7 @@ codex-login = { workspace = true } codex-model-provider = { workspace = true } codex-plugin = { workspace = true } codex-utils-cli = { workspace = true } +codex-utils-plugins = { workspace = true } serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["full"] } diff --git a/codex-rs/chatgpt/src/chatgpt_client.rs b/codex-rs/chatgpt/src/chatgpt_client.rs index 372f62e6966d..543bb9cf4270 100644 --- a/codex-rs/chatgpt/src/chatgpt_client.rs +++ b/codex-rs/chatgpt/src/chatgpt_client.rs @@ -1,6 +1,9 @@ use codex_core::config::Config; use codex_login::AuthManager; +use codex_login::default_client::chatgpt_cloudflare_cookie_header; use codex_login::default_client::create_client; +use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; +use codex_utils_plugins::plugin_service_routing::plugin_service_routing_cookie; use anyhow::Context; use serde::de::DeserializeOwned; @@ -9,18 +12,54 @@ use std::time::Duration; const OAI_PRODUCT_SKU_HEADER: &str = "OAI-Product-Sku"; const CODEX_PRODUCT_SKU: &str = "codex"; +#[derive(Clone, Copy)] +enum ChatgptRequestRouting { + Default, + PluginService, +} + /// Make a GET request to the ChatGPT backend API. pub(crate) async fn chatgpt_get_request( config: &Config, path: String, ) -> anyhow::Result { - chatgpt_get_request_with_timeout(config, path, /*timeout*/ None).await + chatgpt_get_request_with_timeout_inner( + config, + path, + /*timeout*/ None, + ChatgptRequestRouting::Default, + ) + .await } pub(crate) async fn chatgpt_get_request_with_timeout( config: &Config, path: String, timeout: Option, +) -> anyhow::Result { + chatgpt_get_request_with_timeout_inner(config, path, timeout, ChatgptRequestRouting::Default) + .await +} + +pub(crate) async fn chatgpt_get_plugin_service_request_with_timeout( + config: &Config, + path: String, + timeout: Option, +) -> anyhow::Result { + chatgpt_get_request_with_timeout_inner( + config, + path, + timeout, + ChatgptRequestRouting::PluginService, + ) + .await +} + +async fn chatgpt_get_request_with_timeout_inner( + config: &Config, + path: String, + timeout: Option, + routing: ChatgptRequestRouting, ) -> anyhow::Result { let chatgpt_base_url = &config.chatgpt_base_url; let auth_manager = @@ -52,6 +91,16 @@ pub(crate) async fn chatgpt_get_request_with_timeout( .header(OAI_PRODUCT_SKU_HEADER, CODEX_PRODUCT_SKU) .header("Content-Type", "application/json"); + if matches!(routing, ChatgptRequestRouting::PluginService) && plugin_service_preview_enabled() { + let cloudflare_cookie = chatgpt_cloudflare_cookie_header(&url); + let existing_cookie_headers = cloudflare_cookie.as_deref().into_iter().collect::>(); + if let Some(routing_cookie) = + plugin_service_routing_cookie(&existing_cookie_headers, /*preview_enabled*/ true) + { + request = request.header("Cookie", routing_cookie); + } + } + if let Some(timeout) = timeout { request = request.timeout(timeout); } diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index fa1a894cca65..b3817f9876b0 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::collections::HashSet; use std::time::Duration; -use crate::chatgpt_client::chatgpt_get_request_with_timeout; +use crate::chatgpt_client::chatgpt_get_plugin_service_request_with_timeout; use codex_app_server_protocol::AppInfo; use codex_connectors::ConnectorDirectoryCacheContext; @@ -21,6 +21,7 @@ pub use codex_core::connectors::with_app_enabled_state; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_plugin::AppConnectorId; +use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60); @@ -101,7 +102,7 @@ pub async fn list_all_connectors_with_options( auth.is_workspace_account(), force_refetch, |path| async move { - chatgpt_get_request_with_timeout::( + chatgpt_get_plugin_service_request_with_timeout::( config, path, Some(DIRECTORY_CONNECTORS_TIMEOUT), @@ -127,7 +128,8 @@ fn connector_directory_cache_context( auth.get_account_id(), auth.get_chatgpt_user_id(), auth.is_workspace_account(), - ), + ) + .plugin_service_preview(plugin_service_preview_enabled()), ) } diff --git a/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs b/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs index c5f4bbd4eb1e..d97de8a83071 100644 --- a/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs +++ b/codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs @@ -55,6 +55,18 @@ pub fn with_chatgpt_cloudflare_cookie_store( builder.cookie_provider(Arc::clone(&SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE)) } +/// Returns the Cloudflare infrastructure cookies that the shared Codex cookie store would attach +/// to `url`. +/// +/// Use this only when a caller must construct an explicit `Cookie` header. Reqwest does not add +/// cookie-provider values when that header is already present. +pub fn chatgpt_cloudflare_cookie_header(url: &str) -> Option> { + let url = reqwest::Url::parse(url).ok()?; + SHARED_CHATGPT_CLOUDFLARE_COOKIE_STORE + .cookies(&url) + .map(|value| value.as_bytes().to_vec()) +} + fn is_chatgpt_cookie_url(url: &reqwest::Url) -> bool { match url.scheme() { "https" => {} diff --git a/codex-rs/codex-client/src/lib.rs b/codex-rs/codex-client/src/lib.rs index 78daa6c28817..13ec94754c77 100644 --- a/codex-rs/codex-client/src/lib.rs +++ b/codex-rs/codex-client/src/lib.rs @@ -10,6 +10,7 @@ mod sse; mod telemetry; mod transport; +pub use crate::chatgpt_cloudflare_cookies::chatgpt_cloudflare_cookie_header; pub use crate::chatgpt_cloudflare_cookies::with_chatgpt_cloudflare_cookie_store; pub use crate::chatgpt_hosts::is_allowed_chatgpt_host; pub use crate::custom_ca::BuildCustomCaTransportError; diff --git a/codex-rs/codex-mcp/src/codex_apps.rs b/codex-rs/codex-mcp/src/codex_apps.rs index 53b546b8a010..b01b351e4903 100644 --- a/codex-rs/codex-mcp/src/codex_apps.rs +++ b/codex-rs/codex-mcp/src/codex_apps.rs @@ -16,6 +16,7 @@ use anyhow::Context; use codex_login::CodexAuth; use codex_protocol::mcp::McpServerInfo; use codex_utils_plugins::mcp_connector::sanitize_name; +use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; use serde::Deserialize; use serde::Serialize; use sha1::Digest; @@ -26,6 +27,8 @@ pub struct CodexAppsToolsCacheKey { pub(crate) account_id: Option, pub(crate) chatgpt_user_id: Option, pub(crate) is_workspace_account: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub(crate) plugin_service_preview: bool, } pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCacheKey { @@ -33,6 +36,7 @@ pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCac account_id: auth.and_then(CodexAuth::get_account_id), chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id), is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account), + plugin_service_preview: plugin_service_preview_enabled(), } } diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 05b391702708..ed4c1a766113 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -86,6 +86,7 @@ fn create_codex_apps_tools_cache_context( account_id: account_id.map(ToOwned::to_owned), chatgpt_user_id: chatgpt_user_id.map(ToOwned::to_owned), is_workspace_account: false, + plugin_service_preview: false, }, } } @@ -611,6 +612,24 @@ fn codex_apps_tools_cache_is_scoped_per_user() { ); } +#[test] +fn codex_apps_tools_cache_is_scoped_by_plugin_service_preview() { + let codex_home = tempdir().expect("tempdir"); + let regular = create_codex_apps_tools_cache_context( + codex_home.path().to_path_buf(), + Some("account-one"), + Some("user-one"), + ); + let mut preview = regular.clone(); + preview.user_key.plugin_service_preview = true; + + assert_ne!(regular.tools_cache_path(), preview.tools_cache_path()); + assert_ne!( + regular.server_info_cache_path(), + preview.server_info_cache_path() + ); +} + #[test] fn codex_apps_tools_cache_preserves_formerly_disallowed_connectors() { let codex_home = tempdir().expect("tempdir"); @@ -1263,6 +1282,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() { account_id: None, chatgpt_user_id: None, is_workspace_account: false, + plugin_service_preview: false, }, /*prefix_mcp_tool_names*/ true, ElicitationCapability::default(), diff --git a/codex-rs/connectors/src/lib.rs b/codex-rs/connectors/src/lib.rs index a75120c0a457..8a391f150b4c 100644 --- a/codex-rs/connectors/src/lib.rs +++ b/codex-rs/connectors/src/lib.rs @@ -33,6 +33,8 @@ pub struct ConnectorDirectoryCacheKey { account_id: Option, chatgpt_user_id: Option, is_workspace_account: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + plugin_service_preview: bool, } impl ConnectorDirectoryCacheKey { @@ -47,8 +49,14 @@ impl ConnectorDirectoryCacheKey { account_id, chatgpt_user_id, is_workspace_account, + plugin_service_preview: false, } } + + pub fn plugin_service_preview(mut self, plugin_service_preview: bool) -> Self { + self.plugin_service_preview = plugin_service_preview; + self + } } #[derive(Clone)] @@ -492,6 +500,18 @@ mod tests { ConnectorDirectoryCacheContext::new(codex_home.path().to_path_buf(), cache_key(id)) } + #[test] + fn directory_cache_is_scoped_by_plugin_service_preview() { + let codex_home = tempfile::tempdir().expect("tempdir"); + let regular = cache_context(&codex_home, "user"); + let preview = ConnectorDirectoryCacheContext::new( + codex_home.path().to_path_buf(), + cache_key("user").plugin_service_preview(true), + ); + + assert_ne!(regular.cache_path(), preview.cache_path()); + } + fn clear_directory_memory_cache() { let mut cache_guard = CONNECTOR_DIRECTORY_CACHE .lock() diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 7e71622bf5a9..b5f26b982987 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -10,6 +10,7 @@ use codex_app_server_protocol::PluginInterface; use codex_app_server_protocol::SkillInterface; use codex_login::CodexAuth; use codex_login::default_client::build_reqwest_client; +use codex_login::default_client::chatgpt_cloudflare_cookie_header; use codex_plugin::AppConnectorId; use codex_plugin::AppDeclaration; use codex_plugin::PluginId; @@ -1878,12 +1879,31 @@ async fn send_plugin_service_request_with_preview( ) -> Result { let (client, request) = request.build_split(); let mut request = request?; - let headers = request.headers_mut(); - let existing_cookie_headers = headers + let cloudflare_cookie = (preview_enabled && !request.headers().contains_key(COOKIE)) + .then(|| chatgpt_cloudflare_cookie_header(request.url().as_str())) + .flatten(); + apply_plugin_service_routing_cookie( + request.headers_mut(), + preview_enabled, + cloudflare_cookie.as_deref(), + ); + + client.execute(request).await +} + +fn apply_plugin_service_routing_cookie( + headers: &mut reqwest::header::HeaderMap, + preview_enabled: bool, + cloudflare_cookie: Option<&[u8]>, +) { + let mut existing_cookie_headers = headers .get_all(COOKIE) .iter() .map(|value| value.as_bytes().to_vec()) .collect::>(); + if let Some(cloudflare_cookie) = cloudflare_cookie { + existing_cookie_headers.push(cloudflare_cookie.to_vec()); + } let existing_cookie_headers = existing_cookie_headers .iter() .map(Vec::as_slice) @@ -1896,6 +1916,4 @@ async fn send_plugin_service_request_with_preview( { headers.insert(COOKIE, routing_cookie); } - - client.execute(request).await } diff --git a/codex-rs/core-plugins/src/remote/catalog_cache.rs b/codex-rs/core-plugins/src/remote/catalog_cache.rs index d49885dfbb60..52161cec3b5d 100644 --- a/codex-rs/core-plugins/src/remote/catalog_cache.rs +++ b/codex-rs/core-plugins/src/remote/catalog_cache.rs @@ -1,6 +1,7 @@ use super::RemotePluginDirectoryItem; use super::RemotePluginServiceConfig; use codex_login::CodexAuth; +use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; use serde::Deserialize; use serde::Serialize; use std::path::Path; @@ -16,6 +17,8 @@ struct RemotePluginCatalogCacheKey { account_id: Option, chatgpt_user_id: Option, is_workspace_account: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + plugin_service_preview: bool, } impl RemotePluginCatalogCacheKey { @@ -25,6 +28,7 @@ impl RemotePluginCatalogCacheKey { account_id: auth.get_account_id(), chatgpt_user_id: auth.get_chatgpt_user_id(), is_workspace_account: auth.is_workspace_account(), + plugin_service_preview: plugin_service_preview_enabled(), } } } @@ -109,3 +113,7 @@ fn cache_path(codex_home: &Path, cache_key: &RemotePluginCatalogCacheKey) -> Pat .join(REMOTE_PLUGIN_CATALOG_DISK_CACHE_DIR) .join(format!("{cache_key_hash:016x}.json")) } + +#[cfg(test)] +#[path = "catalog_cache_tests.rs"] +mod tests; diff --git a/codex-rs/core-plugins/src/remote/catalog_cache_tests.rs b/codex-rs/core-plugins/src/remote/catalog_cache_tests.rs new file mode 100644 index 000000000000..60232274dffd --- /dev/null +++ b/codex-rs/core-plugins/src/remote/catalog_cache_tests.rs @@ -0,0 +1,21 @@ +use super::*; + +#[test] +fn remote_plugin_catalog_cache_is_scoped_by_plugin_service_preview() { + let regular = RemotePluginCatalogCacheKey { + chatgpt_base_url: "https://chatgpt.com/backend-api".to_string(), + account_id: Some("account".to_string()), + chatgpt_user_id: Some("user".to_string()), + is_workspace_account: true, + plugin_service_preview: false, + }; + let preview = RemotePluginCatalogCacheKey { + plugin_service_preview: true, + ..regular.clone() + }; + + assert_ne!( + cache_path(Path::new("/codex-home"), ®ular), + cache_path(Path::new("/codex-home"), &preview) + ); +} diff --git a/codex-rs/core-plugins/src/remote_tests.rs b/codex-rs/core-plugins/src/remote_tests.rs index bef7f543f57a..60a44bf978af 100644 --- a/codex-rs/core-plugins/src/remote_tests.rs +++ b/codex-rs/core-plugins/src/remote_tests.rs @@ -299,6 +299,23 @@ fn recommended_plugins_ignore_invalid_remote_plugin_ids() { ); } +#[test] +fn plugin_service_routing_preserves_cloudflare_cookie_jar_values() { + let mut headers = reqwest::header::HeaderMap::new(); + let cloudflare_cookie = HeaderValue::from_static("cf_clearance=clearance; _cfuvid=visitor"); + + apply_plugin_service_routing_cookie( + &mut headers, + /*preview_enabled*/ true, + Some(cloudflare_cookie.as_bytes()), + ); + + assert_eq!( + headers.get(COOKIE).and_then(|value| value.to_str().ok()), + Some("cf_clearance=clearance; _cfuvid=visitor; oai-chat-plugin-service-preview=true"), + ); +} + #[tokio::test] async fn plugin_service_request_does_not_add_preview_cookie_when_disabled() { let server = MockServer::start().await; diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 87c309716277..aa442f23d99f 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -41,6 +41,7 @@ use codex_mcp::codex_apps_tools_cache_key; use codex_mcp::compute_auth_statuses; use codex_mcp::effective_mcp_servers; use codex_mcp::tool_plugin_provenance; +use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30); @@ -460,7 +461,8 @@ async fn cached_directory_connectors_for_tool_suggest_with_auth( Some(account_id), auth.get_chatgpt_user_id(), is_workspace_account, - ), + ) + .plugin_service_preview(plugin_service_preview_enabled()), ); codex_connectors::cached_directory_connectors(&cache_context).unwrap_or_default() diff --git a/codex-rs/login/src/auth/default_client.rs b/codex-rs/login/src/auth/default_client.rs index f72727bfca97..5bbc8d5e4e5e 100644 --- a/codex-rs/login/src/auth/default_client.rs +++ b/codex-rs/login/src/auth/default_client.rs @@ -11,6 +11,7 @@ use codex_client::CodexHttpClient; pub use codex_client::CodexRequestBuilder; use codex_client::build_reqwest_client_for_route; use codex_client::build_reqwest_client_with_custom_ca; +pub use codex_client::chatgpt_cloudflare_cookie_header; use codex_client::with_chatgpt_cloudflare_cookie_store; use codex_terminal_detection::user_agent; use reqwest::header::HeaderMap; diff --git a/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs b/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs index a2c3c3917c35..3dfab4fe17f8 100644 --- a/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs +++ b/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs @@ -22,10 +22,19 @@ fn preview_signal_requires_exact_enabled_value() { #[test] fn routing_cookie_is_disabled_by_default_and_cannot_be_enabled_by_caller() { - assert_eq!(plugin_service_routing_cookie(&[], false), None); assert_eq!( - plugin_service_routing_cookie(&[b"oai-chat-plugin-service-preview=true".as_slice()], false,), - None, + plugin_service_routing_cookie(&[], /*preview_enabled*/ false), + None + ); + assert_eq!( + plugin_service_routing_cookie( + &[ + b"session=abc; oai-chat-plugin-service-preview=true".as_slice(), + b"theme=dark".as_slice(), + ], + /*preview_enabled*/ false, + ), + Some(b"session=abc; theme=dark".to_vec()), ); } @@ -37,7 +46,7 @@ fn routing_cookie_preserves_unrelated_cookies_and_replaces_caller_value() { b"session=abc; oai-chat-plugin-service-preview=false".as_slice(), b"theme=dark; oai-chat-plugin-service-preview=true".as_slice(), ], - true, + /*preview_enabled*/ true, ), Some(b"session=abc; theme=dark; oai-chat-plugin-service-preview=true".to_vec()), ); From 39df203474a8f4a92f92c79b52e489ab59befb00 Mon Sep 17 00:00:00 2001 From: Steven Lee Date: Tue, 16 Jun 2026 21:36:00 +0000 Subject: [PATCH 3/5] codex: use config for plugin service preview (#28582) --- codex-rs/Cargo.lock | 1 + .../src/request_processors/plugins.rs | 51 +++++++------------ codex-rs/chatgpt/Cargo.toml | 1 + codex-rs/chatgpt/src/chatgpt_client.rs | 6 ++- codex-rs/chatgpt/src/connectors.rs | 4 +- codex-rs/codex-mcp/src/codex_apps.rs | 8 +-- .../codex-mcp/src/connection_manager_tests.rs | 1 + codex-rs/codex-mcp/src/mcp/mod.rs | 13 +++-- codex-rs/codex-mcp/src/mcp/mod_tests.rs | 21 ++++++-- .../core-plugins/src/discoverable_tests.rs | 1 + codex-rs/core-plugins/src/manager.rs | 8 +++ codex-rs/core-plugins/src/remote.rs | 24 +++++---- .../core-plugins/src/remote/catalog_cache.rs | 3 +- .../remote/remote_installed_plugin_sync.rs | 1 + codex-rs/core-plugins/src/remote/share.rs | 14 ++--- .../core-plugins/src/remote/share/tests.rs | 1 + codex-rs/core/config.schema.json | 6 +++ codex-rs/core/src/config/config_tests.rs | 48 +++++++++++++++++ codex-rs/core/src/config/mod.rs | 2 + codex-rs/core/src/connectors.rs | 5 +- codex-rs/core/src/mcp.rs | 1 + codex-rs/core/src/mcp_tool_call_tests.rs | 8 ++- .../core/src/plugins/discoverable_tests.rs | 1 + codex-rs/core/src/session/mcp.rs | 2 +- codex-rs/core/src/session/session.rs | 7 ++- codex-rs/core/src/thread_manager_tests.rs | 1 + codex-rs/ext/mcp/src/lib.rs | 3 ++ codex-rs/features/src/lib.rs | 11 ++++ .../plugins/src/plugin_service_routing.rs | 24 ++------- .../src/plugin_service_routing_tests.rs | 19 ------- 30 files changed, 182 insertions(+), 114 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ef47818555de..6311c2d31756 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2294,6 +2294,7 @@ dependencies = [ "codex-app-server-protocol", "codex-connectors", "codex-core", + "codex-features", "codex-git-utils", "codex-login", "codex-model-provider", diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 76123b6aa240..6be34be83e7e 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -115,6 +115,13 @@ fn load_shared_plugin_ids_by_local_path( }) } +fn remote_plugin_service_config(config: &Config) -> RemotePluginServiceConfig { + RemotePluginServiceConfig { + chatgpt_base_url: config.chatgpt_base_url.clone(), + plugin_service_preview: config.features.enabled(Feature::PluginServicePreview), + } +} + fn share_context_for_source( source: &MarketplacePluginSource, shared_plugin_ids_by_local_path: &std::collections::BTreeMap, @@ -567,9 +574,7 @@ impl PluginRequestProcessor { !explicit_marketplace_kinds && config.features.enabled(Feature::RemotePlugin); let use_remote_global_catalog = include_global_remote && auth_mode.is_some_and(AuthMode::uses_codex_backend); - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); let refresh_global_remote_catalog_cache = use_remote_global_catalog && codex_core_plugins::remote::has_cached_global_remote_plugin_catalog( config.codex_home.as_path(), @@ -1016,9 +1021,7 @@ impl PluginRequestProcessor { ); let share_context = match share_context { Some(context) => { - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); match codex_core_plugins::remote::fetch_remote_plugin_share_context( &remote_plugin_service_config, auth.as_ref(), @@ -1122,9 +1125,7 @@ impl PluginRequestProcessor { "remote plugin read is not enabled for marketplace {remote_marketplace_name}" ))); } - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); validate_remote_plugin_id(&plugin_name)?; let remote_detail = codex_core_plugins::remote::fetch_remote_plugin_detail( &remote_plugin_service_config, @@ -1180,9 +1181,7 @@ impl PluginRequestProcessor { } let auth = self.auth_manager.auth().await; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); let remote_skill_detail = codex_core_plugins::remote::fetch_remote_plugin_skill_detail( &remote_plugin_service_config, auth.as_ref(), @@ -1233,9 +1232,7 @@ impl PluginRequestProcessor { validate_client_plugin_share_targets(share_targets)?; } - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); let access_policy = codex_core_plugins::remote::RemotePluginShareAccessPolicy { discoverability: discoverability.map(remote_plugin_share_discoverability), share_targets: share_targets.map(remote_plugin_share_targets), @@ -1276,9 +1273,7 @@ impl PluginRequestProcessor { } validate_client_plugin_share_targets(&share_targets)?; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); let result = codex_core_plugins::remote::update_remote_plugin_share_targets( &remote_plugin_service_config, auth.as_ref(), @@ -1306,9 +1301,7 @@ impl PluginRequestProcessor { _params: PluginShareListParams, ) -> Result { let (config, auth) = self.load_plugin_share_config_and_auth().await?; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); let data = codex_core_plugins::remote::list_remote_plugin_shares( &remote_plugin_service_config, auth.as_ref(), @@ -1345,9 +1338,7 @@ impl PluginRequestProcessor { return Err(invalid_request("invalid remote plugin id")); } - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); let result = codex_core_plugins::remote::checkout_remote_plugin_share( &remote_plugin_service_config, auth.as_ref(), @@ -1378,9 +1369,7 @@ impl PluginRequestProcessor { return Err(invalid_request("invalid remote plugin id")); } - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); codex_core_plugins::remote::delete_remote_plugin_share( &remote_plugin_service_config, auth.as_ref(), @@ -1512,9 +1501,7 @@ impl PluginRequestProcessor { validate_remote_plugin_id(&remote_plugin_id)?; let auth = self.auth_manager.auth().await; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); let remote_detail = codex_core_plugins::remote::fetch_remote_plugin_detail_with_download_urls( &remote_plugin_service_config, @@ -1977,9 +1964,7 @@ impl PluginRequestProcessor { validate_remote_plugin_id(&plugin_id)?; let auth = self.auth_manager.auth().await; - let remote_plugin_service_config = RemotePluginServiceConfig { - chatgpt_base_url: config.chatgpt_base_url.clone(), - }; + let remote_plugin_service_config = remote_plugin_service_config(&config); let uninstall_result = codex_core_plugins::remote::uninstall_remote_plugin( &remote_plugin_service_config, auth.as_ref(), diff --git a/codex-rs/chatgpt/Cargo.toml b/codex-rs/chatgpt/Cargo.toml index 89a0279cd766..255df168db71 100644 --- a/codex-rs/chatgpt/Cargo.toml +++ b/codex-rs/chatgpt/Cargo.toml @@ -13,6 +13,7 @@ clap = { workspace = true, features = ["derive"] } codex-app-server-protocol = { workspace = true } codex-connectors = { workspace = true } codex-core = { workspace = true } +codex-features = { workspace = true } codex-git-utils = { workspace = true } codex-login = { workspace = true } codex-model-provider = { workspace = true } diff --git a/codex-rs/chatgpt/src/chatgpt_client.rs b/codex-rs/chatgpt/src/chatgpt_client.rs index 543bb9cf4270..80643c688197 100644 --- a/codex-rs/chatgpt/src/chatgpt_client.rs +++ b/codex-rs/chatgpt/src/chatgpt_client.rs @@ -1,8 +1,8 @@ use codex_core::config::Config; +use codex_features::Feature; use codex_login::AuthManager; use codex_login::default_client::chatgpt_cloudflare_cookie_header; use codex_login::default_client::create_client; -use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; use codex_utils_plugins::plugin_service_routing::plugin_service_routing_cookie; use anyhow::Context; @@ -91,7 +91,9 @@ async fn chatgpt_get_request_with_timeout_inner( .header(OAI_PRODUCT_SKU_HEADER, CODEX_PRODUCT_SKU) .header("Content-Type", "application/json"); - if matches!(routing, ChatgptRequestRouting::PluginService) && plugin_service_preview_enabled() { + if matches!(routing, ChatgptRequestRouting::PluginService) + && config.features.enabled(Feature::PluginServicePreview) + { let cloudflare_cookie = chatgpt_cloudflare_cookie_header(&url); let existing_cookie_headers = cloudflare_cookie.as_deref().into_iter().collect::>(); if let Some(routing_cookie) = diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index b3817f9876b0..b52366f2196b 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -18,10 +18,10 @@ pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_o pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status; pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools; pub use codex_core::connectors::with_app_enabled_state; +use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_plugin::AppConnectorId; -use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60); @@ -129,7 +129,7 @@ fn connector_directory_cache_context( auth.get_chatgpt_user_id(), auth.is_workspace_account(), ) - .plugin_service_preview(plugin_service_preview_enabled()), + .plugin_service_preview(config.features.enabled(Feature::PluginServicePreview)), ) } diff --git a/codex-rs/codex-mcp/src/codex_apps.rs b/codex-rs/codex-mcp/src/codex_apps.rs index b01b351e4903..20c25afe2859 100644 --- a/codex-rs/codex-mcp/src/codex_apps.rs +++ b/codex-rs/codex-mcp/src/codex_apps.rs @@ -16,7 +16,6 @@ use anyhow::Context; use codex_login::CodexAuth; use codex_protocol::mcp::McpServerInfo; use codex_utils_plugins::mcp_connector::sanitize_name; -use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; use serde::Deserialize; use serde::Serialize; use sha1::Digest; @@ -31,12 +30,15 @@ pub struct CodexAppsToolsCacheKey { pub(crate) plugin_service_preview: bool, } -pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCacheKey { +pub fn codex_apps_tools_cache_key( + auth: Option<&CodexAuth>, + plugin_service_preview: bool, +) -> CodexAppsToolsCacheKey { CodexAppsToolsCacheKey { account_id: auth.and_then(CodexAuth::get_account_id), chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id), is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account), - plugin_service_preview: plugin_service_preview_enabled(), + plugin_service_preview, } } diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index ed4c1a766113..2111bf72c585 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -1183,6 +1183,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, + /*plugin_service_preview*/ false, ); config.environment_id = "remote".to_string(); config.default_tools_approval_mode = Some(AppToolApproval::Prompt); diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 6c036e4694eb..229c301b9eeb 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -34,7 +34,6 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::McpAuthStatus; use codex_utils_plugins::plugin_service_routing::PLUGIN_SERVICE_PREVIEW_COOKIE; -use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; use rmcp::model::ElicitationCapability; use rmcp::model::ReadResourceRequestParams; use rmcp::model::ReadResourceResult; @@ -137,6 +136,8 @@ pub struct McpConfig { /// ChatGPT auth is checked separately before a materialized host-owned Apps /// server can be used. pub apps_enabled: bool, + /// Whether built-in plugin-service requests should use preview routing. + pub plugin_service_preview: bool, /// Whether model-visible MCP tool namespaces should keep the legacy /// `mcp__` prefix. pub prefix_mcp_tool_names: bool, @@ -302,7 +303,7 @@ pub async fn read_mcp_resource( PermissionProfile::default(), runtime_context, config.codex_home.clone(), - codex_apps_tools_cache_key(auth), + codex_apps_tools_cache_key(auth, config.plugin_service_preview), config.prefix_mcp_tool_names, config.client_elicitation_capability.clone(), /*supports_openai_form_elicitation*/ false, @@ -375,7 +376,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( PermissionProfile::default(), runtime_context, config.codex_home.clone(), - codex_apps_tools_cache_key(auth), + codex_apps_tools_cache_key(auth, config.plugin_service_preview), config.prefix_mcp_tool_names, config.client_elicitation_capability.clone(), /*supports_openai_form_elicitation*/ false, @@ -453,11 +454,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>, + plugin_service_preview: bool, ) -> McpServerConfig { mcp_server_config_for_url( codex_apps_mcp_url_for_base_url(chatgpt_base_url), apps_mcp_product_sku, - plugin_service_preview_enabled(), + plugin_service_preview, ) } @@ -465,6 +467,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>, + plugin_service_preview: bool, ) -> 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") { @@ -475,7 +478,7 @@ pub fn hosted_plugin_runtime_mcp_server_config( mcp_server_config_for_url( format!("{base_url}/ps/mcp"), apps_mcp_product_sku, - plugin_service_preview_enabled(), + plugin_service_preview, ) } diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index 4cc79bff5f89..f6620372eab2 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -31,6 +31,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig { codex_linux_sandbox_exe: None, use_legacy_landlock: false, apps_enabled: false, + plugin_service_preview: false, prefix_mcp_tool_names: true, client_elicitation_capability: ElicitationCapability::default(), mcp_server_catalog: ResolvedMcpCatalog::default(), @@ -131,7 +132,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, + /*plugin_service_preview*/ false, + ), )); config.mcp_server_catalog = catalog.build(); config.plugin_capability_summaries = vec![ @@ -197,7 +202,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, + /*plugin_service_preview*/ false, + ), )); config.mcp_server_catalog = catalog.build(); config.plugin_capability_summaries = vec![PluginCapabilitySummary { @@ -249,8 +258,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, + /*plugin_service_preview*/ false, + ); let url = match &config.transport { McpServerTransportConfig::StreamableHttp { url, .. } => url, _ => panic!("expected streamable http transport for codex apps"), @@ -402,6 +414,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(), + config.plugin_service_preview, ), )); config.mcp_server_catalog = catalog.build(); diff --git a/codex-rs/core-plugins/src/discoverable_tests.rs b/codex-rs/core-plugins/src/discoverable_tests.rs index d472c76344c4..f7785700afd9 100644 --- a/codex-rs/core-plugins/src/discoverable_tests.rs +++ b/codex-rs/core-plugins/src/discoverable_tests.rs @@ -850,6 +850,7 @@ remote_plugin = true codex_home.path(), &RemotePluginServiceConfig { chatgpt_base_url: plugins.chatgpt_base_url.clone(), + plugin_service_preview: false, }, Some(&auth), ) diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 19b48b2c017e..6407743198aa 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -104,6 +104,7 @@ pub struct PluginsConfigInput { pub config_layer_stack: ConfigLayerStack, pub plugins_enabled: bool, pub remote_plugin_enabled: bool, + pub plugin_service_preview: bool, pub chatgpt_base_url: String, } @@ -118,9 +119,15 @@ impl PluginsConfigInput { config_layer_stack, plugins_enabled, remote_plugin_enabled, + plugin_service_preview: false, chatgpt_base_url, } } + + pub fn plugin_service_preview(mut self, plugin_service_preview: bool) -> Self { + self.plugin_service_preview = plugin_service_preview; + self + } } /// Inputs used to select endpoint-backed plugin install candidates. @@ -219,6 +226,7 @@ struct ConfiguredMarketplaceUpgradeState { fn remote_plugin_service_config(config: &PluginsConfigInput) -> RemotePluginServiceConfig { RemotePluginServiceConfig { chatgpt_base_url: config.chatgpt_base_url.clone(), + plugin_service_preview: config.plugin_service_preview, } } diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index b5f26b982987..67cdb55e56a5 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -16,7 +16,6 @@ use codex_plugin::AppDeclaration; use codex_plugin::PluginId; use codex_plugin::app_connector_ids_from_declarations; use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; use codex_utils_plugins::plugin_service_routing::plugin_service_routing_cookie; use reqwest::RequestBuilder; use reqwest::header::COOKIE; @@ -123,6 +122,7 @@ const REMOTE_INSTALLED_MARKETPLACE_DISPLAY_ORDER: [(&str, &str); 6] = [ #[derive(Debug, Clone, PartialEq, Eq)] pub struct RemotePluginServiceConfig { pub chatgpt_base_url: String, + pub plugin_service_preview: bool, } #[derive(Debug, Clone, PartialEq)] @@ -824,7 +824,7 @@ pub async fn fetch_recommended_plugins( let request = authenticated_request(client.get(&url), auth)? .timeout(RECOMMENDED_PLUGINS_TIMEOUT) .query(&[("scope", "GLOBAL")]); - let response: RecommendedPluginsResponse = send_and_decode(request, &url).await?; + let response: RecommendedPluginsResponse = send_and_decode(config, request, &url).await?; Ok(recommended_plugins_mode(response)) } @@ -1128,7 +1128,7 @@ pub async fn fetch_remote_plugin_skill_detail( let url = remote_plugin_skill_detail_url(config, plugin_id, skill_name)?; let client = build_reqwest_client(); let request = authenticated_request(client.get(&url), auth)?; - let response: RemotePluginSkillDetailResponse = send_and_decode(request, &url).await?; + let response: RemotePluginSkillDetailResponse = send_and_decode(config, request, &url).await?; if response.plugin_id != plugin_id { return Err(RemotePluginCatalogError::UnexpectedPluginId { expected: plugin_id.to_string(), @@ -1289,7 +1289,7 @@ pub async fn install_remote_plugin( .query(&[("includeAppsNeedingAuth", "true")]), auth, )?; - let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; + let response: RemotePluginMutationResponse = send_and_decode(config, request, &url).await?; if response.id != plugin_id { return Err(RemotePluginCatalogError::UnexpectedPluginId { expected: plugin_id.to_string(), @@ -1327,7 +1327,7 @@ pub async fn uninstall_remote_plugin( let url = format!("{base_url}/ps/plugins/{plugin_id}/uninstall"); let client = build_reqwest_client(); let request = authenticated_request(client.post(&url), auth)?; - let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; + let response: RemotePluginMutationResponse = send_and_decode(config, request, &url).await?; if response.id != plugin_id { return Err(RemotePluginCatalogError::UnexpectedPluginId { expected: plugin_id.to_string(), @@ -1744,7 +1744,7 @@ async fn get_remote_plugin_list_page( if let Some(page_token) = page_token { request = request.query(&[("pageToken", page_token)]); } - send_and_decode(request, &url).await + send_and_decode(config, request, &url).await } async fn get_remote_shared_workspace_plugins_page( @@ -1760,7 +1760,7 @@ async fn get_remote_shared_workspace_plugins_page( if let Some(page_token) = page_token { request = request.query(&[("pageToken", page_token)]); } - send_and_decode(request, &url).await + send_and_decode(config, request, &url).await } async fn get_remote_plugin_installed_page( @@ -1781,7 +1781,7 @@ async fn get_remote_plugin_installed_page( if let Some(page_token) = page_token { request = request.query(&[("pageToken", page_token)]); } - send_and_decode(request, &url).await + send_and_decode(config, request, &url).await } async fn fetch_plugin_detail( @@ -1797,7 +1797,7 @@ async fn fetch_plugin_detail( if include_download_urls { request = request.query(&[("includeDownloadUrls", true)]); } - send_and_decode(request, &url).await + send_and_decode(config, request, &url).await } fn remote_plugin_skill_detail_url( @@ -1842,10 +1842,11 @@ fn authenticated_request( } async fn send_and_decode Deserialize<'de>>( + config: &RemotePluginServiceConfig, request: RequestBuilder, url: &str, ) -> Result { - let response = send_plugin_service_request(request) + let response = send_plugin_service_request(config, request) .await .map_err(|source| RemotePluginCatalogError::Request { url: url.to_string(), @@ -1868,9 +1869,10 @@ async fn send_and_decode Deserialize<'de>>( } pub(super) async fn send_plugin_service_request( + config: &RemotePluginServiceConfig, request: RequestBuilder, ) -> Result { - send_plugin_service_request_with_preview(request, plugin_service_preview_enabled()).await + send_plugin_service_request_with_preview(request, config.plugin_service_preview).await } async fn send_plugin_service_request_with_preview( diff --git a/codex-rs/core-plugins/src/remote/catalog_cache.rs b/codex-rs/core-plugins/src/remote/catalog_cache.rs index 52161cec3b5d..a1dc0280d26e 100644 --- a/codex-rs/core-plugins/src/remote/catalog_cache.rs +++ b/codex-rs/core-plugins/src/remote/catalog_cache.rs @@ -1,7 +1,6 @@ use super::RemotePluginDirectoryItem; use super::RemotePluginServiceConfig; use codex_login::CodexAuth; -use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; use serde::Deserialize; use serde::Serialize; use std::path::Path; @@ -28,7 +27,7 @@ impl RemotePluginCatalogCacheKey { account_id: auth.get_account_id(), chatgpt_user_id: auth.get_chatgpt_user_id(), is_workspace_account: auth.is_workspace_account(), - plugin_service_preview: plugin_service_preview_enabled(), + plugin_service_preview: config.plugin_service_preview, } } } 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 2a23b066edaa..7c385bf7a92e 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 @@ -544,6 +544,7 @@ mod tests { .await; let config = RemotePluginServiceConfig { chatgpt_base_url: format!("{}/backend-api", server.uri()), + plugin_service_preview: false, }; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); diff --git a/codex-rs/core-plugins/src/remote/share.rs b/codex-rs/core-plugins/src/remote/share.rs index 961257d24591..12c75d79f0b9 100644 --- a/codex-rs/core-plugins/src/remote/share.rs +++ b/codex-rs/core-plugins/src/remote/share.rs @@ -281,7 +281,7 @@ pub async fn delete_remote_plugin_share( let url = format!("{base_url}/public/plugins/workspace/{remote_plugin_id}"); let client = build_reqwest_client(); let request = authenticated_request(client.delete(&url), auth)?; - send_and_expect_status(request, &url, &[StatusCode::NO_CONTENT]).await?; + send_and_expect_status(config, request, &url, &[StatusCode::NO_CONTENT]).await?; if let Err(err) = local_paths::remove_plugin_share_local_path(codex_home, remote_plugin_id) { warn!( remote_plugin_id = %remote_plugin_id, @@ -319,7 +319,8 @@ pub async fn update_remote_plugin_share_targets( targets, }, ); - let response: RemotePluginShareUpdateTargetsResponse = send_and_decode(request, &url).await?; + let response: RemotePluginShareUpdateTargetsResponse = + send_and_decode(config, request, &url).await?; Ok(RemotePluginShareUpdateTargetsResult { principals: response.principals, discoverability: response.discoverability, @@ -384,7 +385,7 @@ async fn get_created_workspace_plugins_page( if let Some(page_token) = page_token { request = request.query(&[("pageToken", page_token)]); } - send_and_decode(request, &url).await + send_and_decode(config, request, &url).await } async fn create_workspace_plugin_upload( @@ -405,7 +406,7 @@ async fn create_workspace_plugin_upload( plugin_id: remote_plugin_id, }, ); - send_and_decode(request, &url).await + send_and_decode(config, request, &url).await } async fn put_workspace_plugin_upload( @@ -452,7 +453,7 @@ async fn finalize_workspace_plugin_upload( }; let client = build_reqwest_client(); let request = authenticated_request(client.post(&url), auth)?.json(&body); - send_and_decode(request, &url).await + send_and_decode(config, request, &url).await } fn archive_filename(plugin_path: &Path) -> Result { @@ -489,11 +490,12 @@ fn archive_plugin_for_upload_with_limit( } async fn send_and_expect_status( + config: &RemotePluginServiceConfig, request: RequestBuilder, url_for_error: &str, expected_statuses: &[StatusCode], ) -> Result<(), RemotePluginCatalogError> { - let response = send_plugin_service_request(request) + let response = send_plugin_service_request(config, request) .await .map_err(|source| RemotePluginCatalogError::Request { url: url_for_error.to_string(), diff --git a/codex-rs/core-plugins/src/remote/share/tests.rs b/codex-rs/core-plugins/src/remote/share/tests.rs index ed4834123747..2ae7490d5163 100644 --- a/codex-rs/core-plugins/src/remote/share/tests.rs +++ b/codex-rs/core-plugins/src/remote/share/tests.rs @@ -25,6 +25,7 @@ use wiremock::matchers::query_param_is_missing; fn test_config(server: &MockServer) -> RemotePluginServiceConfig { RemotePluginServiceConfig { chatgpt_base_url: format!("{}/backend-api", server.uri()), + plugin_service_preview: false, } } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 31dcc667d1bf..ba31d4226784 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -581,6 +581,9 @@ "plugin_hooks": { "type": "boolean" }, + "plugin_service_preview": { + "type": "boolean" + }, "plugin_sharing": { "type": "boolean" }, @@ -4892,6 +4895,9 @@ "plugin_hooks": { "type": "boolean" }, + "plugin_service_preview": { + "type": "boolean" + }, "plugin_sharing": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 67cb0932de69..7f07050d7d5f 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -5835,6 +5835,54 @@ async fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<( Ok(()) } +#[tokio::test] +async fn plugin_service_preview_cli_override_reaches_plugin_service_consumers() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let default_config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) + .build() + .await?; + let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); + + assert!( + !default_config + .features + .enabled(Feature::PluginServicePreview) + ); + assert!(!default_config.plugins_config_input().plugin_service_preview); + assert!( + !default_config + .to_mcp_config(&plugins_manager) + .await + .plugin_service_preview + ); + + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) + .cli_overrides(vec![( + "features.plugin_service_preview".to_string(), + TomlValue::Boolean(true), + )]) + .build() + .await?; + + assert!(config.features.enabled(Feature::PluginServicePreview)); + assert!(config.plugins_config_input().plugin_service_preview); + assert!( + config + .to_mcp_config(&plugins_manager) + .await + .plugin_service_preview + ); + + Ok(()) +} + #[tokio::test] async fn to_mcp_config_flows_mcp_tool_prefix_from_feature() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 953d0af1a187..e67f58724d82 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1478,6 +1478,7 @@ impl Config { self.features.enabled(Feature::RemotePlugin), self.chatgpt_base_url.clone(), ) + .plugin_service_preview(self.features.enabled(Feature::PluginServicePreview)) } /// Applies managed MCP requirements to servers supplied by one plugin. @@ -1565,6 +1566,7 @@ impl Config { codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(), use_legacy_landlock: self.features.use_legacy_landlock(), apps_enabled: self.features.enabled(Feature::Apps), + plugin_service_preview: self.features.enabled(Feature::PluginServicePreview), prefix_mcp_tool_names: self.prefix_mcp_tool_names(), client_elicitation_capability: if self.features.enabled(Feature::AuthElicitation) { ElicitationCapability { diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index aa442f23d99f..13cff4a18585 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -41,7 +41,6 @@ use codex_mcp::codex_apps_tools_cache_key; use codex_mcp::compute_auth_statuses; use codex_mcp::effective_mcp_servers; use codex_mcp::tool_plugin_provenance; -use codex_utils_plugins::plugin_service_routing::plugin_service_preview_enabled; const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30); @@ -273,7 +272,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager( // one exists, but do not reintroduce the old hidden-local fallback. McpRuntimeContext::new(environment_manager, config.cwd.to_path_buf()), config.codex_home.to_path_buf(), - codex_apps_tools_cache_key(auth.as_ref()), + codex_apps_tools_cache_key(auth.as_ref(), mcp_config.plugin_service_preview), mcp_config.prefix_mcp_tool_names, mcp_config.client_elicitation_capability, /*supports_openai_form_elicitation*/ false, @@ -462,7 +461,7 @@ async fn cached_directory_connectors_for_tool_suggest_with_auth( auth.get_chatgpt_user_id(), is_workspace_account, ) - .plugin_service_preview(plugin_service_preview_enabled()), + .plugin_service_preview(config.features.enabled(Feature::PluginServicePreview)), ); codex_connectors::cached_directory_connectors(&cache_context).unwrap_or_default() diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index 90e6b01e758a..36e4484ec7ff 100644 --- a/codex-rs/core/src/mcp.rs +++ b/codex-rs/core/src/mcp.rs @@ -140,6 +140,7 @@ impl McpManager { codex_apps_mcp_server_config( &mcp_config.chatgpt_base_url, mcp_config.apps_mcp_product_sku.as_deref(), + mcp_config.plugin_service_preview, ), )); } else { diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 2a951bfe55c5..74f2f7960e33 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1353,7 +1353,13 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: }, ), turn_context.config.codex_home.to_path_buf(), - codex_mcp::codex_apps_tools_cache_key(auth.as_ref()), + codex_mcp::codex_apps_tools_cache_key( + auth.as_ref(), + turn_context + .config + .features + .enabled(codex_features::Feature::PluginServicePreview), + ), turn_context.config.prefix_mcp_tool_names(), rmcp::model::ElicitationCapability::default(), /*supports_openai_form_elicitation*/ false, diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index 2d92e70964a9..06f6862fb731 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -224,6 +224,7 @@ remote_plugin = true codex_home.path(), &RemotePluginServiceConfig { chatgpt_base_url: config.chatgpt_base_url.clone(), + plugin_service_preview: false, }, Some(&auth), ) diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 42c274fdb4a2..f2faaaf23f43 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -356,7 +356,7 @@ impl Session { turn_context.permission_profile(), mcp_runtime_context, config.codex_home.to_path_buf(), - codex_apps_tools_cache_key(auth.as_ref()), + codex_apps_tools_cache_key(auth.as_ref(), mcp_config.plugin_service_preview), mcp_config.prefix_mcp_tool_names, mcp_config.client_elicitation_capability, self.services diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 69f1959cce2e..e8908d6b8d87 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1179,7 +1179,12 @@ impl Session { session_configuration.permission_profile(), mcp_runtime_context, config.codex_home.to_path_buf(), - codex_apps_tools_cache_key(auth), + codex_apps_tools_cache_key( + auth, + config + .features + .enabled(codex_features::Feature::PluginServicePreview), + ), config.prefix_mcp_tool_names(), client_elicitation_capability, sess.services diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index afb103b883b2..69966025b13f 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -397,6 +397,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, + /*plugin_service_preview*/ false, ); let CapabilityRootLocation::Environment { environment_id, .. } = &selected_root.location; diff --git a/codex-rs/ext/mcp/src/lib.rs b/codex-rs/ext/mcp/src/lib.rs index e32316ac36fb..a64ded507f3a 100644 --- a/codex-rs/ext/mcp/src/lib.rs +++ b/codex-rs/ext/mcp/src/lib.rs @@ -32,6 +32,9 @@ impl McpServerContributor for HostedPluginRuntimeExtension { config: Box::new(hosted_plugin_runtime_mcp_server_config( &config.chatgpt_base_url, config.apps_mcp_product_sku.as_deref(), + config + .features + .enabled(codex_features::Feature::PluginServicePreview), )), }] }) diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 86fdd2e29ef7..904c7e3dced5 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -189,6 +189,11 @@ pub enum Feature { ComputerUse, /// Temporary internal-only flag for PS-backed remote plugin catalog development. RemotePlugin, + /// Route eligible plugin-service requests to the preview deployment. + /// + /// This is a routing selector, not an authorization boundary. The gateway must only honor the + /// corresponding cookie for internal traffic. + PluginServicePreview, /// Enable remote plugin sharing flows. PluginSharing, /// Removed compatibility flag retained as a no-op. @@ -1143,6 +1148,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::PluginServicePreview, + key: "plugin_service_preview", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::PluginSharing, key: "plugin_sharing", diff --git a/codex-rs/utils/plugins/src/plugin_service_routing.rs b/codex-rs/utils/plugins/src/plugin_service_routing.rs index fbb3e78b4973..92dad6b66f4d 100644 --- a/codex-rs/utils/plugins/src/plugin_service_routing.rs +++ b/codex-rs/utils/plugins/src/plugin_service_routing.rs @@ -1,23 +1,9 @@ -use std::ffi::OsStr; - -/// Process signal set by an eligible host to opt plugin-service requests into preview routing. -pub const CODEX_PLUGIN_SERVICE_PREVIEW_ENV_VAR: &str = "CODEX_PLUGIN_SERVICE_PREVIEW"; - -const PLUGIN_SERVICE_PREVIEW_ENABLED_VALUE: &str = "1"; const PLUGIN_SERVICE_PREVIEW_COOKIE_NAME: &[u8] = b"oai-chat-plugin-service-preview"; /// Routing cookie added to eligible plugin-service requests. -pub const PLUGIN_SERVICE_PREVIEW_COOKIE: &str = "oai-chat-plugin-service-preview=true"; - -/// Returns whether the host opted this process into plugin-service preview routing. /// -/// The host owns employee eligibility. This signal is defense-in-depth routing, not an -/// authorization boundary; authentication and authorization remain the responsibility of the -/// existing request path and plugin-service. -pub fn plugin_service_preview_enabled() -> bool { - plugin_service_preview_enabled_from_value( - std::env::var_os(CODEX_PLUGIN_SERVICE_PREVIEW_ENV_VAR).as_deref(), - ) -} +/// This cookie is intentionally public and untrusted. It selects a deployment after normal +/// authentication; the gateway must independently restrict preview routing to internal traffic. +pub const PLUGIN_SERVICE_PREVIEW_COOKIE: &str = "oai-chat-plugin-service-preview=true"; /// Rewrites plugin-service cookies so callers cannot override the process routing signal. /// @@ -49,10 +35,6 @@ pub fn plugin_service_routing_cookie( (!cookies.is_empty()).then(|| cookies.join(&b"; "[..])) } -fn plugin_service_preview_enabled_from_value(value: Option<&OsStr>) -> bool { - value == Some(OsStr::new(PLUGIN_SERVICE_PREVIEW_ENABLED_VALUE)) -} - fn trim_cookie_whitespace(mut value: &[u8]) -> &[u8] { while value .first() diff --git a/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs b/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs index 3dfab4fe17f8..ea73636e4098 100644 --- a/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs +++ b/codex-rs/utils/plugins/src/plugin_service_routing_tests.rs @@ -1,25 +1,6 @@ use super::*; use pretty_assertions::assert_eq; -#[test] -fn preview_signal_requires_exact_enabled_value() { - for value in [ - None, - Some(""), - Some("0"), - Some("true"), - Some(" 1"), - Some("1 "), - ] { - assert!(!plugin_service_preview_enabled_from_value( - value.map(OsStr::new) - )); - } - assert!(plugin_service_preview_enabled_from_value(Some(OsStr::new( - "1" - )))); -} - #[test] fn routing_cookie_is_disabled_by_default_and_cannot_be_enabled_by_caller() { assert_eq!( From 04d61f49f14c4fa775173653d6f141d6d34202a9 Mon Sep 17 00:00:00 2001 From: Steven Lee Date: Tue, 23 Jun 2026 16:56:57 +0000 Subject: [PATCH 4/5] codex: fix CI failure on PR #28582 --- codex-rs/codex-mcp/src/connection_manager_tests.rs | 1 + codex-rs/core/src/mcp_tool_call_tests.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index 16c5b86d324a..ae109a186e64 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -1222,6 +1222,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, + /*plugin_service_preview*/ false, )); manager.server_metadata.insert( CODEX_APPS_MCP_SERVER_NAME.to_string(), diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index 2c04c0e441bf..b4b59ebcb90c 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1343,6 +1343,7 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context: codex_mcp::EffectiveMcpServer::configured(codex_mcp::codex_apps_mcp_server_config( "https://chatgpt.com", /*apps_mcp_product_sku*/ None, + /*plugin_service_preview*/ false, )), )]); let manager = codex_mcp::McpConnectionManager::new( From d8753c9c05db906bc31c824aca1178e54795705c Mon Sep 17 00:00:00 2001 From: Steven Lee Date: Fri, 26 Jun 2026 20:31:23 +0000 Subject: [PATCH 5/5] Simplify plugin service preview tests --- .../codex-mcp/src/codex_apps_cache_tests.rs | 18 +---- codex-rs/codex-mcp/src/mcp/mod_tests.rs | 56 ++++++--------- codex-rs/core-plugins/src/remote.rs | 8 +-- codex-rs/core-plugins/src/remote_tests.rs | 69 ++----------------- codex-rs/core/src/config/config_tests.rs | 20 ------ 5 files changed, 31 insertions(+), 140 deletions(-) diff --git a/codex-rs/codex-mcp/src/codex_apps_cache_tests.rs b/codex-rs/codex-mcp/src/codex_apps_cache_tests.rs index 1646a3998920..4edbc8737bb1 100644 --- a/codex-rs/codex-mcp/src/codex_apps_cache_tests.rs +++ b/codex-rs/codex-mcp/src/codex_apps_cache_tests.rs @@ -141,28 +141,14 @@ fn codex_apps_tools_cache_is_scoped_by_plugin_service_preview() { let cache = CodexAppsToolsCache::default(); let regular = cache.context( codex_home.path().to_path_buf(), - CodexAppsToolsCacheKey { - account_id: Some("account-one".to_string()), - chatgpt_user_id: Some("user-one".to_string()), - is_workspace_account: false, - plugin_service_preview: false, - }, + codex_apps_tools_cache_key(/*auth*/ None, /*plugin_service_preview*/ false), ); let preview = cache.context( codex_home.path().to_path_buf(), - CodexAppsToolsCacheKey { - account_id: Some("account-one".to_string()), - chatgpt_user_id: Some("user-one".to_string()), - is_workspace_account: false, - plugin_service_preview: true, - }, + codex_apps_tools_cache_key(/*auth*/ None, /*plugin_service_preview*/ true), ); assert_ne!(regular.tools_cache_path(), preview.tools_cache_path()); - assert_ne!( - regular.server_info_cache_path(), - preview.server_info_cache_path() - ); } #[test] diff --git a/codex-rs/codex-mcp/src/mcp/mod_tests.rs b/codex-rs/codex-mcp/src/mcp/mod_tests.rs index 10a9d680a875..c6bbc0eb0891 100644 --- a/codex-rs/codex-mcp/src/mcp/mod_tests.rs +++ b/codex-rs/codex-mcp/src/mcp/mod_tests.rs @@ -304,17 +304,15 @@ fn codex_apps_server_config_forwards_configured_product_sku_header() { #[test] fn built_in_plugin_service_mcp_config_only_adds_preview_cookie_when_enabled() { - let disabled = mcp_server_config_for_url( - "https://chatgpt.com/backend-api/ps/mcp".to_string(), + let disabled = hosted_plugin_runtime_mcp_server_config( + "https://chatgpt.com", /*apps_mcp_product_sku*/ None, - McpServerAuth::ChatGpt, - /*preview_enabled*/ false, + /*plugin_service_preview*/ false, ); - let enabled = mcp_server_config_for_url( - "https://chatgpt.com/backend-api/ps/mcp".to_string(), + let enabled = hosted_plugin_runtime_mcp_server_config( + "https://chatgpt.com", /*apps_mcp_product_sku*/ None, - McpServerAuth::ChatGpt, - /*preview_enabled*/ true, + /*plugin_service_preview*/ true, ); fn headers(config: &McpServerConfig) -> Option<&HashMap> { @@ -331,31 +329,6 @@ fn built_in_plugin_service_mcp_config_only_adds_preview_cookie_when_enabled() { "oai-chat-plugin-service-preview=true".to_string(), )])), ); - - let unrelated_server = McpServerConfig { - auth: Default::default(), - transport: McpServerTransportConfig::StreamableHttp { - url: "https://third-party.example/mcp".to_string(), - bearer_token_env_var: None, - http_headers: None, - env_http_headers: None, - }, - environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), - enabled: true, - required: false, - supports_parallel_tool_calls: false, - disabled_reason: None, - startup_timeout_sec: None, - tool_timeout_sec: None, - default_tools_approval_mode: None, - enabled_tools: None, - disabled_tools: None, - scopes: None, - oauth: None, - oauth_resource: None, - tools: HashMap::new(), - }; - assert_eq!(headers(&unrelated_server), None); } #[tokio::test] @@ -363,6 +336,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() { let codex_home = tempfile::tempdir().expect("tempdir"); let mut config = test_mcp_config(codex_home.path().to_path_buf()); config.apps_enabled = true; + config.plugin_service_preview = true; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let mut catalog = ResolvedMcpCatalog::builder(); @@ -449,8 +423,11 @@ async fn effective_mcp_servers_preserve_runtime_servers() { .expect("codex apps should use configured transport"); match &sample.transport { - McpServerTransportConfig::StreamableHttp { url, .. } => { + McpServerTransportConfig::StreamableHttp { + url, http_headers, .. + } => { assert_eq!(url, "https://user.example/mcp"); + assert_eq!(http_headers, &None); } other => panic!("expected streamable http transport, got {other:?}"), } @@ -461,8 +438,17 @@ async fn effective_mcp_servers_preserve_runtime_servers() { other => panic!("expected streamable http transport, got {other:?}"), } match &codex_apps.transport { - McpServerTransportConfig::StreamableHttp { url, .. } => { + McpServerTransportConfig::StreamableHttp { + url, http_headers, .. + } => { assert_eq!(url, "https://chatgpt.com/backend-api/wham/apps"); + assert_eq!( + http_headers, + &Some(HashMap::from([( + "Cookie".to_string(), + "oai-chat-plugin-service-preview=true".to_string(), + )])) + ); } other => panic!("expected streamable http transport, got {other:?}"), } diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index fa9829875eee..13a4ddedc228 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -1935,13 +1935,7 @@ pub(super) async fn send_plugin_service_request( config: &RemotePluginServiceConfig, request: RequestBuilder, ) -> Result { - send_plugin_service_request_with_preview(request, config.plugin_service_preview).await -} - -async fn send_plugin_service_request_with_preview( - request: RequestBuilder, - preview_enabled: bool, -) -> Result { + let preview_enabled = config.plugin_service_preview; let (client, request) = request.build_split(); let mut request = request?; let cloudflare_cookie = (preview_enabled && !request.headers().contains_key(COOKIE)) diff --git a/codex-rs/core-plugins/src/remote_tests.rs b/codex-rs/core-plugins/src/remote_tests.rs index 60a44bf978af..7a0b3417ba19 100644 --- a/codex-rs/core-plugins/src/remote_tests.rs +++ b/codex-rs/core-plugins/src/remote_tests.rs @@ -1,9 +1,5 @@ use super::*; use pretty_assertions::assert_eq; -use wiremock::Mock; -use wiremock::MockServer; -use wiremock::ResponseTemplate; -use wiremock::matchers::method; #[test] fn build_remote_marketplace_preserves_directory_order_and_appends_installed_only_plugins() { @@ -301,7 +297,10 @@ fn recommended_plugins_ignore_invalid_remote_plugin_ids() { #[test] fn plugin_service_routing_preserves_cloudflare_cookie_jar_values() { - let mut headers = reqwest::header::HeaderMap::new(); + let mut headers = reqwest::header::HeaderMap::from_iter([( + COOKIE, + HeaderValue::from_static("session=abc; oai-chat-plugin-service-preview=false"), + )]); let cloudflare_cookie = HeaderValue::from_static("cf_clearance=clearance; _cfuvid=visitor"); apply_plugin_service_routing_cookie( @@ -312,63 +311,9 @@ fn plugin_service_routing_preserves_cloudflare_cookie_jar_values() { assert_eq!( headers.get(COOKIE).and_then(|value| value.to_str().ok()), - Some("cf_clearance=clearance; _cfuvid=visitor; oai-chat-plugin-service-preview=true"), - ); -} - -#[tokio::test] -async fn plugin_service_request_does_not_add_preview_cookie_when_disabled() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - let response = send_plugin_service_request_with_preview( - reqwest::Client::new().get(server.uri()), - /*preview_enabled*/ false, - ) - .await - .expect("request should succeed"); - assert!(response.status().is_success()); - - let requests = server - .received_requests() - .await - .expect("request recording should be available"); - assert_eq!(requests.len(), 1); - assert_eq!(requests[0].headers.get("cookie"), None); -} - -#[tokio::test] -async fn plugin_service_request_sanitizes_and_preserves_caller_cookies() { - let server = MockServer::start().await; - Mock::given(method("GET")) - .respond_with(ResponseTemplate::new(200)) - .mount(&server) - .await; - - let response = send_plugin_service_request_with_preview( - reqwest::Client::new().get(server.uri()).header( - COOKIE, - "session=abc; oai-chat-plugin-service-preview=false; theme=dark", + Some( + "session=abc; cf_clearance=clearance; _cfuvid=visitor; \ + oai-chat-plugin-service-preview=true" ), - /*preview_enabled*/ true, - ) - .await - .expect("request should succeed"); - assert!(response.status().is_success()); - - let requests = server - .received_requests() - .await - .expect("request recording should be available"); - assert_eq!(requests.len(), 1); - assert_eq!( - requests[0] - .headers - .get("cookie") - .and_then(|value| value.to_str().ok()), - Some("session=abc; theme=dark; oai-chat-plugin-service-preview=true"), ); } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 0b79114e7bb3..a7519bd2ca92 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -6007,27 +6007,7 @@ async fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<( async fn plugin_service_preview_cli_override_reaches_plugin_service_consumers() -> std::io::Result<()> { let codex_home = TempDir::new()?; - let default_config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) - .build() - .await?; let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); - - assert!( - !default_config - .features - .enabled(Feature::PluginServicePreview) - ); - assert!(!default_config.plugins_config_input().plugin_service_preview); - assert!( - !default_config - .to_mcp_config(&plugins_manager) - .await - .plugin_service_preview - ); - let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf()))