Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 18 additions & 33 deletions codex-rs/app-server/src/request_processors/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,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<AbsolutePathBuf, String>,
Expand Down Expand Up @@ -569,9 +576,7 @@ impl PluginRequestProcessor {
!explicit_marketplace_kinds && config.features.enabled(Feature::RemotePlugin);
let use_remote_global_catalog =
include_global_remote && auth_mode.is_some_and(DomainAuthMode::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(),
Expand Down Expand Up @@ -1018,9 +1023,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(),
Expand Down Expand Up @@ -1124,9 +1127,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,
Expand Down Expand Up @@ -1182,9 +1183,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(),
Expand Down Expand Up @@ -1235,9 +1234,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),
Expand Down Expand Up @@ -1278,9 +1275,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(),
Expand Down Expand Up @@ -1308,9 +1303,7 @@ impl PluginRequestProcessor {
_params: PluginShareListParams,
) -> Result<PluginShareListResponse, JSONRPCErrorError> {
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(),
Expand Down Expand Up @@ -1347,9 +1340,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(),
Expand Down Expand Up @@ -1380,9 +1371,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(),
Expand Down Expand Up @@ -1517,9 +1506,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,
Expand Down Expand Up @@ -1995,9 +1982,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_target = codex_core_plugins::remote::resolve_remote_plugin_uninstall_target(
&remote_plugin_service_config,
auth.as_ref(),
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/chatgpt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
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 }
codex-plugin = { workspace = true }
codex-utils-cli = { workspace = true }
codex-utils-plugins = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["full"] }

Expand Down
53 changes: 52 additions & 1 deletion codex-rs/chatgpt/src/chatgpt_client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
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_routing_cookie;

use anyhow::Context;
use serde::de::DeserializeOwned;
Expand All @@ -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<T: DeserializeOwned>(
config: &Config,
path: String,
) -> anyhow::Result<T> {
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<T: DeserializeOwned>(
config: &Config,
path: String,
timeout: Option<Duration>,
) -> anyhow::Result<T> {
chatgpt_get_request_with_timeout_inner(config, path, timeout, ChatgptRequestRouting::Default)
.await
}

pub(crate) async fn chatgpt_get_plugin_service_request_with_timeout<T: DeserializeOwned>(
config: &Config,
path: String,
timeout: Option<Duration>,
) -> anyhow::Result<T> {
chatgpt_get_request_with_timeout_inner(
config,
path,
timeout,
ChatgptRequestRouting::PluginService,
)
.await
}

async fn chatgpt_get_request_with_timeout_inner<T: DeserializeOwned>(
config: &Config,
path: String,
timeout: Option<Duration>,
routing: ChatgptRequestRouting,
) -> anyhow::Result<T> {
let chatgpt_base_url = &config.chatgpt_base_url;
let auth_manager =
Expand Down Expand Up @@ -52,6 +91,18 @@ pub(crate) async fn chatgpt_get_request_with_timeout<T: DeserializeOwned>(
.header(OAI_PRODUCT_SKU_HEADER, CODEX_PRODUCT_SKU)
.header("Content-Type", "application/json");

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::<Vec<_>>();
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);
}
Expand Down
8 changes: 5 additions & 3 deletions codex-rs/chatgpt/src/connectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_connectors::AppInfo;
use codex_connectors::ConnectorDirectoryCacheContext;
Expand All @@ -18,6 +18,7 @@ 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;
Expand Down Expand Up @@ -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::<DirectoryListResponse>(
chatgpt_get_plugin_service_request_with_timeout::<DirectoryListResponse>(
config,
path,
Some(DIRECTORY_CONNECTORS_TIMEOUT),
Expand All @@ -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(config.features.enabled(Feature::PluginServicePreview)),
)
}

Expand Down
12 changes: 12 additions & 0 deletions codex-rs/codex-client/src/chatgpt_cloudflare_cookies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>> {
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" => {}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/codex-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions codex-rs/codex-mcp/src/codex_apps_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use crate::tools::ToolInfo;

const MCP_TOOLS_CACHE_PUBLISH_DURATION_METRIC: &str = "codex.mcp.tools.cache_publish.duration_ms";

/// The CodexAuth bits that identify a Codex Apps catalog.
/// The auth and routing inputs that identify a Codex Apps catalog.
///
/// Debug bearer-token overrides bypass the shared cache, so shared entries only
/// need the CodexAuth-backed identity.
Expand All @@ -38,14 +38,20 @@ pub struct CodexAppsToolsCacheKey {
pub(crate) account_id: Option<String>,
pub(crate) chatgpt_user_id: Option<String>,
pub(crate) is_workspace_account: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) plugin_service_preview: bool,
}

/// Builds the CodexAuth-backed Codex Apps cache key.
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,
}
}

Expand Down
Loading
Loading