Respect remote plugin visibility scopes

Fixes #26275
This commit is contained in:
Eric Traut
2026-06-04 22:34:21 -07:00
parent b9ff450902
commit 26fadfdff3
10 changed files with 275 additions and 139 deletions
@@ -148,17 +148,6 @@ fn convert_configured_marketplace_plugin_to_plugin_summary(
}
}
fn remote_installed_plugin_visible_scopes(config: &Config) -> Vec<RemotePluginScope> {
let mut scopes = Vec::new();
if config.features.enabled(Feature::RemotePlugin) {
scopes.push(RemotePluginScope::Global);
}
if config.features.enabled(Feature::PluginSharing) {
scopes.push(RemotePluginScope::Workspace);
}
scopes
}
fn filter_openai_curated_installed_conflicts(
marketplaces: &mut Vec<PluginMarketplaceEntry>,
prefer_remote_curated_conflicts: bool,
@@ -764,7 +753,7 @@ impl PluginRequestProcessor {
let plugins_input = config.plugins_config_input();
let remote_installed_plugin_visible_scopes =
remote_installed_plugin_visible_scopes(&config);
plugins_input.remote_installed_plugin_visible_scopes();
plugins_manager.maybe_start_remote_installed_plugin_bundle_sync(
&plugins_input,
auth.clone(),
@@ -2305,8 +2305,6 @@ plugin_sharing = true
.expect("installed plugins should be an array")
.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 mut mcp = TestAppServer::new(codex_home.path()).await?;
@@ -2356,7 +2354,6 @@ plugin_sharing = true
]
);
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
Ok(())
}
@@ -2395,8 +2392,6 @@ plugin_sharing = false
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;
let mut mcp = TestAppServer::new_with_env(
codex_home.path(),
@@ -2434,7 +2429,6 @@ plugin_sharing = false
.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?;
Ok(())
}
@@ -2586,7 +2580,6 @@ 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;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
@@ -2716,7 +2709,6 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
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/list", /*expected_count*/ 0).await?;
Ok(())
}
+4
View File
@@ -223,6 +223,10 @@ fn merge_configured_plugins_with_remote_installed(
.collect::<HashMap<_, _>>();
for (plugin_key, plugin_config) in extra_plugins {
if configured_plugins.contains_key(&plugin_key) {
continue;
}
let remote_curated_plugin_name = installed_plugin_name_for_marketplace(
&plugin_key,
REMOTE_GLOBAL_MARKETPLACE_NAME,
+85 -18
View File
@@ -92,6 +92,7 @@ pub struct PluginsConfigInput {
pub config_layer_stack: ConfigLayerStack,
pub plugins_enabled: bool,
pub remote_plugin_enabled: bool,
pub plugin_sharing_enabled: bool,
pub chatgpt_base_url: String,
}
@@ -100,15 +101,28 @@ impl PluginsConfigInput {
config_layer_stack: ConfigLayerStack,
plugins_enabled: bool,
remote_plugin_enabled: bool,
plugin_sharing_enabled: bool,
chatgpt_base_url: String,
) -> Self {
Self {
config_layer_stack,
plugins_enabled,
remote_plugin_enabled,
plugin_sharing_enabled,
chatgpt_base_url,
}
}
pub fn remote_installed_plugin_visible_scopes(&self) -> Vec<RemotePluginScope> {
let mut scopes = Vec::new();
if self.remote_plugin_enabled {
scopes.push(RemotePluginScope::Global);
}
if self.plugin_sharing_enabled {
scopes.push(RemotePluginScope::Workspace);
}
scopes
}
}
#[derive(Clone, PartialEq, Eq)]
@@ -129,6 +143,7 @@ struct CachedFeaturedPluginIds {
struct RemoteInstalledPluginsCacheRefreshRequest {
service_config: RemotePluginServiceConfig,
auth: Option<CodexAuth>,
scopes: Vec<RemotePluginScope>,
notify: RemoteInstalledPluginsCacheRefreshNotify,
// App-server attaches side effects such as skills metadata invalidation and MCP refreshes when
// remote installed state changes.
@@ -404,7 +419,7 @@ pub struct PluginsManager {
configured_marketplace_upgrade_state: RwLock<ConfiguredMarketplaceUpgradeState>,
non_curated_cache_refresh_state: RwLock<NonCuratedCacheRefreshState>,
cached_enabled_outcome: RwLock<Option<CachedPluginLoadOutcome>>,
remote_installed_plugins_cache: RwLock<Option<Vec<RemoteInstalledPlugin>>>,
remote_installed_plugins_cache: RwLock<Option<CachedRemoteInstalledPlugins>>,
remote_installed_plugins_cache_refresh_state: RwLock<RemoteInstalledPluginsCacheRefreshState>,
remote_sync_lock: Semaphore,
restriction_product: Option<Product>,
@@ -417,6 +432,12 @@ struct CachedPluginLoadOutcome {
outcome: PluginLoadOutcome,
}
#[derive(PartialEq)]
struct CachedRemoteInstalledPlugins {
scopes: Vec<RemotePluginScope>,
plugins: Vec<RemoteInstalledPlugin>,
}
impl PluginsManager {
pub fn new(codex_home: PathBuf) -> Self {
Self::new_with_restriction_product(codex_home, Some(Product::Codex))
@@ -491,7 +512,7 @@ impl PluginsManager {
let outcome = load_plugins_from_layer_stack(
&config.config_layer_stack,
self.remote_installed_plugin_configs(),
self.remote_installed_plugin_configs(config),
&self.store,
self.restriction_product,
config.remote_plugin_enabled,
@@ -537,7 +558,7 @@ impl PluginsManager {
}
load_plugins_from_layer_stack(
config_layer_stack,
self.remote_installed_plugin_configs(),
self.remote_installed_plugin_configs(config),
&self.store,
self.restriction_product,
config.remote_plugin_enabled,
@@ -556,7 +577,7 @@ impl PluginsManager {
}
load_plugin_hooks_from_layer_stack(
config_layer_stack,
self.remote_installed_plugin_configs(),
self.remote_installed_plugin_configs(config),
&self.store,
config.remote_plugin_enabled,
)
@@ -588,16 +609,32 @@ impl PluginsManager {
}
}
fn remote_installed_plugin_configs(&self) -> HashMap<String, PluginConfig> {
fn remote_installed_plugin_configs(
&self,
config: &PluginsConfigInput,
) -> HashMap<String, PluginConfig> {
let visible_scopes = config.remote_installed_plugin_visible_scopes();
if visible_scopes.is_empty() {
return HashMap::new();
}
let cache = match self.remote_installed_plugins_cache.read() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
};
let Some(plugins) = cache.as_ref() else {
let Some(cache) = cache.as_ref() else {
return HashMap::new();
};
remote_installed_plugins_to_config(plugins, &self.store)
let plugins = cache
.plugins
.iter()
.filter(|plugin| {
RemotePluginScope::from_marketplace_name(&plugin.marketplace_name)
.is_some_and(|scope| visible_scopes.contains(&scope))
})
.cloned()
.collect::<Vec<_>>();
remote_installed_plugins_to_config(&plugins, &self.store)
}
pub fn build_remote_installed_plugin_marketplaces_from_cache(
@@ -608,8 +645,19 @@ impl PluginsManager {
Ok(cache) => cache,
Err(err) => err.into_inner(),
};
let plugins = cache.as_ref()?;
Some(crate::remote::group_remote_installed_plugins_by_marketplaces(plugins, visible_scopes))
let cache = cache.as_ref()?;
if !visible_scopes
.iter()
.all(|scope| cache.scopes.contains(scope))
{
return None;
}
Some(
crate::remote::group_remote_installed_plugins_by_marketplaces(
&cache.plugins,
visible_scopes,
),
)
}
pub fn cached_global_remote_discoverable_plugins_for_config(
@@ -644,29 +692,42 @@ 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(
if visible_scopes.is_empty() {
return Ok(Vec::new());
}
let plugins = crate::remote::fetch_remote_installed_plugins_for_scopes(
&remote_plugin_service_config(config),
auth,
visible_scopes,
)
.await?;
let marketplaces =
crate::remote::group_remote_installed_plugins_by_marketplaces(&plugins, visible_scopes);
let changed = self.write_remote_installed_plugins_cache(plugins);
let changed = self.write_remote_installed_plugins_cache(visible_scopes, plugins);
if changed && let Some(on_effective_plugins_changed) = on_effective_plugins_changed {
on_effective_plugins_changed();
}
Ok(marketplaces)
}
fn write_remote_installed_plugins_cache(&self, plugins: Vec<RemoteInstalledPlugin>) -> bool {
fn write_remote_installed_plugins_cache(
&self,
scopes: &[RemotePluginScope],
plugins: Vec<RemoteInstalledPlugin>,
) -> bool {
let mut scopes = scopes.to_vec();
scopes.sort_unstable();
scopes.dedup();
let cached_plugins = CachedRemoteInstalledPlugins { scopes, plugins };
let mut cache = match self.remote_installed_plugins_cache.write() {
Ok(cache) => cache,
Err(err) => err.into_inner(),
};
if cache.as_ref().is_some_and(|cache| cache.eq(&plugins)) {
if cache.as_ref().is_some_and(|cache| cache == &cached_plugins) {
return false;
}
*cache = Some(plugins);
*cache = Some(cached_plugins);
drop(cache);
self.clear_enabled_outcome_cache();
true
@@ -721,7 +782,8 @@ impl PluginsManager {
notify: RemoteInstalledPluginsCacheRefreshNotify,
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
) {
if !config.plugins_enabled {
let scopes = config.remote_installed_plugin_visible_scopes();
if !config.plugins_enabled || scopes.is_empty() {
return;
}
@@ -729,6 +791,7 @@ impl PluginsManager {
RemoteInstalledPluginsCacheRefreshRequest {
service_config: remote_plugin_service_config(config),
auth,
scopes,
notify,
on_effective_plugins_changed,
},
@@ -741,7 +804,8 @@ impl PluginsManager {
auth: Option<CodexAuth>,
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
) {
if !config.plugins_enabled {
let scopes = config.remote_installed_plugin_visible_scopes();
if !config.plugins_enabled || scopes.is_empty() {
return;
}
@@ -760,6 +824,7 @@ impl PluginsManager {
self.codex_home.clone(),
remote_plugin_service_config(config),
auth,
scopes,
Some(on_local_cache_changed),
);
}
@@ -1856,16 +1921,18 @@ impl PluginsManager {
}
};
let installed_plugins = crate::remote::fetch_remote_installed_plugins(
let installed_plugins = crate::remote::fetch_remote_installed_plugins_for_scopes(
&request.service_config,
request.auth.as_ref(),
&request.scopes,
)
.await;
match installed_plugins {
Ok(installed_plugins) => {
// 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 changed = self
.write_remote_installed_plugins_cache(&request.scopes, installed_plugins);
let should_notify = changed
|| matches!(
request.notify,
+97 -16
View File
@@ -137,8 +137,15 @@ fn remote_installed_linear_plugin() -> RemoteInstalledPlugin {
}
fn remote_installed_plugin(name: &str) -> RemoteInstalledPlugin {
remote_installed_plugin_in_marketplace(name, "openai-curated-remote")
}
fn remote_installed_plugin_in_marketplace(
name: &str,
marketplace_name: &str,
) -> RemoteInstalledPlugin {
RemoteInstalledPlugin {
marketplace_name: "openai-curated-remote".to_string(),
marketplace_name: marketplace_name.to_string(),
id: format!("plugins~Plugin_{name}"),
name: name.to_string(),
enabled: true,
@@ -362,14 +369,17 @@ remote_plugin = true
let config = load_config(codex_home.path(), codex_home.path()).await;
let manager = PluginsManager::new(codex_home.path().to_path_buf());
manager.write_remote_installed_plugins_cache(vec![remote_installed_linear_plugin()]);
manager.write_remote_installed_plugins_cache(
&[RemotePluginScope::Global],
vec![remote_installed_linear_plugin()],
);
let outcome = manager.plugins_for_config(&config).await;
assert_eq!(outcome, PluginLoadOutcome::default());
}
#[tokio::test]
async fn remote_installed_cache_prefers_local_curated_conflicts_when_remote_plugin_disabled() {
async fn remote_installed_cache_omits_global_remote_plugins_when_remote_plugin_disabled() {
let codex_home = TempDir::new().unwrap();
write_file(
&codex_home.path().join(CONFIG_TOML_FILE),
@@ -391,10 +401,13 @@ enabled = true
let config = load_config(codex_home.path(), codex_home.path()).await;
let manager = PluginsManager::new(codex_home.path().to_path_buf());
manager.write_remote_installed_plugins_cache(vec![
remote_installed_plugin("linear"),
remote_installed_plugin("remote-only"),
]);
manager.write_remote_installed_plugins_cache(
&[RemotePluginScope::Global],
vec![
remote_installed_plugin("linear"),
remote_installed_plugin("remote-only"),
],
);
let outcome = manager.plugins_for_config(&config).await;
assert_eq!(
@@ -406,11 +419,44 @@ enabled = true
vec![
"calendar@openai-curated".to_string(),
"linear@openai-curated".to_string(),
"remote-only@openai-curated-remote".to_string(),
]
);
}
#[tokio::test]
async fn remote_installed_cache_includes_workspace_plugins_when_plugin_sharing_enabled() {
let codex_home = TempDir::new().unwrap();
write_file(
&codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = false
plugin_sharing = true
"#,
);
write_cached_plugin(codex_home.path(), "workspace-directory", "workspace-only");
let config = load_config(codex_home.path(), codex_home.path()).await;
let manager = PluginsManager::new(codex_home.path().to_path_buf());
manager.write_remote_installed_plugins_cache(
&[RemotePluginScope::Workspace],
vec![remote_installed_plugin_in_marketplace(
"workspace-only",
"workspace-directory",
)],
);
let outcome = manager.plugins_for_config(&config).await;
assert_eq!(
outcome
.plugins()
.iter()
.map(|plugin| plugin.config_name.clone())
.collect::<Vec<_>>(),
vec!["workspace-only@workspace-directory".to_string()]
);
}
#[tokio::test]
async fn remote_installed_cache_prefers_remote_curated_conflicts_when_remote_plugin_enabled() {
let codex_home = TempDir::new().unwrap();
@@ -434,10 +480,13 @@ enabled = true
let config = load_config(codex_home.path(), codex_home.path()).await;
let manager = PluginsManager::new(codex_home.path().to_path_buf());
manager.write_remote_installed_plugins_cache(vec![
remote_installed_plugin("linear"),
remote_installed_plugin("remote-only"),
]);
manager.write_remote_installed_plugins_cache(
&[RemotePluginScope::Global],
vec![
remote_installed_plugin("linear"),
remote_installed_plugin("remote-only"),
],
);
let outcome = manager.plugins_for_config(&config).await;
assert_eq!(
@@ -454,6 +503,39 @@ enabled = true
);
}
#[tokio::test]
async fn remote_installed_cache_respects_local_disabled_remote_plugin_config() {
let codex_home = TempDir::new().unwrap();
write_file(
&codex_home.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
remote_plugin = true
[plugins."remote-only@openai-curated-remote"]
enabled = false
"#,
);
write_cached_plugin(codex_home.path(), "openai-curated-remote", "remote-only");
let config = load_config(codex_home.path(), codex_home.path()).await;
let manager = PluginsManager::new(codex_home.path().to_path_buf());
manager.write_remote_installed_plugins_cache(
&[RemotePluginScope::Global],
vec![remote_installed_plugin("remote-only")],
);
let outcome = manager.plugins_for_config(&config).await;
assert_eq!(
outcome
.plugins()
.iter()
.map(|plugin| (plugin.config_name.clone(), plugin.enabled))
.collect::<Vec<_>>(),
vec![("remote-only@openai-curated-remote".to_string(), false)]
);
}
#[tokio::test]
async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metadata() {
let codex_home = TempDir::new().unwrap();
@@ -481,7 +563,7 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad
screenshot_urls: Vec::new(),
});
plugin.keywords = vec!["issues".to_string()];
manager.write_remote_installed_plugins_cache(vec![plugin]);
manager.write_remote_installed_plugins_cache(&[RemotePluginScope::Global], vec![plugin]);
let marketplaces = manager
.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Global])
@@ -521,9 +603,8 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad
);
assert_eq!(
manager
.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Workspace])
.expect("remote installed cache should be present"),
Vec::new()
.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Workspace]),
None
);
}
+15 -20
View File
@@ -360,7 +360,7 @@ impl RemotePluginScope {
}
}
fn from_marketplace_name(name: &str) -> Option<Self> {
pub(crate) fn from_marketplace_name(name: &str) -> Option<Self> {
match name {
REMOTE_GLOBAL_MARKETPLACE_NAME => Some(Self::Global),
REMOTE_WORKSPACE_MARKETPLACE_NAME
@@ -781,34 +781,29 @@ fn build_remote_marketplace(
}))
}
pub(crate) async fn fetch_remote_installed_plugins(
pub(crate) async fn fetch_remote_installed_plugins_for_scopes(
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
scopes: &[RemotePluginScope],
) -> Result<Vec<RemoteInstalledPlugin>, 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 mut scopes = scopes.to_vec();
scopes.sort_unstable();
scopes.dedup();
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| {
let mut remote_installed_plugins = Vec::new();
for scope in scopes {
let installed_items = fetch_installed_plugins_for_scope(config, auth, scope).await?;
for plugin in installed_items {
remote_installed_plugins.push(remote_installed_plugin_to_cache_entry(&plugin)?);
}
}
remote_installed_plugins.sort_by(|left, right| {
left.marketplace_name
.cmp(&right.marketplace_name)
.then_with(|| left.id.cmp(&right.id))
});
Ok(installed_plugins)
Ok(remote_installed_plugins)
}
pub fn group_remote_installed_plugins_by_marketplaces(
@@ -82,11 +82,15 @@ pub(crate) fn maybe_start_remote_installed_plugin_bundle_sync(
codex_home: PathBuf,
config: RemotePluginServiceConfig,
auth: Option<CodexAuth>,
scopes: Vec<RemotePluginScope>,
on_local_cache_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
) {
let Some(auth) = auth else {
return;
};
if scopes.is_empty() {
return;
}
let key = RemoteInstalledPluginBundleSyncKey {
plugin_cache_root: remote_plugin_cache_root(&codex_home),
};
@@ -96,7 +100,8 @@ 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_once(codex_home, &config, Some(&auth), &scopes)
.await;
match result {
Ok(outcome) => {
if outcome.changed_local_cache()
@@ -126,51 +131,32 @@ pub async fn sync_remote_installed_plugin_bundles_once(
codex_home: PathBuf,
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
scopes: &[RemotePluginScope],
) -> Result<RemoteInstalledPluginBundleSyncOutcome, RemoteInstalledPluginBundleSyncError> {
if scopes.is_empty() {
return Ok(RemoteInstalledPluginBundleSyncOutcome::default());
}
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 (global, workspace) = tokio::try_join!(global, workspace)?;
let store = PluginStore::try_new(codex_home.clone())?;
let mut scopes = scopes.to_vec();
scopes.sort_unstable();
scopes.dedup();
let mut installed_plugin_names_by_marketplace =
BTreeMap::<String, BTreeSet<String>>::from_iter([
(REMOTE_GLOBAL_MARKETPLACE_NAME.to_string(), BTreeSet::new()),
(
REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
(
REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
(
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
(
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME.to_string(),
BTreeSet::new(),
),
]);
installed_plugin_names_by_marketplace_for_scopes(scopes.iter().copied());
let mut installed_plugins_by_scope = Vec::new();
for scope in scopes {
let installed_plugins = fetch_installed_plugins_for_scope_with_download_url(
config, auth, scope, /*include_download_urls*/ true,
)
.await?;
installed_plugins_by_scope.push(installed_plugins);
}
let store = PluginStore::try_new(codex_home.clone())?;
let mut installed_plugin_ids = BTreeSet::new();
let mut failed_remote_plugin_ids = BTreeSet::new();
for (_scope, installed_plugins) in [global, workspace] {
for installed_plugins in installed_plugins_by_scope {
for installed_plugin in installed_plugins {
let plugin = installed_plugin.plugin;
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
@@ -306,21 +292,11 @@ fn remove_stale_remote_plugin_caches(
installed_plugin_names_by_marketplace: &BTreeMap<String, BTreeSet<String>>,
) -> Result<Vec<String>, String> {
let mut removed_cache_plugin_ids = Vec::new();
for marketplace_name in [
REMOTE_GLOBAL_MARKETPLACE_NAME,
REMOTE_WORKSPACE_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME,
] {
for (marketplace_name, installed_plugin_names) in installed_plugin_names_by_marketplace {
let marketplace_root = codex_home.join(PLUGINS_CACHE_DIR).join(marketplace_name);
if !marketplace_root.exists() {
continue;
}
let installed_plugin_names = installed_plugin_names_by_marketplace
.get(marketplace_name)
.cloned()
.unwrap_or_default();
for entry in fs::read_dir(&marketplace_root).map_err(|err| {
format!(
"failed to read remote plugin cache directory {}: {err}",
@@ -375,6 +351,34 @@ fn remove_stale_remote_plugin_caches(
Ok(removed_cache_plugin_ids)
}
fn installed_plugin_names_by_marketplace_for_scopes(
scopes: impl IntoIterator<Item = RemotePluginScope>,
) -> BTreeMap<String, BTreeSet<String>> {
let mut installed_plugin_names_by_marketplace = BTreeMap::<String, BTreeSet<String>>::new();
for scope in scopes {
match scope {
RemotePluginScope::Global => {
installed_plugin_names_by_marketplace
.entry(REMOTE_GLOBAL_MARKETPLACE_NAME.to_string())
.or_default();
}
RemotePluginScope::Workspace => {
for marketplace_name in [
REMOTE_WORKSPACE_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME,
REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME,
] {
installed_plugin_names_by_marketplace
.entry(marketplace_name.to_string())
.or_default();
}
}
}
}
installed_plugin_names_by_marketplace
}
fn remote_plugin_cache_root(codex_home: &Path) -> PathBuf {
codex_home.join(PLUGINS_CACHE_DIR)
}
@@ -118,6 +118,11 @@ pub(crate) async fn load_plugins_config(codex_home: &Path, cwd: &Path) -> Plugin
"remote_plugin",
/*default_enabled*/ false,
),
feature_enabled(
&effective_config,
"plugin_sharing",
/*default_enabled*/ true,
),
"https://chatgpt.com/backend-api/".to_string(),
)
}
+1
View File
@@ -1368,6 +1368,7 @@ impl Config {
self.config_layer_stack.clone(),
self.features.enabled(Feature::Plugins),
self.features.enabled(Feature::RemotePlugin),
self.features.enabled(Feature::PluginSharing),
self.chatgpt_base_url.clone(),
)
}
+12 -14
View File
@@ -344,20 +344,18 @@ remote_plugin = true
.all(|plugin| plugin.id != "github@openai-curated-remote")
);
for scope in ["GLOBAL", "WORKSPACE"] {
Mock::given(method("GET"))
.and(path("/backend-api/ps/plugins/installed"))
.and(query_param("scope", scope))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"plugins": [],
"pagination": {
"next_page_token": null
}
})))
.expect(1)
.mount(&server)
.await;
}
Mock::given(method("GET"))
.and(path("/backend-api/ps/plugins/installed"))
.and(query_param("scope", "GLOBAL"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"plugins": [],
"pagination": {
"next_page_token": null
}
})))
.expect(1)
.mount(&server)
.await;
plugins_manager
.build_and_cache_remote_installed_plugin_marketplaces(
&config.plugins_config_input(),