mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix: Deduplicate installed local and remote curated plugins (#25681)
## Summary - Deduplicate installed `openai-curated` and `openai-curated-remote` plugin conflicts by feature flag. - Prefer remote when remote plugins are enabled; otherwise prefer local, while preserving one-sided installs. ## Testing - `just fmt` - `git diff --check` - Targeted `just test` was blocked locally because `cargo-nextest` is not installed.
This commit is contained in:
@@ -302,7 +302,6 @@ use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_core::windows_sandbox::WindowsSandboxSetupMode as CoreWindowsSandboxSetupMode;
|
||||
use codex_core::windows_sandbox::WindowsSandboxSetupRequest;
|
||||
use codex_core::windows_sandbox::sandbox_setup_is_complete;
|
||||
use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::PluginInstallError as CorePluginInstallError;
|
||||
use codex_core_plugins::PluginInstallRequest;
|
||||
use codex_core_plugins::PluginLoadOutcome;
|
||||
|
||||
@@ -6,6 +6,8 @@ use codex_app_server_protocol::PluginInstallPolicy;
|
||||
use codex_app_server_protocol::PluginSharePrincipalRole;
|
||||
use codex_app_server_protocol::PluginShareTargetRole;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::RemotePluginScope;
|
||||
use codex_core_plugins::remote::is_valid_remote_plugin_id;
|
||||
use codex_core_plugins::remote::validate_remote_plugin_id;
|
||||
@@ -156,6 +158,52 @@ fn remote_installed_plugin_visible_scopes(config: &Config) -> Vec<RemotePluginSc
|
||||
scopes
|
||||
}
|
||||
|
||||
fn filter_openai_curated_installed_conflicts(
|
||||
marketplaces: &mut Vec<PluginMarketplaceEntry>,
|
||||
prefer_remote_curated_conflicts: bool,
|
||||
) {
|
||||
let local_installed_plugin_names = marketplaces
|
||||
.iter()
|
||||
.find(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME)
|
||||
.map(|marketplace| installed_plugin_names(&marketplace.plugins))
|
||||
.unwrap_or_default();
|
||||
let remote_installed_plugin_names = marketplaces
|
||||
.iter()
|
||||
.find(|marketplace| marketplace.name == REMOTE_GLOBAL_MARKETPLACE_NAME)
|
||||
.map(|marketplace| installed_plugin_names(&marketplace.plugins))
|
||||
.unwrap_or_default();
|
||||
let conflicting_plugin_names = local_installed_plugin_names
|
||||
.intersection(&remote_installed_plugin_names)
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
if conflicting_plugin_names.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let marketplace_to_filter = if prefer_remote_curated_conflicts {
|
||||
OPENAI_CURATED_MARKETPLACE_NAME
|
||||
} else {
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME
|
||||
};
|
||||
for marketplace in marketplaces.iter_mut() {
|
||||
if marketplace.name != marketplace_to_filter {
|
||||
continue;
|
||||
}
|
||||
marketplace
|
||||
.plugins
|
||||
.retain(|plugin| !plugin.installed || !conflicting_plugin_names.contains(&plugin.name));
|
||||
}
|
||||
marketplaces.retain(|marketplace| !marketplace.plugins.is_empty());
|
||||
}
|
||||
|
||||
fn installed_plugin_names(plugins: &[PluginSummary]) -> HashSet<String> {
|
||||
plugins
|
||||
.iter()
|
||||
.filter(|plugin| plugin.installed)
|
||||
.map(|plugin| plugin.name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn remote_plugin_share_discoverability(
|
||||
discoverability: PluginShareDiscoverability,
|
||||
) -> codex_core_plugins::remote::RemotePluginShareDiscoverability {
|
||||
@@ -740,6 +788,10 @@ impl PluginRequestProcessor {
|
||||
)
|
||||
.await,
|
||||
);
|
||||
filter_openai_curated_installed_conflicts(
|
||||
&mut data,
|
||||
config.features.enabled(Feature::RemotePlugin),
|
||||
);
|
||||
|
||||
Ok(PluginInstalledResponse {
|
||||
marketplaces: data,
|
||||
|
||||
@@ -180,6 +180,107 @@ enabled = true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled() -> Result<()>
|
||||
{
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_openai_curated_marketplace(codex_home.path(), &["linear", "calendar"])?;
|
||||
write_installed_plugin(&codex_home, "openai-curated", "linear")?;
|
||||
write_installed_plugin(&codex_home, "openai-curated", "calendar")?;
|
||||
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
|
||||
|
||||
[plugins."linear@openai-curated"]
|
||||
enabled = true
|
||||
|
||||
[plugins."calendar@openai-curated"]
|
||||
enabled = true
|
||||
"#,
|
||||
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 mut global_installed_body: serde_json::Value = serde_json::from_str(
|
||||
&remote_installed_plugin_body("", "1.2.3", /*enabled*/ true),
|
||||
)?;
|
||||
let mut remote_only = global_installed_body["plugins"][0].clone();
|
||||
remote_only["id"] = serde_json::json!("plugins~Plugin_11111111111111111111111111111111");
|
||||
remote_only["name"] = serde_json::json!("remote-only");
|
||||
remote_only["release"]["display_name"] = serde_json::json!("Remote Only");
|
||||
global_installed_body["plugins"]
|
||||
.as_array_mut()
|
||||
.expect("installed plugins should be an array")
|
||||
.push(remote_only);
|
||||
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;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_installed_request(PluginInstalledParams {
|
||||
cwds: None,
|
||||
install_suggestion_plugin_names: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginInstalledResponse = to_response(response)?;
|
||||
|
||||
let local_marketplace = response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.find(|marketplace| marketplace.name == "openai-curated")
|
||||
.expect("expected openai-curated marketplace entry");
|
||||
assert_eq!(
|
||||
local_marketplace
|
||||
.plugins
|
||||
.iter()
|
||||
.map(|plugin| plugin.id.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["calendar@openai-curated".to_string()]
|
||||
);
|
||||
let remote_marketplace = response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.find(|marketplace| marketplace.name == "openai-curated-remote")
|
||||
.expect("expected openai-curated-remote marketplace entry");
|
||||
assert_eq!(
|
||||
remote_marketplace
|
||||
.plugins
|
||||
.iter()
|
||||
.map(|plugin| plugin.id.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
"linear@openai-curated-remote".to_string(),
|
||||
"remote-only@openai-curated-remote".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(response.marketplace_load_errors, Vec::new());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_installed_ignores_local_cache_without_catalog() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
Reference in New Issue
Block a user