Require remote plugin detail before uninstall (#19966)

## Summary
- Fetch remote plugin detail before sending the uninstall request.
- Use the detail response to derive the marketplace namespace and plugin
name for cache cleanup.
- Stop the uninstall before the backend POST if detail lookup fails, so
backend state and local cache state do not diverge.

## Testing
- `just fmt`
- `cargo test -p codex-app-server plugin_uninstall`
- `cargo test -p codex-core-plugins`
- `git diff --check`
This commit is contained in:
xli-oai
2026-04-29 14:01:11 -07:00
committed by GitHub
Unverified
parent 973c5c823e
commit afbddabc8b
2 changed files with 53 additions and 125 deletions
+39 -85
View File
@@ -13,10 +13,8 @@ use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use tracing::warn;
pub const REMOTE_GLOBAL_MARKETPLACE_NAME: &str = "chatgpt-global";
pub const REMOTE_WORKSPACE_MARKETPLACE_NAME: &str = "chatgpt-workspace";
@@ -476,27 +474,6 @@ async fn fetch_remote_plugin_detail_with_download_url_option(
.await
}
async fn fetch_remote_plugin_detail_by_id(
config: &RemotePluginServiceConfig,
auth: &CodexAuth,
plugin_id: &str,
) -> Result<RemotePluginDetail, RemotePluginCatalogError> {
let plugin = fetch_plugin_detail(
config, auth, plugin_id, /*include_download_urls*/ false,
)
.await?;
let scope = plugin.scope;
build_remote_plugin_detail(
config,
auth,
scope,
scope.marketplace_name().to_string(),
plugin_id,
plugin,
)
.await
}
async fn build_remote_plugin_detail(
config: &RemotePluginServiceConfig,
auth: &CodexAuth,
@@ -589,6 +566,12 @@ pub async fn uninstall_remote_plugin(
plugin_id: &str,
) -> Result<(), RemotePluginCatalogError> {
let auth = ensure_chatgpt_auth(auth)?;
let plugin = fetch_plugin_detail(
config, auth, plugin_id, /*include_download_urls*/ false,
)
.await?;
let marketplace_name = plugin.scope.marketplace_name().to_string();
let plugin_name = plugin.name;
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/plugins/{plugin_id}/uninstall");
@@ -609,19 +592,9 @@ pub async fn uninstall_remote_plugin(
});
}
let remote_detail = match fetch_remote_plugin_detail_by_id(config, auth, plugin_id).await {
Ok(remote_detail) => Some(remote_detail),
Err(err) => {
warn!(
plugin_id,
"failed to read remote plugin details after uninstall; skipping named cache removal: {err}"
);
None
}
};
let legacy_plugin_id = plugin_id.to_string();
tokio::task::spawn_blocking(move || {
remove_remote_plugin_cache(codex_home, remote_detail, legacy_plugin_id)
remove_remote_plugin_cache(codex_home, marketplace_name, plugin_name, legacy_plugin_id)
})
.await
.map_err(|err| {
@@ -636,65 +609,46 @@ pub async fn uninstall_remote_plugin(
fn remove_remote_plugin_cache(
codex_home: PathBuf,
remote_detail: Option<RemotePluginDetail>,
marketplace_name: String,
plugin_name: String,
legacy_plugin_id: String,
) -> Result<(), String> {
if let Some(remote_detail) = remote_detail {
let marketplace_name = remote_detail.marketplace_name;
let plugin_name = remote_detail.summary.name;
let store = PluginStore::try_new(codex_home.clone())
.map_err(|err| format!("failed to resolve remote plugin cache root: {err}"))?;
let plugin_id = PluginId::new(plugin_name.clone(), marketplace_name.clone()).map_err(
|err| {
format!(
"invalid remote plugin cache id for `{plugin_name}` in `{marketplace_name}`: {err}"
)
},
)?;
let plugin_cache_root = store.plugin_base_root(&plugin_id);
store.uninstall(&plugin_id).map_err(|err| {
let store = PluginStore::try_new(codex_home.clone())
.map_err(|err| format!("failed to resolve remote plugin cache root: {err}"))?;
let plugin_id =
PluginId::new(plugin_name.clone(), marketplace_name.clone()).map_err(|err| {
format!(
"failed to remove remote plugin cache entry {}: {err}",
plugin_cache_root.display()
"invalid remote plugin cache id for `{plugin_name}` in `{marketplace_name}`: {err}"
)
})?;
let legacy_remote_plugin_cache_root = codex_home
.join(PLUGINS_CACHE_DIR)
.join(marketplace_name)
.join(legacy_plugin_id);
if legacy_remote_plugin_cache_root != plugin_cache_root.as_path() {
remove_path_if_exists(&legacy_remote_plugin_cache_root)?;
}
return Ok(());
}
for scope in RemotePluginScope::all() {
let legacy_remote_plugin_cache_root = codex_home
.join(PLUGINS_CACHE_DIR)
.join(scope.marketplace_name())
.join(&legacy_plugin_id);
remove_path_if_exists(&legacy_remote_plugin_cache_root)?;
}
Ok(())
}
fn remove_path_if_exists(path: &Path) -> Result<(), String> {
if !path.exists() {
return Ok(());
}
let result = if path.is_dir() {
fs::remove_dir_all(path)
} else {
fs::remove_file(path)
};
result.map_err(|err| {
let plugin_cache_root = store.plugin_base_root(&plugin_id);
store.uninstall(&plugin_id).map_err(|err| {
format!(
"failed to remove remote plugin cache entry {}: {err}",
path.display()
plugin_cache_root.display()
)
})
})?;
let legacy_remote_plugin_cache_root = codex_home
.join(PLUGINS_CACHE_DIR)
.join(marketplace_name)
.join(legacy_plugin_id);
if legacy_remote_plugin_cache_root != plugin_cache_root.as_path()
&& legacy_remote_plugin_cache_root.exists()
{
let result = if legacy_remote_plugin_cache_root.is_dir() {
fs::remove_dir_all(&legacy_remote_plugin_cache_root)
} else {
fs::remove_file(&legacy_remote_plugin_cache_root)
};
result.map_err(|err| {
format!(
"failed to remove remote plugin cache entry {}: {err}",
legacy_remote_plugin_cache_root.display()
)
})?;
}
Ok(())
}
fn build_remote_plugin_summary(