mirror of
https://github.com/openai/codex.git
synced 2026-05-17 09:43:19 +00:00
codex: narrow plugin installed mentions change
This commit is contained in:
@@ -696,12 +696,6 @@ impl PluginRequestProcessor {
|
||||
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let remote_installed_plugin_scopes = remote_installed_plugin_scopes(&config);
|
||||
plugins_manager.maybe_start_remote_installed_plugin_bundle_sync(
|
||||
&plugins_input,
|
||||
auth.clone(),
|
||||
remote_installed_plugin_scopes.clone(),
|
||||
Some(self.effective_plugins_changed_callback()),
|
||||
);
|
||||
|
||||
let (mut data, marketplace_load_errors) = self
|
||||
.load_local_installed_and_suggested_plugins(
|
||||
|
||||
@@ -1951,82 +1951,6 @@ plugin_sharing = false
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_installed_starts_remote_installed_bundle_sync() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
format!(
|
||||
r#"chatgpt_base_url = "{}/backend-api/"
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
plugin_sharing = false
|
||||
"#,
|
||||
server.uri()
|
||||
),
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let bundle_url = mount_remote_plugin_bundle(
|
||||
&server,
|
||||
"linear",
|
||||
remote_plugin_bundle_tar_gz_bytes("linear")?,
|
||||
)
|
||||
.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;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let plugin_installed_request_id = mcp
|
||||
.send_plugin_installed_request(PluginInstalledParams {
|
||||
cwds: None,
|
||||
install_suggestion_plugin_names: None,
|
||||
})
|
||||
.await?;
|
||||
let response: PluginInstalledResponse = to_response(
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(plugin_installed_request_id)),
|
||||
)
|
||||
.await??,
|
||||
)?;
|
||||
|
||||
assert_eq!(response.marketplaces.len(), 1);
|
||||
assert_eq!(response.marketplaces[0].name, "chatgpt-global");
|
||||
assert_eq!(
|
||||
response.marketplaces[0]
|
||||
.plugins
|
||||
.iter()
|
||||
.map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![("linear@chatgpt-global".to_string(), true, true)]
|
||||
);
|
||||
let installed_path = codex_home
|
||||
.path()
|
||||
.join("plugins/cache/chatgpt-global/linear/1.2.3/.codex-plugin/plugin.json");
|
||||
wait_for_path_exists(&installed_path).await?;
|
||||
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
|
||||
assert_no_remote_installed_scope_request(&server, "WORKSPACE").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -429,7 +429,6 @@ pub struct PluginsManager {
|
||||
struct CachedPluginLoadOutcome {
|
||||
config_version: String,
|
||||
plugin_hooks_enabled: bool,
|
||||
remote_installed_plugin_scopes: Vec<RemotePluginScope>,
|
||||
outcome: PluginLoadOutcome,
|
||||
}
|
||||
|
||||
@@ -501,21 +500,17 @@ impl PluginsManager {
|
||||
}
|
||||
|
||||
let plugin_hooks_enabled = config.plugin_hooks_enabled;
|
||||
let remote_installed_plugin_scopes = config.remote_installed_plugin_scopes();
|
||||
let config_version = version_for_toml(&config.config_layer_stack.effective_config());
|
||||
if !force_reload
|
||||
&& let Some(outcome) = self.cached_enabled_outcome(
|
||||
&config_version,
|
||||
plugin_hooks_enabled,
|
||||
&remote_installed_plugin_scopes,
|
||||
)
|
||||
&& let Some(outcome) =
|
||||
self.cached_enabled_outcome(&config_version, plugin_hooks_enabled)
|
||||
{
|
||||
return outcome;
|
||||
}
|
||||
|
||||
let outcome = load_plugins_from_layer_stack(
|
||||
&config.config_layer_stack,
|
||||
self.remote_installed_plugin_configs(&remote_installed_plugin_scopes),
|
||||
self.remote_installed_plugin_configs(),
|
||||
&self.store,
|
||||
self.restriction_product,
|
||||
plugin_hooks_enabled,
|
||||
@@ -529,7 +524,6 @@ impl PluginsManager {
|
||||
*cache = Some(CachedPluginLoadOutcome {
|
||||
config_version,
|
||||
plugin_hooks_enabled,
|
||||
remote_installed_plugin_scopes,
|
||||
outcome: outcome.clone(),
|
||||
});
|
||||
outcome
|
||||
@@ -564,7 +558,7 @@ impl PluginsManager {
|
||||
}
|
||||
load_plugins_from_layer_stack(
|
||||
config_layer_stack,
|
||||
self.remote_installed_plugin_configs(&config.remote_installed_plugin_scopes()),
|
||||
self.remote_installed_plugin_configs(),
|
||||
&self.store,
|
||||
self.restriction_product,
|
||||
plugin_hooks_feature_enabled,
|
||||
@@ -587,7 +581,6 @@ impl PluginsManager {
|
||||
&self,
|
||||
config_version: &str,
|
||||
plugin_hooks_enabled: bool,
|
||||
remote_installed_plugin_scopes: &[RemotePluginScope],
|
||||
) -> Option<PluginLoadOutcome> {
|
||||
match self.cached_enabled_outcome.read() {
|
||||
Ok(cache) => cache
|
||||
@@ -595,7 +588,6 @@ impl PluginsManager {
|
||||
.filter(|cached| {
|
||||
cached.config_version == config_version
|
||||
&& cached.plugin_hooks_enabled == plugin_hooks_enabled
|
||||
&& cached.remote_installed_plugin_scopes == remote_installed_plugin_scopes
|
||||
})
|
||||
.map(|cached| cached.outcome.clone()),
|
||||
Err(err) => err
|
||||
@@ -604,16 +596,12 @@ impl PluginsManager {
|
||||
.filter(|cached| {
|
||||
cached.config_version == config_version
|
||||
&& cached.plugin_hooks_enabled == plugin_hooks_enabled
|
||||
&& cached.remote_installed_plugin_scopes == remote_installed_plugin_scopes
|
||||
})
|
||||
.map(|cached| cached.outcome.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_installed_plugin_configs(
|
||||
&self,
|
||||
scopes: &[RemotePluginScope],
|
||||
) -> HashMap<String, PluginConfig> {
|
||||
fn remote_installed_plugin_configs(&self) -> HashMap<String, PluginConfig> {
|
||||
let cache = match self.remote_installed_plugins_cache.read() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
@@ -622,16 +610,7 @@ impl PluginsManager {
|
||||
return HashMap::new();
|
||||
};
|
||||
|
||||
let plugins = cache
|
||||
.plugins
|
||||
.iter()
|
||||
.filter(|plugin| {
|
||||
remote_installed_scope_allows_marketplace(scopes, &plugin.marketplace_name)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
remote_installed_plugins_to_config(&plugins, &self.store)
|
||||
remote_installed_plugins_to_config(&cache.plugins, &self.store)
|
||||
}
|
||||
|
||||
pub fn build_remote_installed_plugin_marketplaces_from_cache(
|
||||
@@ -792,29 +771,23 @@ impl PluginsManager {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn maybe_start_remote_installed_plugin_bundle_sync(
|
||||
fn maybe_start_remote_installed_plugin_bundle_sync(
|
||||
self: &Arc<Self>,
|
||||
config: &PluginsConfigInput,
|
||||
auth: Option<CodexAuth>,
|
||||
scopes: Vec<RemotePluginScope>,
|
||||
on_effective_plugins_changed: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
|
||||
) {
|
||||
if !config.plugins_enabled {
|
||||
return;
|
||||
}
|
||||
if scopes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let manager = Arc::clone(self);
|
||||
let config_for_refresh = config.clone();
|
||||
let auth_for_refresh = auth.clone();
|
||||
let scopes_for_refresh = scopes.clone();
|
||||
let on_local_cache_changed = Arc::new(move || {
|
||||
manager.maybe_start_remote_installed_plugins_cache_refresh_after_mutation_for_scopes(
|
||||
manager.maybe_start_remote_installed_plugins_cache_refresh_after_mutation(
|
||||
&config_for_refresh,
|
||||
auth_for_refresh.clone(),
|
||||
scopes_for_refresh.clone(),
|
||||
on_effective_plugins_changed.clone(),
|
||||
);
|
||||
});
|
||||
@@ -823,7 +796,6 @@ impl PluginsManager {
|
||||
self.codex_home.clone(),
|
||||
remote_plugin_service_config(config),
|
||||
auth,
|
||||
scopes,
|
||||
Some(on_local_cache_changed),
|
||||
);
|
||||
}
|
||||
@@ -844,7 +816,6 @@ impl PluginsManager {
|
||||
self.maybe_start_remote_installed_plugin_bundle_sync(
|
||||
config,
|
||||
auth,
|
||||
config.remote_installed_plugin_scopes(),
|
||||
on_effective_plugins_changed,
|
||||
);
|
||||
}
|
||||
@@ -1637,7 +1608,6 @@ impl PluginsManager {
|
||||
manager.maybe_start_remote_installed_plugin_bundle_sync(
|
||||
&config_for_remote_sync,
|
||||
auth,
|
||||
config_for_remote_sync.remote_installed_plugin_scopes(),
|
||||
on_effective_plugins_changed,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -65,7 +65,6 @@ pub enum RemoteInstalledPluginBundleSyncError {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct RemoteInstalledPluginBundleSyncKey {
|
||||
plugin_cache_root: PathBuf,
|
||||
scopes: Vec<RemotePluginScope>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -79,35 +78,25 @@ pub struct RemotePluginCacheMutationGuard {
|
||||
key: RemotePluginCacheMutationKey,
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_start_remote_installed_plugin_bundle_sync(
|
||||
pub 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>>,
|
||||
) {
|
||||
if scopes.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(auth) = auth else {
|
||||
return;
|
||||
};
|
||||
let key = RemoteInstalledPluginBundleSyncKey {
|
||||
plugin_cache_root: remote_plugin_cache_root(&codex_home),
|
||||
scopes: scopes.clone(),
|
||||
};
|
||||
if !mark_remote_installed_plugin_bundle_sync_in_flight(key.clone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result = sync_remote_installed_plugin_bundles_once_for_scopes(
|
||||
codex_home,
|
||||
&config,
|
||||
Some(&auth),
|
||||
&scopes,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
sync_remote_installed_plugin_bundles_once(codex_home, &config, Some(&auth)).await;
|
||||
match result {
|
||||
Ok(outcome) => {
|
||||
if outcome.changed_local_cache()
|
||||
@@ -138,55 +127,50 @@ pub async fn sync_remote_installed_plugin_bundles_once(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Result<RemoteInstalledPluginBundleSyncOutcome, RemoteInstalledPluginBundleSyncError> {
|
||||
sync_remote_installed_plugin_bundles_once_for_scopes(
|
||||
codex_home,
|
||||
config,
|
||||
auth,
|
||||
&[RemotePluginScope::Global, RemotePluginScope::Workspace],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn sync_remote_installed_plugin_bundles_once_for_scopes(
|
||||
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 mut installed_plugins_by_scope = Vec::new();
|
||||
for scope in scopes {
|
||||
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,
|
||||
config, auth, scope, /*include_download_urls*/ true,
|
||||
)
|
||||
.await?;
|
||||
installed_plugins_by_scope.push((*scope, installed_plugins));
|
||||
}
|
||||
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 installed_plugin_names_by_marketplace = BTreeMap::<String, BTreeSet<String>>::new();
|
||||
if scopes.contains(&RemotePluginScope::Global) {
|
||||
installed_plugin_names_by_marketplace
|
||||
.insert(REMOTE_GLOBAL_MARKETPLACE_NAME.to_string(), BTreeSet::new());
|
||||
}
|
||||
if scopes.contains(&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
|
||||
.insert(marketplace_name.to_string(), BTreeSet::new());
|
||||
}
|
||||
}
|
||||
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(),
|
||||
),
|
||||
]);
|
||||
let mut installed_plugin_ids = BTreeSet::new();
|
||||
let mut failed_remote_plugin_ids = BTreeSet::new();
|
||||
|
||||
for (_scope, installed_plugins) in installed_plugins_by_scope {
|
||||
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();
|
||||
@@ -321,11 +305,21 @@ 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, installed_plugin_names) in installed_plugin_names_by_marketplace {
|
||||
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,
|
||||
] {
|
||||
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}",
|
||||
@@ -436,7 +430,6 @@ mod tests {
|
||||
let codex_home = tempfile::tempdir().expect("create codex home");
|
||||
let key = RemoteInstalledPluginBundleSyncKey {
|
||||
plugin_cache_root: remote_plugin_cache_root(codex_home.path()),
|
||||
scopes: vec![RemotePluginScope::Global, RemotePluginScope::Workspace],
|
||||
};
|
||||
|
||||
assert!(mark_remote_installed_plugin_bundle_sync_in_flight(
|
||||
|
||||
Reference in New Issue
Block a user