mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Extend PAC routing across Codex network clients
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user