mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
fix(core-plugins): fetch installed plugins once
This commit is contained in:
@@ -617,14 +617,9 @@ impl PluginRequestProcessor {
|
||||
// TODO(remote plugins): Remove this once remote plugins are ready and vertical plugins are
|
||||
// served directly from the normal remote catalog.
|
||||
if include_vertical && !config.features.enabled(Feature::RemotePlugin) {
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
match codex_core_plugins::remote::fetch_openai_curated_remote_collection_marketplace(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await
|
||||
match plugins_manager
|
||||
.fetch_openai_curated_remote_collection_marketplace(&plugins_input, auth.as_ref())
|
||||
.await
|
||||
{
|
||||
Ok(Some(remote_marketplace)) => {
|
||||
data.push(remote_marketplace_to_info(remote_marketplace));
|
||||
@@ -656,16 +651,9 @@ impl PluginRequestProcessor {
|
||||
remote_sources.push(RemoteMarketplaceSource::SharedWithMe);
|
||||
}
|
||||
if !remote_sources.is_empty() {
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
match codex_core_plugins::remote::fetch_remote_marketplaces(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
&remote_sources,
|
||||
/*global_catalog_cache_path*/ Some(config.codex_home.as_path()),
|
||||
)
|
||||
.await
|
||||
match plugins_manager
|
||||
.fetch_remote_marketplaces(&plugins_input, auth.as_ref(), &remote_sources)
|
||||
.await
|
||||
{
|
||||
Ok(remote_marketplaces) => {
|
||||
for remote_marketplace in remote_marketplaces
|
||||
|
||||
@@ -35,6 +35,7 @@ use wiremock::matchers::header;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
use wiremock::matchers::query_param;
|
||||
use wiremock::matchers::query_param_is_missing;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
|
||||
@@ -225,10 +226,16 @@ enabled = true
|
||||
.as_array_mut()
|
||||
.expect("installed plugins should be an array")
|
||||
.push(remote_only);
|
||||
let mut personal = global_installed_body["plugins"][0].clone();
|
||||
personal["id"] = serde_json::json!("plugins~Plugin_22222222222222222222222222222222");
|
||||
personal["name"] = serde_json::json!("personal-only");
|
||||
personal["scope"] = serde_json::json!("USER");
|
||||
global_installed_body["plugins"]
|
||||
.as_array_mut()
|
||||
.expect("installed plugins should be an array")
|
||||
.push(personal);
|
||||
let global_installed_body = serde_json::to_string(&global_installed_body)?;
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())
|
||||
.await;
|
||||
mount_aggregate_remote_installed_plugins(&server, &global_installed_body).await;
|
||||
|
||||
let mut app_server = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, app_server.initialize()).await??;
|
||||
@@ -1495,9 +1502,7 @@ async fn app_server_startup_sync_downloads_remote_installed_plugin_bundles() ->
|
||||
/*enabled*/ true,
|
||||
remote_app_manifest.clone(),
|
||||
);
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())
|
||||
.await;
|
||||
mount_aggregate_remote_installed_plugins(&server, &global_installed_body).await;
|
||||
|
||||
let installed_path = codex_home
|
||||
.path()
|
||||
@@ -1566,9 +1571,7 @@ async fn plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles()
|
||||
);
|
||||
mount_remote_plugin_list(&server, "GLOBAL", &global_installed_body).await;
|
||||
mount_remote_plugin_list(&server, "WORKSPACE", empty_remote_installed_plugins_body()).await;
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())
|
||||
.await;
|
||||
mount_aggregate_remote_installed_plugins(&server, &global_installed_body).await;
|
||||
|
||||
let old_path = codex_home
|
||||
.path()
|
||||
@@ -1739,20 +1742,12 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/installed"))
|
||||
.and(query_param("scope", "GLOBAL"))
|
||||
.and(query_param_is_missing("scope"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.and(header("oai-product-sku", "codex"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(global_installed_body))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/installed"))
|
||||
.and(query_param("scope", "WORKSPACE"))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.and(header("oai-product-sku", "codex"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(empty_page_body))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
@@ -1907,9 +1902,7 @@ async fn plugin_list_includes_openai_curated_remote_collection_when_requested()
|
||||
}
|
||||
}"#;
|
||||
mount_openai_curated_remote_collection_plugin_list(&server, collection_body).await;
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())
|
||||
.await;
|
||||
mount_aggregate_remote_installed_plugins(&server, empty_remote_installed_plugins_body()).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -1994,9 +1987,7 @@ async fn plugin_list_fail_opens_openai_curated_remote_collection_errors() -> Res
|
||||
.respond_with(ResponseTemplate::new(500).set_body_string("temporary failure"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())
|
||||
.await;
|
||||
mount_aggregate_remote_installed_plugins(&server, empty_remote_installed_plugins_body()).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -2221,8 +2212,11 @@ plugin_sharing = true
|
||||
.push(unlisted_installed_body["plugins"][0].clone());
|
||||
let workspace_installed_body = serde_json::to_string(&workspace_installed_body)?;
|
||||
let global_installed_body = remote_installed_plugin_body("", "1.2.3", /*enabled*/ true);
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;
|
||||
let aggregate_installed_body = aggregate_remote_installed_plugin_bodies(&[
|
||||
&global_installed_body,
|
||||
&workspace_installed_body,
|
||||
])?;
|
||||
mount_aggregate_remote_installed_plugins(&server, &aggregate_installed_body).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -2270,8 +2264,12 @@ plugin_sharing = true
|
||||
)
|
||||
]
|
||||
);
|
||||
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
|
||||
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"/ps/plugins/installed",
|
||||
/*expected_count*/ 1,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2309,9 +2307,7 @@ plugin_sharing = false
|
||||
.await;
|
||||
let global_installed_body =
|
||||
remote_installed_plugin_body(&bundle_url, "1.2.3", /*enabled*/ true);
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", &global_installed_body).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", empty_remote_installed_plugins_body())
|
||||
.await;
|
||||
mount_aggregate_remote_installed_plugins(&server, &global_installed_body).await;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
@@ -2348,8 +2344,12 @@ plugin_sharing = false
|
||||
.path()
|
||||
.join("plugins/cache/openai-curated-remote/linear/1.2.3/.codex-plugin/plugin.json");
|
||||
wait_for_path_exists(&installed_path).await?;
|
||||
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
|
||||
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"/ps/plugins/installed",
|
||||
/*expected_count*/ 1,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2385,7 +2385,7 @@ async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag
|
||||
/*enabled*/ Some(false),
|
||||
);
|
||||
mount_remote_plugin_list(&server, "WORKSPACE", &workspace_plugin_body).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;
|
||||
mount_aggregate_remote_installed_plugins(&server, &workspace_installed_body).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -2501,8 +2501,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
.push(unlisted_installed_body["plugins"][0].clone());
|
||||
let workspace_installed_body = serde_json::to_string(&workspace_installed_body)?;
|
||||
mount_shared_workspace_plugins(&server, &shared_plugin_body).await;
|
||||
mount_remote_installed_plugins(&server, "GLOBAL", empty_remote_installed_plugins_body()).await;
|
||||
mount_remote_installed_plugins(&server, "WORKSPACE", &workspace_installed_body).await;
|
||||
mount_aggregate_remote_installed_plugins(&server, &workspace_installed_body).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -2630,8 +2629,12 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
||||
share_context.discoverability,
|
||||
Some(PluginShareDiscoverability::Unlisted)
|
||||
);
|
||||
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
|
||||
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"/ps/plugins/installed",
|
||||
/*expected_count*/ 1,
|
||||
)
|
||||
.await?;
|
||||
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -2782,19 +2785,7 @@ async fn plugin_list_marks_remote_plugin_disabled_by_admin() -> Result<()> {
|
||||
.mount(&server)
|
||||
.await;
|
||||
}
|
||||
for (scope, body) in [
|
||||
("GLOBAL", global_installed_body),
|
||||
("WORKSPACE", empty_page_body),
|
||||
] {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/installed"))
|
||||
.and(query_param("scope", scope))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(body))
|
||||
.mount(&server)
|
||||
.await;
|
||||
}
|
||||
mount_aggregate_remote_installed_plugins(&server, global_installed_body).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -3002,29 +2993,6 @@ async fn wait_for_remote_plugin_request_count(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_remote_installed_scope_request(server: &MockServer, scope: &str) -> Result<()> {
|
||||
timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
let Some(requests) = server.received_requests().await else {
|
||||
bail!("wiremock did not record requests");
|
||||
};
|
||||
if requests.iter().any(|request| {
|
||||
request.method == "GET"
|
||||
&& request.url.path().ends_with("/ps/plugins/installed")
|
||||
&& request
|
||||
.url
|
||||
.query_pairs()
|
||||
.any(|(name, value)| name == "scope" && value == scope)
|
||||
}) {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_path_exists(path: &std::path::Path) -> Result<()> {
|
||||
timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
@@ -3087,17 +3055,34 @@ async fn mount_shared_workspace_plugins(server: &MockServer, body: &str) {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mount_remote_installed_plugins(server: &MockServer, scope: &str, body: &str) {
|
||||
async fn mount_aggregate_remote_installed_plugins(server: &MockServer, body: &str) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/ps/plugins/installed"))
|
||||
.and(query_param("scope", scope))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.and(query_param_is_missing("scope"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(body))
|
||||
.expect(1)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn aggregate_remote_installed_plugin_bodies(bodies: &[&str]) -> Result<String> {
|
||||
let mut aggregate: serde_json::Value =
|
||||
serde_json::from_str(empty_remote_installed_plugins_body())?;
|
||||
let aggregate_plugins = aggregate["plugins"]
|
||||
.as_array_mut()
|
||||
.ok_or_else(|| anyhow::anyhow!("installed plugins should be an array"))?;
|
||||
for body in bodies {
|
||||
let mut page: serde_json::Value = serde_json::from_str(body)?;
|
||||
let page_plugins = page["plugins"]
|
||||
.as_array_mut()
|
||||
.ok_or_else(|| anyhow::anyhow!("installed plugins should be an array"))?;
|
||||
aggregate_plugins.append(page_plugins);
|
||||
}
|
||||
Ok(serde_json::to_string(&aggregate)?)
|
||||
}
|
||||
|
||||
fn empty_remote_installed_plugins_body() -> &'static str {
|
||||
r#"{
|
||||
"plugins": [],
|
||||
|
||||
@@ -37,6 +37,7 @@ use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome;
|
||||
use crate::marketplace_upgrade::configured_git_marketplace_names;
|
||||
use crate::marketplace_upgrade::upgrade_configured_git_marketplaces;
|
||||
use crate::remote::RemoteInstalledPlugin;
|
||||
use crate::remote::RemoteInstalledPluginsSnapshot;
|
||||
use crate::remote::RemotePluginCatalogError;
|
||||
use crate::remote::RemotePluginScope;
|
||||
use crate::remote::RemotePluginServiceConfig;
|
||||
@@ -82,6 +83,8 @@ use tracing::warn;
|
||||
static CURATED_REPO_SYNC_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
const FEATURED_PLUGIN_IDS_CACHE_TTL: std::time::Duration =
|
||||
std::time::Duration::from_secs(60 * 60 * 3);
|
||||
const REMOTE_INSTALLED_PLUGINS_SNAPSHOT_CACHE_TTL: std::time::Duration =
|
||||
std::time::Duration::from_secs(60 * 5);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginsConfigInput {
|
||||
@@ -122,6 +125,21 @@ struct CachedFeaturedPluginIds {
|
||||
featured_plugin_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct RemoteInstalledPluginsSnapshotCacheKey {
|
||||
chatgpt_base_url: String,
|
||||
account_id: Option<String>,
|
||||
chatgpt_user_id: Option<String>,
|
||||
is_workspace_account: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CachedRemoteInstalledPluginsSnapshot {
|
||||
key: RemoteInstalledPluginsSnapshotCacheKey,
|
||||
expires_at: Instant,
|
||||
snapshot: RemoteInstalledPluginsSnapshot,
|
||||
}
|
||||
|
||||
struct RemoteInstalledPluginsCacheRefreshRequest {
|
||||
service_config: RemotePluginServiceConfig,
|
||||
auth: Option<CodexAuth>,
|
||||
@@ -188,6 +206,18 @@ fn featured_plugin_ids_cache_key(
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_installed_plugins_snapshot_cache_key(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> RemoteInstalledPluginsSnapshotCacheKey {
|
||||
RemoteInstalledPluginsSnapshotCacheKey {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PluginInstallRequest {
|
||||
pub plugin_name: String,
|
||||
@@ -301,6 +331,8 @@ pub struct PluginsManager {
|
||||
enabled_outcome_cache: RwLock<EnabledOutcomeCache>,
|
||||
enabled_outcome_load_semaphore: Semaphore,
|
||||
remote_installed_plugins_cache: RwLock<Option<Vec<RemoteInstalledPlugin>>>,
|
||||
remote_installed_plugins_snapshot_cache: RwLock<Option<CachedRemoteInstalledPluginsSnapshot>>,
|
||||
remote_installed_plugins_fetch_semaphore: Semaphore,
|
||||
remote_installed_plugins_cache_refresh_state: RwLock<RemoteInstalledPluginsCacheRefreshState>,
|
||||
restriction_product: Option<Product>,
|
||||
analytics_events_client: RwLock<Option<AnalyticsEventsClient>>,
|
||||
@@ -352,6 +384,8 @@ impl PluginsManager {
|
||||
enabled_outcome_cache: RwLock::new(EnabledOutcomeCache::default()),
|
||||
enabled_outcome_load_semaphore: Semaphore::new(/*permits*/ 1),
|
||||
remote_installed_plugins_cache: RwLock::new(None),
|
||||
remote_installed_plugins_snapshot_cache: RwLock::new(None),
|
||||
remote_installed_plugins_fetch_semaphore: Semaphore::new(/*permits*/ 1),
|
||||
remote_installed_plugins_cache_refresh_state: RwLock::new(
|
||||
RemoteInstalledPluginsCacheRefreshState::default(),
|
||||
),
|
||||
@@ -582,20 +616,160 @@ impl PluginsManager {
|
||||
visible_scopes: &[RemotePluginScope],
|
||||
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
|
||||
) -> Result<Vec<crate::remote::RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let plugins = crate::remote::fetch_remote_installed_plugins(
|
||||
&remote_plugin_service_config(config),
|
||||
auth,
|
||||
)
|
||||
.await?;
|
||||
let service_config = remote_plugin_service_config(config);
|
||||
let (snapshot, changed) = self
|
||||
.load_remote_installed_plugins_snapshot(
|
||||
&service_config,
|
||||
auth,
|
||||
/*force_refresh*/ false,
|
||||
)
|
||||
.await?;
|
||||
let plugins = snapshot.effective_plugins()?;
|
||||
let marketplaces =
|
||||
crate::remote::group_remote_installed_plugins_by_marketplaces(&plugins, visible_scopes);
|
||||
let changed = self.write_remote_installed_plugins_cache(plugins);
|
||||
if changed && let Some(on_effective_plugins_changed) = on_effective_plugins_changed {
|
||||
on_effective_plugins_changed();
|
||||
}
|
||||
Ok(marketplaces)
|
||||
}
|
||||
|
||||
pub async fn fetch_remote_marketplaces(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
auth: Option<&CodexAuth>,
|
||||
sources: &[crate::remote::RemoteMarketplaceSource],
|
||||
) -> Result<Vec<crate::remote::RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let snapshot = self.remote_installed_plugins_snapshot(config, auth).await?;
|
||||
crate::remote::fetch_remote_marketplaces_from_snapshot(
|
||||
&remote_plugin_service_config(config),
|
||||
auth,
|
||||
sources,
|
||||
Some(self.codex_home.as_path()),
|
||||
&snapshot,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_openai_curated_remote_collection_marketplace(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Result<Option<crate::remote::RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let snapshot = self.remote_installed_plugins_snapshot(config, auth).await?;
|
||||
crate::remote::fetch_openai_curated_remote_collection_marketplace_from_snapshot(
|
||||
&remote_plugin_service_config(config),
|
||||
auth,
|
||||
&snapshot,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn remote_installed_plugins_snapshot(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Result<RemoteInstalledPluginsSnapshot, RemotePluginCatalogError> {
|
||||
let service_config = remote_plugin_service_config(config);
|
||||
let (snapshot, _) = self
|
||||
.load_remote_installed_plugins_snapshot(
|
||||
&service_config,
|
||||
auth,
|
||||
/*force_refresh*/ false,
|
||||
)
|
||||
.await?;
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
async fn load_remote_installed_plugins_snapshot(
|
||||
&self,
|
||||
service_config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
force_refresh: bool,
|
||||
) -> Result<(RemoteInstalledPluginsSnapshot, bool), RemotePluginCatalogError> {
|
||||
let cache_key = remote_installed_plugins_snapshot_cache_key(service_config, auth);
|
||||
if !force_refresh
|
||||
&& let Some(snapshot) = self.cached_remote_installed_plugins_snapshot(&cache_key)
|
||||
{
|
||||
return Ok((snapshot, false));
|
||||
}
|
||||
|
||||
let Ok(_fetch_permit) = self
|
||||
.remote_installed_plugins_fetch_semaphore
|
||||
.acquire()
|
||||
.await
|
||||
else {
|
||||
return Err(RemotePluginCatalogError::UnexpectedResponse(
|
||||
"remote installed plugins fetch semaphore closed".to_string(),
|
||||
));
|
||||
};
|
||||
if !force_refresh
|
||||
&& let Some(snapshot) = self.cached_remote_installed_plugins_snapshot(&cache_key)
|
||||
{
|
||||
return Ok((snapshot, false));
|
||||
}
|
||||
|
||||
let snapshot = crate::remote::fetch_remote_installed_plugins(service_config, auth).await?;
|
||||
let changed = self.write_remote_installed_plugins_snapshot(cache_key, snapshot.clone())?;
|
||||
Ok((snapshot, changed))
|
||||
}
|
||||
|
||||
fn cached_remote_installed_plugins_snapshot(
|
||||
&self,
|
||||
cache_key: &RemoteInstalledPluginsSnapshotCacheKey,
|
||||
) -> Option<RemoteInstalledPluginsSnapshot> {
|
||||
{
|
||||
let cache = match self.remote_installed_plugins_snapshot_cache.read() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
let now = Instant::now();
|
||||
if let Some(cached) = cache.as_ref()
|
||||
&& now < cached.expires_at
|
||||
&& cached.key == *cache_key
|
||||
{
|
||||
return Some(cached.snapshot.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = match self.remote_installed_plugins_snapshot_cache.write() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
let now = Instant::now();
|
||||
if cache
|
||||
.as_ref()
|
||||
.is_some_and(|cached| now >= cached.expires_at || cached.key != *cache_key)
|
||||
{
|
||||
*cache = None;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn write_remote_installed_plugins_snapshot(
|
||||
&self,
|
||||
cache_key: RemoteInstalledPluginsSnapshotCacheKey,
|
||||
snapshot: RemoteInstalledPluginsSnapshot,
|
||||
) -> Result<bool, RemotePluginCatalogError> {
|
||||
let plugins = snapshot.effective_plugins()?;
|
||||
match self.remote_installed_plugins_snapshot_cache.write() {
|
||||
Ok(mut cache) => {
|
||||
*cache = Some(CachedRemoteInstalledPluginsSnapshot {
|
||||
key: cache_key,
|
||||
expires_at: Instant::now() + REMOTE_INSTALLED_PLUGINS_SNAPSHOT_CACHE_TTL,
|
||||
snapshot,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
*err.into_inner() = Some(CachedRemoteInstalledPluginsSnapshot {
|
||||
key: cache_key,
|
||||
expires_at: Instant::now() + REMOTE_INSTALLED_PLUGINS_SNAPSHOT_CACHE_TTL,
|
||||
snapshot,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(self.write_remote_installed_plugins_cache(plugins))
|
||||
}
|
||||
|
||||
fn write_remote_installed_plugins_cache(&self, plugins: Vec<RemoteInstalledPlugin>) -> bool {
|
||||
let mut cache = match self.remote_installed_plugins_cache.write() {
|
||||
Ok(cache) => cache,
|
||||
@@ -611,6 +785,10 @@ impl PluginsManager {
|
||||
}
|
||||
|
||||
pub fn clear_remote_installed_plugins_cache(&self) -> bool {
|
||||
match self.remote_installed_plugins_snapshot_cache.write() {
|
||||
Ok(mut cache) => *cache = None,
|
||||
Err(err) => *err.into_inner() = None,
|
||||
}
|
||||
let mut cache = match self.remote_installed_plugins_cache.write() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
@@ -684,22 +862,31 @@ impl PluginsManager {
|
||||
}
|
||||
|
||||
let manager = Arc::clone(self);
|
||||
let config_for_refresh = config.clone();
|
||||
let auth_for_refresh = auth.clone();
|
||||
let on_local_cache_changed = Arc::new(move || {
|
||||
manager.maybe_start_remote_installed_plugins_cache_refresh_after_mutation(
|
||||
&config_for_refresh,
|
||||
auth_for_refresh.clone(),
|
||||
on_effective_plugins_changed.clone(),
|
||||
);
|
||||
let config = config.clone();
|
||||
tokio::spawn(async move {
|
||||
match manager
|
||||
.remote_installed_plugins_snapshot(&config, auth.as_ref())
|
||||
.await
|
||||
{
|
||||
Ok(snapshot) => {
|
||||
crate::remote::maybe_start_remote_installed_plugin_bundle_sync(
|
||||
manager.codex_home.clone(),
|
||||
snapshot,
|
||||
on_effective_plugins_changed,
|
||||
);
|
||||
}
|
||||
Err(
|
||||
RemotePluginCatalogError::AuthRequired
|
||||
| RemotePluginCatalogError::UnsupportedAuthMode,
|
||||
) => {}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
error = %err,
|
||||
"failed to load installed plugins for remote bundle sync"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
crate::remote::maybe_start_remote_installed_plugin_bundle_sync(
|
||||
self.codex_home.clone(),
|
||||
remote_plugin_service_config(config),
|
||||
auth,
|
||||
Some(on_local_cache_changed),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn maybe_start_plugin_list_background_tasks_for_config(
|
||||
@@ -710,11 +897,6 @@ impl PluginsManager {
|
||||
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
|
||||
) {
|
||||
self.maybe_start_non_curated_plugin_cache_refresh(roots);
|
||||
self.maybe_start_remote_installed_plugins_cache_refresh(
|
||||
config,
|
||||
auth.clone(),
|
||||
on_effective_plugins_changed.clone(),
|
||||
);
|
||||
self.maybe_start_remote_installed_plugin_bundle_sync(
|
||||
config,
|
||||
auth,
|
||||
@@ -1299,11 +1481,6 @@ impl PluginsManager {
|
||||
let on_effective_plugins_changed = on_effective_plugins_changed.clone();
|
||||
tokio::spawn(async move {
|
||||
let auth = auth_manager_for_remote_sync.auth().await;
|
||||
manager.maybe_start_remote_installed_plugins_cache_refresh(
|
||||
&config_for_remote_sync,
|
||||
auth.clone(),
|
||||
on_effective_plugins_changed.clone(),
|
||||
);
|
||||
manager.maybe_start_remote_installed_plugin_bundle_sync(
|
||||
&config_for_remote_sync,
|
||||
auth.clone(),
|
||||
@@ -1569,16 +1746,17 @@ impl PluginsManager {
|
||||
}
|
||||
};
|
||||
|
||||
let installed_plugins = crate::remote::fetch_remote_installed_plugins(
|
||||
&request.service_config,
|
||||
request.auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let installed_plugins = self
|
||||
.load_remote_installed_plugins_snapshot(
|
||||
&request.service_config,
|
||||
request.auth.as_ref(),
|
||||
/*force_refresh*/ true,
|
||||
)
|
||||
.await;
|
||||
match installed_plugins {
|
||||
Ok(installed_plugins) => {
|
||||
Ok((_, changed)) => {
|
||||
// TODO(remote plugins): reconcile missing or stale local bundles before
|
||||
// publishing remote installed state as effective local plugin config.
|
||||
let changed = self.write_remote_installed_plugins_cache(installed_plugins);
|
||||
let should_notify = changed
|
||||
|| matches!(
|
||||
request.notify,
|
||||
|
||||
@@ -126,6 +126,11 @@ pub struct RemoteInstalledPlugin {
|
||||
pub keywords: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct RemoteInstalledPluginsSnapshot {
|
||||
plugins: Vec<RemotePluginInstalledItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RemotePluginSummary {
|
||||
pub id: String,
|
||||
@@ -336,6 +341,8 @@ pub enum RemotePluginCatalogError {
|
||||
pub enum RemotePluginScope {
|
||||
#[serde(rename = "GLOBAL")]
|
||||
Global,
|
||||
#[serde(rename = "USER")]
|
||||
User,
|
||||
#[serde(rename = "WORKSPACE")]
|
||||
Workspace,
|
||||
}
|
||||
@@ -344,6 +351,7 @@ impl RemotePluginScope {
|
||||
fn api_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Global => "GLOBAL",
|
||||
Self::User => "USER",
|
||||
Self::Workspace => "WORKSPACE",
|
||||
}
|
||||
}
|
||||
@@ -351,6 +359,7 @@ impl RemotePluginScope {
|
||||
fn marketplace_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Global => REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||
Self::User => unreachable!("personal plugins do not map to a Codex marketplace"),
|
||||
Self::Workspace => REMOTE_WORKSPACE_MARKETPLACE_NAME,
|
||||
}
|
||||
}
|
||||
@@ -358,6 +367,7 @@ impl RemotePluginScope {
|
||||
fn marketplace_display_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Global => REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME,
|
||||
Self::User => unreachable!("personal plugins do not map to a Codex marketplace"),
|
||||
Self::Workspace => REMOTE_WORKSPACE_MARKETPLACE_DISPLAY_NAME,
|
||||
}
|
||||
}
|
||||
@@ -374,6 +384,33 @@ impl RemotePluginScope {
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteInstalledPluginsSnapshot {
|
||||
fn plugins_for_scope(&self, scope: RemotePluginScope) -> Vec<RemotePluginInstalledItem> {
|
||||
self.plugins
|
||||
.iter()
|
||||
.filter(|plugin| plugin.plugin.scope == scope)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn effective_plugins(
|
||||
&self,
|
||||
) -> Result<Vec<RemoteInstalledPlugin>, RemotePluginCatalogError> {
|
||||
let mut plugins = self
|
||||
.plugins
|
||||
.iter()
|
||||
.filter(|plugin| plugin.plugin.scope != RemotePluginScope::User)
|
||||
.map(remote_installed_plugin_to_cache_entry)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
plugins.sort_by(|left, right| {
|
||||
left.marketplace_name
|
||||
.cmp(&right.marketplace_name)
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
Ok(plugins)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct RemotePluginPagination {
|
||||
next_page_token: Option<String>,
|
||||
@@ -496,6 +533,10 @@ fn remote_plugin_canonical_marketplace_name(
|
||||
) -> Result<&'static str, RemotePluginCatalogError> {
|
||||
match plugin.scope {
|
||||
RemotePluginScope::Global => Ok(REMOTE_GLOBAL_MARKETPLACE_NAME),
|
||||
RemotePluginScope::User => Err(RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"personal plugin `{}` does not map to a Codex marketplace",
|
||||
plugin.id
|
||||
))),
|
||||
RemotePluginScope::Workspace => match workspace_plugin_discoverability(plugin)? {
|
||||
RemotePluginShareDiscoverability::Listed => Ok(REMOTE_WORKSPACE_MARKETPLACE_NAME),
|
||||
RemotePluginShareDiscoverability::Private
|
||||
@@ -557,36 +598,43 @@ pub async fn fetch_remote_marketplaces(
|
||||
auth: Option<&CodexAuth>,
|
||||
sources: &[RemoteMarketplaceSource],
|
||||
global_catalog_cache_path: Option<&Path>,
|
||||
) -> Result<Vec<RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let installed_plugins = fetch_remote_installed_plugins(config, auth).await?;
|
||||
fetch_remote_marketplaces_from_snapshot(
|
||||
config,
|
||||
auth,
|
||||
sources,
|
||||
global_catalog_cache_path,
|
||||
&installed_plugins,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn fetch_remote_marketplaces_from_snapshot(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
sources: &[RemoteMarketplaceSource],
|
||||
global_catalog_cache_path: Option<&Path>,
|
||||
installed_plugins: &RemoteInstalledPluginsSnapshot,
|
||||
) -> Result<Vec<RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let mut marketplaces = Vec::new();
|
||||
let needs_workspace_installed = sources.iter().any(|source| {
|
||||
matches!(
|
||||
source,
|
||||
RemoteMarketplaceSource::WorkspaceDirectory | RemoteMarketplaceSource::SharedWithMe
|
||||
)
|
||||
});
|
||||
let workspace_installed_plugins = if needs_workspace_installed {
|
||||
Some(fetch_installed_plugins_for_scope(config, auth, RemotePluginScope::Workspace).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let workspace_installed_plugins =
|
||||
installed_plugins.plugins_for_scope(RemotePluginScope::Workspace);
|
||||
|
||||
for source in sources {
|
||||
match source {
|
||||
RemoteMarketplaceSource::Global => {
|
||||
let scope = RemotePluginScope::Global;
|
||||
let (directory_plugins, installed_plugins) = tokio::try_join!(
|
||||
fetch_directory_plugins_for_scope(config, auth, scope),
|
||||
fetch_installed_plugins_for_scope(config, auth, scope),
|
||||
)?;
|
||||
let directory_plugins =
|
||||
fetch_directory_plugins_for_scope(config, auth, scope).await?;
|
||||
let directory_plugins_for_cache =
|
||||
global_catalog_cache_path.map(|_| directory_plugins.clone());
|
||||
if let Some(marketplace) = build_remote_marketplace(
|
||||
scope.marketplace_name(),
|
||||
scope.marketplace_display_name(),
|
||||
directory_plugins,
|
||||
installed_plugins,
|
||||
installed_plugins.plugins_for_scope(scope),
|
||||
/*include_installed_only*/ true,
|
||||
)? {
|
||||
marketplaces.push(marketplace);
|
||||
@@ -610,7 +658,7 @@ pub async fn fetch_remote_marketplaces(
|
||||
scope.marketplace_name(),
|
||||
scope.marketplace_display_name(),
|
||||
directory_plugins,
|
||||
workspace_installed_plugins.clone().unwrap_or_default(),
|
||||
workspace_installed_plugins.clone(),
|
||||
/*include_installed_only*/ false,
|
||||
)? {
|
||||
marketplaces.push(marketplace);
|
||||
@@ -640,7 +688,7 @@ pub async fn fetch_remote_marketplaces(
|
||||
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME,
|
||||
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_DISPLAY_NAME,
|
||||
directly_shared_plugins,
|
||||
workspace_installed_plugins.clone().unwrap_or_default(),
|
||||
workspace_installed_plugins.clone(),
|
||||
/*include_installed_only*/ false,
|
||||
)? {
|
||||
marketplaces.push(marketplace);
|
||||
@@ -648,7 +696,6 @@ pub async fn fetch_remote_marketplaces(
|
||||
|
||||
let unlisted_installed_plugins = workspace_installed_plugins
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|plugin| match workspace_plugin_discoverability(&plugin.plugin) {
|
||||
@@ -713,24 +760,36 @@ pub fn cached_global_remote_discoverable_plugins(
|
||||
pub async fn fetch_openai_curated_remote_collection_marketplace(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Result<Option<RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let installed_plugins = fetch_remote_installed_plugins(config, auth).await?;
|
||||
fetch_openai_curated_remote_collection_marketplace_from_snapshot(
|
||||
config,
|
||||
auth,
|
||||
&installed_plugins,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn fetch_openai_curated_remote_collection_marketplace_from_snapshot(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
installed_plugins: &RemoteInstalledPluginsSnapshot,
|
||||
) -> Result<Option<RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let scope = RemotePluginScope::Global;
|
||||
let (directory_plugins, installed_plugins) = tokio::try_join!(
|
||||
fetch_directory_plugins_for_scope_with_collection(
|
||||
config,
|
||||
auth,
|
||||
scope,
|
||||
OPENAI_CURATED_REMOTE_COLLECTION_KEY,
|
||||
),
|
||||
fetch_installed_plugins_for_scope(config, auth, scope),
|
||||
)?;
|
||||
let directory_plugins = fetch_directory_plugins_for_scope_with_collection(
|
||||
config,
|
||||
auth,
|
||||
scope,
|
||||
OPENAI_CURATED_REMOTE_COLLECTION_KEY,
|
||||
)
|
||||
.await?;
|
||||
|
||||
build_remote_marketplace(
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||
REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME,
|
||||
directory_plugins,
|
||||
installed_plugins,
|
||||
installed_plugins.plugins_for_scope(scope),
|
||||
/*include_installed_only*/ false,
|
||||
)
|
||||
}
|
||||
@@ -786,31 +845,9 @@ fn build_remote_marketplace(
|
||||
pub(crate) async fn fetch_remote_installed_plugins(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Result<Vec<RemoteInstalledPlugin>, RemotePluginCatalogError> {
|
||||
) -> Result<RemoteInstalledPluginsSnapshot, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let global = async {
|
||||
let scope = RemotePluginScope::Global;
|
||||
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
|
||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
||||
};
|
||||
let workspace = async {
|
||||
let scope = RemotePluginScope::Workspace;
|
||||
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
|
||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
||||
};
|
||||
|
||||
let (global, workspace) = tokio::try_join!(global, workspace)?;
|
||||
let mut installed_plugins = [global, workspace]
|
||||
.into_iter()
|
||||
.flat_map(|(_scope, plugins)| plugins)
|
||||
.map(|plugin| remote_installed_plugin_to_cache_entry(&plugin))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
installed_plugins.sort_by(|left, right| {
|
||||
left.marketplace_name
|
||||
.cmp(&right.marketplace_name)
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
Ok(installed_plugins)
|
||||
fetch_installed_plugins(config, auth, /*include_download_urls*/ true).await
|
||||
}
|
||||
|
||||
pub fn group_remote_installed_plugins_by_marketplaces(
|
||||
@@ -1220,6 +1257,10 @@ fn remote_plugin_share_context(
|
||||
) -> Result<Option<RemotePluginShareContext>, RemotePluginCatalogError> {
|
||||
match plugin.scope {
|
||||
RemotePluginScope::Global => Ok(None),
|
||||
RemotePluginScope::User => Err(RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"personal plugin `{}` does not expose Codex share context",
|
||||
plugin.id
|
||||
))),
|
||||
RemotePluginScope::Workspace => {
|
||||
let discoverability = workspace_plugin_discoverability(plugin)?;
|
||||
Ok(Some(RemotePluginShareContext {
|
||||
@@ -1455,6 +1496,31 @@ async fn fetch_installed_plugins_for_scope(
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_installed_plugins(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: &CodexAuth,
|
||||
include_download_urls: bool,
|
||||
) -> Result<RemoteInstalledPluginsSnapshot, RemotePluginCatalogError> {
|
||||
let mut plugins = Vec::new();
|
||||
let mut page_token = None;
|
||||
loop {
|
||||
let response = get_remote_plugin_installed_page(
|
||||
config,
|
||||
auth,
|
||||
/*scope*/ None,
|
||||
page_token.as_deref(),
|
||||
include_download_urls,
|
||||
)
|
||||
.await?;
|
||||
plugins.extend(response.plugins);
|
||||
let Some(next_page_token) = response.pagination.next_page_token else {
|
||||
break;
|
||||
};
|
||||
page_token = Some(next_page_token);
|
||||
}
|
||||
Ok(RemoteInstalledPluginsSnapshot { plugins })
|
||||
}
|
||||
|
||||
async fn fetch_installed_plugins_for_scope_with_download_url(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: &CodexAuth,
|
||||
@@ -1467,7 +1533,7 @@ async fn fetch_installed_plugins_for_scope_with_download_url(
|
||||
let response = get_remote_plugin_installed_page(
|
||||
config,
|
||||
auth,
|
||||
scope,
|
||||
Some(scope),
|
||||
page_token.as_deref(),
|
||||
include_download_urls,
|
||||
)
|
||||
@@ -1522,7 +1588,7 @@ async fn get_remote_shared_workspace_plugins_page(
|
||||
async fn get_remote_plugin_installed_page(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: &CodexAuth,
|
||||
scope: RemotePluginScope,
|
||||
scope: Option<RemotePluginScope>,
|
||||
page_token: Option<&str>,
|
||||
include_download_urls: bool,
|
||||
) -> Result<RemotePluginInstalledResponse, RemotePluginCatalogError> {
|
||||
@@ -1530,7 +1596,9 @@ async fn get_remote_plugin_installed_page(
|
||||
let url = format!("{base_url}/ps/plugins/installed");
|
||||
let client = build_reqwest_client();
|
||||
let mut request = authenticated_request(client.get(&url), auth)?;
|
||||
request = request.query(&[("scope", scope.api_value())]);
|
||||
if let Some(scope) = scope {
|
||||
request = request.query(&[("scope", scope.api_value())]);
|
||||
}
|
||||
if include_download_urls {
|
||||
request = request.query(&[("includeDownloadUrls", true)]);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ use super::REMOTE_WORKSPACE_MARKETPLACE_NAME;
|
||||
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;
|
||||
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME;
|
||||
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME;
|
||||
use super::RemoteInstalledPluginsSnapshot;
|
||||
use super::RemotePluginCatalogError;
|
||||
use super::RemotePluginScope;
|
||||
use super::RemotePluginServiceConfig;
|
||||
use super::ensure_chatgpt_auth;
|
||||
use super::fetch_installed_plugins_for_scope_with_download_url;
|
||||
use super::fetch_remote_installed_plugins;
|
||||
use super::remote_plugin_canonical_marketplace_name;
|
||||
use crate::store::PLUGINS_CACHE_DIR;
|
||||
use crate::store::PluginStore;
|
||||
@@ -80,13 +80,9 @@ pub struct RemotePluginCacheMutationGuard {
|
||||
|
||||
pub(crate) fn maybe_start_remote_installed_plugin_bundle_sync(
|
||||
codex_home: PathBuf,
|
||||
config: RemotePluginServiceConfig,
|
||||
auth: Option<CodexAuth>,
|
||||
installed_plugins: RemoteInstalledPluginsSnapshot,
|
||||
on_local_cache_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
|
||||
) {
|
||||
let Some(auth) = auth else {
|
||||
return;
|
||||
};
|
||||
let key = RemoteInstalledPluginBundleSyncKey {
|
||||
plugin_cache_root: remote_plugin_cache_root(&codex_home),
|
||||
};
|
||||
@@ -96,7 +92,7 @@ pub(crate) fn maybe_start_remote_installed_plugin_bundle_sync(
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result =
|
||||
sync_remote_installed_plugin_bundles_once(codex_home, &config, Some(&auth)).await;
|
||||
sync_remote_installed_plugin_bundles_from_snapshot(codex_home, installed_plugins).await;
|
||||
match result {
|
||||
Ok(outcome) => {
|
||||
if outcome.changed_local_cache()
|
||||
@@ -127,25 +123,14 @@ pub async fn sync_remote_installed_plugin_bundles_once(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Result<RemoteInstalledPluginBundleSyncOutcome, RemoteInstalledPluginBundleSyncError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let global = async {
|
||||
let scope = RemotePluginScope::Global;
|
||||
let installed_plugins = fetch_installed_plugins_for_scope_with_download_url(
|
||||
config, auth, scope, /*include_download_urls*/ true,
|
||||
)
|
||||
.await?;
|
||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
||||
};
|
||||
let workspace = async {
|
||||
let scope = RemotePluginScope::Workspace;
|
||||
let installed_plugins = fetch_installed_plugins_for_scope_with_download_url(
|
||||
config, auth, scope, /*include_download_urls*/ true,
|
||||
)
|
||||
.await?;
|
||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
||||
};
|
||||
let installed_plugins = fetch_remote_installed_plugins(config, auth).await?;
|
||||
sync_remote_installed_plugin_bundles_from_snapshot(codex_home, installed_plugins).await
|
||||
}
|
||||
|
||||
let (global, workspace) = tokio::try_join!(global, workspace)?;
|
||||
async fn sync_remote_installed_plugin_bundles_from_snapshot(
|
||||
codex_home: PathBuf,
|
||||
installed_plugins: RemoteInstalledPluginsSnapshot,
|
||||
) -> Result<RemoteInstalledPluginBundleSyncOutcome, RemoteInstalledPluginBundleSyncError> {
|
||||
let store = PluginStore::try_new(codex_home.clone())?;
|
||||
let mut installed_plugin_names_by_marketplace =
|
||||
BTreeMap::<String, BTreeSet<String>>::from_iter([
|
||||
@@ -170,79 +155,80 @@ pub async fn sync_remote_installed_plugin_bundles_once(
|
||||
let mut installed_plugin_ids = BTreeSet::new();
|
||||
let mut failed_remote_plugin_ids = BTreeSet::new();
|
||||
|
||||
for (_scope, installed_plugins) in [global, workspace] {
|
||||
for installed_plugin in installed_plugins {
|
||||
let plugin = installed_plugin.plugin;
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
installed_plugin_names_by_marketplace
|
||||
.entry(marketplace_name.clone())
|
||||
.or_default()
|
||||
.insert(plugin.name.clone());
|
||||
let plugin_id = match PluginId::new(plugin.name.clone(), marketplace_name.clone()) {
|
||||
Ok(plugin_id) => plugin_id,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
remote_plugin_id = %plugin.id,
|
||||
plugin = %plugin.name,
|
||||
marketplace = %marketplace_name,
|
||||
error = %err,
|
||||
"skipping remote installed plugin with invalid local cache id"
|
||||
);
|
||||
failed_remote_plugin_ids.insert(plugin.id);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let release_version = plugin
|
||||
.release
|
||||
.version
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|version| !version.is_empty());
|
||||
if store.active_plugin_version(&plugin_id).as_deref() == release_version {
|
||||
for installed_plugin in installed_plugins.plugins {
|
||||
if installed_plugin.plugin.scope == RemotePluginScope::User {
|
||||
continue;
|
||||
}
|
||||
let plugin = installed_plugin.plugin;
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
installed_plugin_names_by_marketplace
|
||||
.entry(marketplace_name.clone())
|
||||
.or_default()
|
||||
.insert(plugin.name.clone());
|
||||
let plugin_id = match PluginId::new(plugin.name.clone(), marketplace_name.clone()) {
|
||||
Ok(plugin_id) => plugin_id,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
remote_plugin_id = %plugin.id,
|
||||
plugin = %plugin.name,
|
||||
marketplace = %marketplace_name,
|
||||
error = %err,
|
||||
"skipping remote installed plugin with invalid local cache id"
|
||||
);
|
||||
failed_remote_plugin_ids.insert(plugin.id);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let release_version = plugin
|
||||
.release
|
||||
.version
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|version| !version.is_empty());
|
||||
if store.active_plugin_version(&plugin_id).as_deref() == release_version {
|
||||
continue;
|
||||
}
|
||||
|
||||
let bundle = match crate::remote_bundle::validate_remote_plugin_bundle(
|
||||
&plugin.id,
|
||||
&marketplace_name,
|
||||
&plugin.name,
|
||||
release_version,
|
||||
plugin.release.bundle_download_url.as_deref(),
|
||||
plugin.release.app_manifest.clone(),
|
||||
) {
|
||||
Ok(bundle) => bundle,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
remote_plugin_id = %plugin.id,
|
||||
plugin = %plugin.name,
|
||||
marketplace = %marketplace_name,
|
||||
error = %err,
|
||||
"skipping remote installed plugin bundle download"
|
||||
);
|
||||
failed_remote_plugin_ids.insert(plugin.id);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let bundle = match crate::remote_bundle::validate_remote_plugin_bundle(
|
||||
&plugin.id,
|
||||
&marketplace_name,
|
||||
&plugin.name,
|
||||
release_version,
|
||||
plugin.release.bundle_download_url.as_deref(),
|
||||
plugin.release.app_manifest.clone(),
|
||||
) {
|
||||
Ok(bundle) => bundle,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
remote_plugin_id = %plugin.id,
|
||||
plugin = %plugin.name,
|
||||
marketplace = %marketplace_name,
|
||||
error = %err,
|
||||
"skipping remote installed plugin bundle download"
|
||||
);
|
||||
failed_remote_plugin_ids.insert(plugin.id);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match crate::remote_bundle::download_and_install_remote_plugin_bundle(
|
||||
codex_home.clone(),
|
||||
bundle,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
installed_plugin_ids.insert(result.plugin_id.as_key());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
remote_plugin_id = %plugin.id,
|
||||
plugin = %plugin.name,
|
||||
marketplace = %marketplace_name,
|
||||
error = %err,
|
||||
"failed to download remote installed plugin bundle"
|
||||
);
|
||||
failed_remote_plugin_ids.insert(plugin.id);
|
||||
}
|
||||
match crate::remote_bundle::download_and_install_remote_plugin_bundle(
|
||||
codex_home.clone(),
|
||||
bundle,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
installed_plugin_ids.insert(result.plugin_id.as_key());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
remote_plugin_id = %plugin.id,
|
||||
plugin = %plugin.name,
|
||||
marketplace = %marketplace_name,
|
||||
error = %err,
|
||||
"failed to download remote installed plugin bundle"
|
||||
);
|
||||
failed_remote_plugin_ids.insert(plugin.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user