Extend PAC routing across Codex network clients

This commit is contained in:
canvrno-oai
2026-06-15 11:09:13 -07:00
parent 8cd829a70a
commit a2e88abd08
26 changed files with 419 additions and 109 deletions
+11 -6
View File
@@ -58,6 +58,7 @@ use codex_core_skills::config_rules::SkillConfigRules;
use codex_core_skills::config_rules::skill_config_rules_from_stack;
use codex_hooks::plugin_hook_declarations;
use codex_login::AuthManager;
use codex_login::AuthRouteConfig;
use codex_login::CodexAuth;
use codex_plugin::AppConnectorId;
use codex_plugin::PluginCapabilitySummary;
@@ -89,6 +90,7 @@ pub struct PluginsConfigInput {
pub plugins_enabled: bool,
pub remote_plugin_enabled: bool,
pub chatgpt_base_url: String,
pub auth_route_config: Option<AuthRouteConfig>,
}
impl PluginsConfigInput {
@@ -97,12 +99,14 @@ impl PluginsConfigInput {
plugins_enabled: bool,
remote_plugin_enabled: bool,
chatgpt_base_url: String,
auth_route_config: Option<AuthRouteConfig>,
) -> Self {
Self {
config_layer_stack,
plugins_enabled,
remote_plugin_enabled,
chatgpt_base_url,
auth_route_config,
}
}
}
@@ -189,6 +193,7 @@ struct ConfiguredMarketplaceUpgradeState {
fn remote_plugin_service_config(config: &PluginsConfigInput) -> RemotePluginServiceConfig {
RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
auth_route_config: config.auth_route_config.clone(),
}
}
@@ -1333,7 +1338,7 @@ impl PluginsManager {
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
) {
if config.plugins_enabled {
self.start_curated_repo_sync();
self.start_curated_repo_sync(config.auth_route_config.clone());
let should_spawn_marketplace_auto_upgrade = {
let mut state = match self.configured_marketplace_upgrade_state.write() {
Ok(state) => state,
@@ -1630,7 +1635,7 @@ impl PluginsManager {
}
}
fn start_curated_repo_sync(self: &Arc<Self>) {
fn start_curated_repo_sync(self: &Arc<Self>, auth_route_config: Option<AuthRouteConfig>) {
if CURATED_REPO_SYNC_STARTED.swap(true, Ordering::SeqCst) {
return;
}
@@ -1638,8 +1643,8 @@ impl PluginsManager {
let codex_home = self.codex_home.clone();
if let Err(err) = std::thread::Builder::new()
.name("plugins-curated-repo-sync".to_string())
.spawn(
move || match sync_openai_plugins_repo(codex_home.as_path()) {
.spawn(move || {
match sync_openai_plugins_repo(codex_home.as_path(), auth_route_config.as_ref()) {
Ok(curated_plugin_version) => {
let configured_curated_plugin_ids =
configured_curated_plugin_ids_from_codex_home(codex_home.as_path());
@@ -1664,8 +1669,8 @@ impl PluginsManager {
CURATED_REPO_SYNC_STARTED.store(false, Ordering::SeqCst);
warn!("failed to sync curated plugins repo: {err}");
}
},
)
}
})
{
CURATED_REPO_SYNC_STARTED.store(false, Ordering::SeqCst);
warn!("failed to start curated plugins repo sync task: {err}");
@@ -1412,6 +1412,7 @@ async fn plugin_cache_ignores_unrelated_session_overrides() {
/*plugins_enabled*/ true,
/*remote_plugin_enabled*/ false,
"https://chatgpt.com".to_string(),
/*auth_route_config*/ None,
)
};
let manager = PluginsManager::new(codex_home.path().to_path_buf());
+41 -8
View File
@@ -6,8 +6,10 @@ use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginInterface;
use codex_app_server_protocol::SkillInterface;
use codex_login::AuthRouteConfig;
use codex_login::CodexAuth;
use codex_login::default_client::build_reqwest_client;
use codex_login::default_client::ClientRouteClass;
use codex_login::default_client::build_default_reqwest_client_for_route;
use codex_plugin::PluginId;
use codex_utils_absolute_path::AbsolutePathBuf;
use reqwest::RequestBuilder;
@@ -97,6 +99,30 @@ const REMOTE_INSTALLED_MARKETPLACE_DISPLAY_ORDER: [(&str, &str); 5] = [
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemotePluginServiceConfig {
pub chatgpt_base_url: String,
pub auth_route_config: Option<AuthRouteConfig>,
}
impl RemotePluginServiceConfig {
pub(crate) fn build_http_client(&self, url: &str) -> anyhow::Result<reqwest::Client> {
build_default_reqwest_client_for_route(
url,
ClientRouteClass::Api,
self.auth_route_config.as_ref(),
)
.map_err(Into::into)
}
}
fn build_remote_plugin_http_client(
config: &RemotePluginServiceConfig,
url: &str,
) -> Result<reqwest::Client, RemotePluginCatalogError> {
config
.build_http_client(url)
.map_err(|source| RemotePluginCatalogError::HttpClient {
url: url.to_string(),
source,
})
}
#[derive(Debug, Clone, PartialEq)]
@@ -253,6 +279,13 @@ pub enum RemotePluginCatalogError {
#[error("failed to read auth token for remote plugin catalog: {0}")]
AuthToken(#[source] std::io::Error),
#[error("failed to configure remote plugin catalog client for {url}: {source}")]
HttpClient {
url: String,
#[source]
source: anyhow::Error,
},
#[error("failed to send remote plugin catalog request to {url}: {source}")]
Request {
url: String,
@@ -959,7 +992,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 client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.get(&url), auth)?;
let response: RemotePluginSkillDetailResponse = send_and_decode(request, &url).await?;
if response.plugin_id != plugin_id {
@@ -1086,7 +1119,7 @@ pub async fn install_remote_plugin(
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{plugin_id}/install");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(
client
.post(&url)
@@ -1129,7 +1162,7 @@ pub async fn uninstall_remote_plugin(
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{plugin_id}/uninstall");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.post(&url), auth)?;
let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?;
if response.id != plugin_id {
@@ -1535,7 +1568,7 @@ async fn get_remote_plugin_list_page(
) -> Result<RemotePluginListResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/list");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
request = request.query(&[("scope", scope.api_value())]);
request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]);
@@ -1555,7 +1588,7 @@ async fn get_remote_shared_workspace_plugins_page(
) -> Result<RemotePluginListResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/workspace/shared");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]);
if let Some(page_token) = page_token {
@@ -1573,7 +1606,7 @@ async fn get_remote_plugin_installed_page(
) -> Result<RemotePluginInstalledResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/installed");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
request = request.query(&[("scope", scope.api_value())]);
if include_download_urls {
@@ -1593,7 +1626,7 @@ async fn fetch_plugin_detail(
) -> Result<RemotePluginDirectoryItem, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{plugin_id}");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
if include_download_urls {
request = request.query(&[("includeDownloadUrls", true)]);
@@ -227,6 +227,7 @@ pub async fn sync_remote_installed_plugin_bundles_once(
match crate::remote_bundle::download_and_install_remote_plugin_bundle(
codex_home.clone(),
bundle,
config.auth_route_config.as_ref(),
)
.await
{
+13 -8
View File
@@ -2,7 +2,6 @@ use super::*;
use crate::plugin_bundle_archive::PluginBundlePackError;
use crate::plugin_bundle_archive::pack_plugin_bundle_tar_gz;
use codex_login::CodexAuth;
use codex_login::default_client::build_reqwest_client;
use codex_utils_absolute_path::AbsolutePathBuf;
use reqwest::RequestBuilder;
use reqwest::StatusCode;
@@ -163,7 +162,7 @@ pub async fn save_remote_plugin_share(
let etag = upload
.etag
.ok_or(RemotePluginCatalogError::MissingUploadEtag)?;
put_workspace_plugin_upload(&upload.upload_url, archive_bytes).await?;
put_workspace_plugin_upload(config, &upload.upload_url, archive_bytes).await?;
let share_targets = access_policy.share_targets;
let share_targets =
ensure_unlisted_workspace_target(auth, access_policy.discoverability, share_targets)?;
@@ -279,7 +278,7 @@ pub async fn delete_remote_plugin_share(
let auth = ensure_chatgpt_auth(auth)?;
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/public/plugins/workspace/{remote_plugin_id}");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.delete(&url), auth)?;
send_and_expect_status(request, &url, &[StatusCode::NO_CONTENT]).await?;
if let Err(err) = local_paths::remove_plugin_share_local_path(codex_home, remote_plugin_id) {
@@ -312,7 +311,7 @@ pub async fn update_remote_plugin_share_targets(
.unwrap_or_default();
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/shares");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.put(&url), auth)?.json(
&RemotePluginShareUpdateTargetsRequest {
discoverability,
@@ -378,7 +377,7 @@ async fn get_created_workspace_plugins_page(
) -> Result<RemotePluginListResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/workspace/created");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let mut request = authenticated_request(client.get(&url), auth)?;
request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]);
if let Some(page_token) = page_token {
@@ -396,7 +395,7 @@ async fn create_workspace_plugin_upload(
) -> Result<RemoteWorkspacePluginUploadUrlResponse, RemotePluginCatalogError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/public/plugins/workspace/upload-url");
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.post(&url), auth)?.json(
&RemoteWorkspacePluginUploadUrlRequest {
filename,
@@ -409,10 +408,16 @@ async fn create_workspace_plugin_upload(
}
async fn put_workspace_plugin_upload(
config: &RemotePluginServiceConfig,
upload_url: &str,
archive_bytes: Vec<u8>,
) -> Result<(), RemotePluginCatalogError> {
let client = build_reqwest_client();
let client = config.build_http_client(upload_url).map_err(|source| {
RemotePluginCatalogError::HttpClient {
url: "workspace plugin upload URL".to_string(),
source,
}
})?;
let request = client
.put(upload_url)
.timeout(REMOTE_PLUGIN_CATALOG_TIMEOUT)
@@ -450,7 +455,7 @@ async fn finalize_workspace_plugin_upload(
} else {
format!("{base_url}/public/plugins/workspace")
};
let client = build_reqwest_client();
let client = build_remote_plugin_http_client(config, &url)?;
let request = authenticated_request(client.post(&url), auth)?.json(&body);
send_and_decode(request, &url).await
}
@@ -95,6 +95,7 @@ pub async fn checkout_remote_plugin_share(
crate::remote_bundle::download_and_extract_remote_plugin_bundle_to_path(
bundle,
local_plugin_path.clone(),
config.auth_route_config.as_ref(),
)
.await
.map_err(|err| {
@@ -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()),
auth_route_config: None,
}
}
+24 -2
View File
@@ -5,7 +5,9 @@ use crate::store::PluginInstallResult;
use crate::store::PluginStore;
use crate::store::PluginStoreError;
use crate::store::validate_plugin_version_segment;
use codex_login::default_client::build_reqwest_client;
use codex_login::AuthRouteConfig;
use codex_login::default_client::ClientRouteClass;
use codex_login::default_client::build_default_reqwest_client_for_route;
use codex_plugin::PluginId;
use codex_plugin::PluginIdError;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -88,6 +90,13 @@ pub enum RemotePluginBundleInstallError {
source: reqwest::Error,
},
#[error("failed to configure remote plugin bundle client for {url}: {source}")]
DownloadClient {
url: String,
#[source]
source: anyhow::Error,
},
#[error("remote plugin bundle download from {url} failed with status {status}: {body}")]
DownloadStatus {
url: String,
@@ -226,10 +235,12 @@ fn is_loopback_url(url: &Url) -> bool {
pub async fn download_and_install_remote_plugin_bundle(
codex_home: PathBuf,
bundle: ValidatedRemotePluginBundle,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<PluginInstallResult, RemotePluginBundleInstallError> {
let bundle_bytes = download_remote_plugin_bundle_with_limit(
&bundle.bundle_download_url,
/*max_bytes*/ REMOTE_PLUGIN_BUNDLE_MAX_DOWNLOAD_BYTES,
auth_route_config,
)
.await?;
tokio::task::spawn_blocking(move || {
@@ -246,10 +257,12 @@ pub async fn download_and_install_remote_plugin_bundle(
pub(crate) async fn download_and_extract_remote_plugin_bundle_to_path(
bundle: ValidatedRemotePluginBundle,
destination: AbsolutePathBuf,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<AbsolutePathBuf, RemotePluginBundleInstallError> {
let bundle_bytes = download_remote_plugin_bundle_with_limit(
&bundle.bundle_download_url,
/*max_bytes*/ REMOTE_PLUGIN_BUNDLE_MAX_DOWNLOAD_BYTES,
auth_route_config,
)
.await?;
tokio::task::spawn_blocking(move || {
@@ -266,8 +279,17 @@ pub(crate) async fn download_and_extract_remote_plugin_bundle_to_path(
async fn download_remote_plugin_bundle_with_limit(
bundle_download_url: &str,
max_bytes: u64,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<Vec<u8>, RemotePluginBundleInstallError> {
let client = build_reqwest_client();
let client = build_default_reqwest_client_for_route(
bundle_download_url,
ClientRouteClass::Api,
auth_route_config,
)
.map_err(|source| RemotePluginBundleInstallError::DownloadClient {
url: bundle_download_url.to_string(),
source: source.into(),
})?;
let response = client
.get(bundle_download_url)
.timeout(REMOTE_PLUGIN_BUNDLE_DOWNLOAD_TIMEOUT)
+28 -3
View File
@@ -1,6 +1,5 @@
use crate::remote::RemotePluginServiceConfig;
use codex_login::CodexAuth;
use codex_login::default_client::build_reqwest_client;
use codex_protocol::protocol::Product;
use serde::Deserialize;
use std::time::Duration;
@@ -35,6 +34,13 @@ pub enum RemotePluginMutationError {
#[error("chatgpt base url cannot be used for plugin mutation")]
InvalidBaseUrlPath,
#[error("failed to configure remote plugin mutation client for {url}: {source}")]
HttpClient {
url: String,
#[source]
source: anyhow::Error,
},
#[error("failed to send remote plugin mutation request to {url}: {source}")]
Request {
url: String,
@@ -73,6 +79,13 @@ pub enum RemotePluginMutationError {
#[derive(Debug, thiserror::Error)]
pub enum RemotePluginFetchError {
#[error("failed to configure remote featured plugin client for {url}: {source}")]
HttpClient {
url: String,
#[source]
source: anyhow::Error,
},
#[error("failed to send remote featured plugin request to {url}: {source}")]
Request {
url: String,
@@ -102,7 +115,13 @@ pub async fn fetch_remote_featured_plugin_ids(
) -> Result<Vec<String>, RemotePluginFetchError> {
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/plugins/featured");
let client = build_reqwest_client();
let client =
config
.build_http_client(&url)
.map_err(|source| RemotePluginFetchError::HttpClient {
url: url.clone(),
source,
})?;
let mut request = client
.get(&url)
.query(&[(
@@ -173,7 +192,13 @@ async fn post_remote_plugin_mutation(
) -> Result<RemotePluginMutationResponse, RemotePluginMutationError> {
let auth = ensure_codex_backend_auth(auth)?;
let url = remote_plugin_mutation_url(config, plugin_id, action)?;
let client = build_reqwest_client();
let client =
config
.build_http_client(&url)
.map_err(|source| RemotePluginMutationError::HttpClient {
url: url.clone(),
source,
})?;
let request = client
.post(url.clone())
.timeout(REMOTE_PLUGIN_MUTATION_TIMEOUT)
+59 -18
View File
@@ -6,6 +6,9 @@ use std::process::Output;
use std::process::Stdio;
use std::time::Duration;
use codex_login::AuthRouteConfig;
use codex_login::default_client::ClientRouteClass;
use codex_login::default_client::build_default_reqwest_client_for_route;
use codex_otel::CURATED_PLUGINS_STARTUP_SYNC_FINAL_METRIC;
use codex_otel::CURATED_PLUGINS_STARTUP_SYNC_METRIC;
use reqwest::Client;
@@ -14,8 +17,6 @@ use tempfile::TempDir;
use tracing::warn;
use zip::ZipArchive;
use codex_login::default_client::build_reqwest_client;
const GITHUB_API_BASE_URL: &str = "https://api.github.com";
const GITHUB_API_ACCEPT_HEADER: &str = "application/vnd.github+json";
const GITHUB_API_VERSION_HEADER: &str = "2022-11-28";
@@ -67,12 +68,16 @@ fn curated_plugins_sha_path(codex_home: &Path) -> PathBuf {
codex_home.join(CURATED_PLUGINS_SHA_FILE)
}
pub fn sync_openai_plugins_repo(codex_home: &Path) -> Result<String, String> {
pub fn sync_openai_plugins_repo(
codex_home: &Path,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
sync_openai_plugins_repo_with_transport_overrides(
codex_home,
"git",
GITHUB_API_BASE_URL,
CURATED_PLUGINS_BACKUP_ARCHIVE_API_URL,
auth_route_config,
)
}
@@ -81,23 +86,33 @@ fn sync_openai_plugins_repo_with_transport_overrides(
git_binary: &str,
api_base_url: &str,
backup_archive_api_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
let _file_guard = lock_curated_plugins_startup_sync(codex_home)?;
match sync_openai_plugins_repo_via_git(codex_home, git_binary) {
let route_aware_http =
auth_route_config.is_some_and(AuthRouteConfig::requires_route_aware_transport);
let git_result = if route_aware_http {
Err("route-aware plugin sync requires the HTTP transport".to_string())
} else {
sync_openai_plugins_repo_via_git(codex_home, git_binary)
};
match git_result {
Ok(remote_sha) => {
emit_curated_plugins_startup_sync_metric("git", "success");
emit_curated_plugins_startup_sync_final_metric("git", "success");
Ok(remote_sha)
}
Err(err) => {
emit_curated_plugins_startup_sync_metric("git", "failure");
warn!(
error = %err,
git_binary,
"git sync failed for curated plugin sync; falling back to GitHub HTTP"
);
match sync_openai_plugins_repo_via_http(codex_home, api_base_url) {
if !route_aware_http {
emit_curated_plugins_startup_sync_metric("git", "failure");
warn!(
error = %err,
git_binary,
"git sync failed for curated plugin sync; falling back to GitHub HTTP"
);
}
match sync_openai_plugins_repo_via_http(codex_home, api_base_url, auth_route_config) {
Ok(remote_sha) => {
emit_curated_plugins_startup_sync_metric("http", "success");
emit_curated_plugins_startup_sync_final_metric("http", "success");
@@ -125,6 +140,7 @@ fn sync_openai_plugins_repo_with_transport_overrides(
let result = sync_openai_plugins_repo_via_backup_archive(
codex_home,
backup_archive_api_url,
auth_route_config,
);
let status = if result.is_ok() { "success" } else { "failure" };
emit_curated_plugins_startup_sync_metric("export_archive", status);
@@ -288,6 +304,7 @@ fn run_git_in_repo(
fn sync_openai_plugins_repo_via_http(
codex_home: &Path,
api_base_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
let repo_path = curated_plugins_repo_path(codex_home);
let sha_path = codex_home.join(CURATED_PLUGINS_SHA_FILE);
@@ -295,7 +312,10 @@ fn sync_openai_plugins_repo_via_http(
.enable_all()
.build()
.map_err(|err| format!("failed to create curated plugins sync runtime: {err}"))?;
let remote_sha = runtime.block_on(fetch_curated_repo_remote_sha(api_base_url))?;
let remote_sha = runtime.block_on(fetch_curated_repo_remote_sha(
api_base_url,
auth_route_config,
))?;
let local_sha = read_sha_file(&sha_path);
if local_sha.as_deref() == Some(remote_sha.as_str()) && repo_path.is_dir() {
@@ -303,7 +323,11 @@ fn sync_openai_plugins_repo_via_http(
}
let staged_repo_dir = prepare_curated_repo_parent_and_temp_dir(&repo_path)?;
let zipball_bytes = runtime.block_on(fetch_curated_repo_zipball(api_base_url, &remote_sha))?;
let zipball_bytes = runtime.block_on(fetch_curated_repo_zipball(
api_base_url,
&remote_sha,
auth_route_config,
))?;
extract_zipball_to_dir(&zipball_bytes, staged_repo_dir.path())?;
ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
activate_curated_repo(&repo_path, staged_repo_dir)?;
@@ -314,6 +338,7 @@ fn sync_openai_plugins_repo_via_http(
fn sync_openai_plugins_repo_via_backup_archive(
codex_home: &Path,
backup_archive_api_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
let repo_path = curated_plugins_repo_path(codex_home);
let sha_path = curated_plugins_sha_path(codex_home);
@@ -324,6 +349,7 @@ fn sync_openai_plugins_repo_via_backup_archive(
let staged_repo_dir = prepare_curated_repo_parent_and_temp_dir(&repo_path)?;
let zipball_bytes = runtime.block_on(fetch_curated_repo_backup_archive_zip(
backup_archive_api_url,
auth_route_config,
))?;
extract_zipball_to_dir(&zipball_bytes, staged_repo_dir.path())?;
ensure_marketplace_manifest_exists(staged_repo_dir.path())?;
@@ -700,10 +726,13 @@ fn ensure_git_success(output: &Output, context: &str) -> Result<(), String> {
}
}
async fn fetch_curated_repo_remote_sha(api_base_url: &str) -> Result<String, String> {
async fn fetch_curated_repo_remote_sha(
api_base_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<String, String> {
let api_base_url = api_base_url.trim_end_matches('/');
let repo_url = format!("{api_base_url}/repos/{OPENAI_PLUGINS_OWNER}/{OPENAI_PLUGINS_REPO}");
let client = build_reqwest_client();
let client = build_startup_sync_client(&repo_url, auth_route_config)?;
let repo_body = fetch_github_text(&client, &repo_url, "get curated plugins repository").await?;
let repo_summary: GitHubRepositorySummary =
serde_json::from_str(&repo_body).map_err(|err| {
@@ -733,18 +762,20 @@ async fn fetch_curated_repo_remote_sha(api_base_url: &str) -> Result<String, Str
async fn fetch_curated_repo_zipball(
api_base_url: &str,
remote_sha: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<Vec<u8>, String> {
let api_base_url = api_base_url.trim_end_matches('/');
let repo_url = format!("{api_base_url}/repos/{OPENAI_PLUGINS_OWNER}/{OPENAI_PLUGINS_REPO}");
let zipball_url = format!("{repo_url}/zipball/{remote_sha}");
let client = build_reqwest_client();
let client = build_startup_sync_client(&zipball_url, auth_route_config)?;
fetch_github_bytes(&client, &zipball_url, "download curated plugins archive").await
}
async fn fetch_curated_repo_backup_archive_zip(
backup_archive_api_url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<Vec<u8>, String> {
let client = build_reqwest_client();
let client = build_startup_sync_client(backup_archive_api_url, auth_route_config)?;
let export_body = fetch_public_text(
&client,
backup_archive_api_url,
@@ -763,14 +794,24 @@ async fn fetch_curated_repo_backup_archive_zip(
));
}
let download_client =
build_startup_sync_client(&export_response.download_url, auth_route_config)?;
fetch_public_bytes(
&client,
&download_client,
&export_response.download_url,
"download curated plugins export archive",
)
.await
}
fn build_startup_sync_client(
url: &str,
auth_route_config: Option<&AuthRouteConfig>,
) -> Result<Client, String> {
build_default_reqwest_client_for_route(url, ClientRouteClass::Api, auth_route_config)
.map_err(|err| format!("failed to configure curated plugins HTTP client: {err}"))
}
fn read_extracted_backup_archive_git_sha(repo_path: &Path) -> Result<Option<String>, String> {
let git_dir = repo_path.join(".git");
if !git_dir.is_dir() {
@@ -178,6 +178,7 @@ async fn run_sync_with_transport_overrides(
&git_binary,
&api_base_url,
&backup_archive_api_url,
/*auth_route_config*/ None,
)
})
.await
@@ -190,7 +191,11 @@ async fn run_http_sync(
) -> Result<String, String> {
let api_base_url = api_base_url.into();
tokio::task::spawn_blocking(move || {
sync_openai_plugins_repo_via_http(codex_home.as_path(), &api_base_url)
sync_openai_plugins_repo_via_http(
codex_home.as_path(),
&api_base_url,
/*auth_route_config*/ None,
)
})
.await
.expect("sync task should join")
@@ -329,6 +334,7 @@ exit 1
git_path.to_str().expect("utf8 path"),
"http://127.0.0.1:9",
"http://127.0.0.1:9/backend-api/plugins/export/curated",
/*auth_route_config*/ None,
)
};
let first = scope.spawn(run_sync);
@@ -115,6 +115,7 @@ pub(crate) async fn load_plugins_config(codex_home: &Path, cwd: &Path) -> Plugin
/*default_enabled*/ false,
),
"https://chatgpt.com/backend-api/".to_string(),
/*auth_route_config*/ None,
)
}