[codex] Add installed-plugin mention API (#22448)

## Summary
- add app-server `plugin/installed` for mention-oriented plugin loading
- return installed plugins plus explicitly requested install-suggestion
rows
- keep remote handling on installed-state data instead of the broad
catalog listing path

## Why
The `@` mention surface only needs plugins that are usable now, plus a
small product-approved set of install suggestions. It does not need the
full catalog-shaped `plugin/list` payload that the Plugins page uses.

## Validation
- `just write-app-server-schema`
- `just fmt`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-core-plugins`
- `cargo test -p codex-app-server --test all plugin_installed_`

## Notes
- The package-wide `cargo test -p codex-app-server` run still hits an
existing unrelated stack overflow in
`in_process::tests::in_process_start_clamps_zero_channel_capacity`.
- Companion webview PR: https://github.com/openai/openai/pull/915672
This commit is contained in:
xli-oai
2026-05-18 03:11:54 -07:00
committed by GitHub
parent 22dd9ad392
commit da14dd2add
22 changed files with 1609 additions and 82 deletions
@@ -1102,6 +1102,9 @@ impl MessageProcessor {
ClientRequest::PluginList { params, .. } => {
self.plugin_processor.plugin_list(params).await
}
ClientRequest::PluginInstalled { params, .. } => {
self.plugin_processor.plugin_installed(params).await
}
ClientRequest::PluginRead { params, .. } => {
self.plugin_processor.plugin_read(params).await
}
@@ -107,6 +107,8 @@ use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::PluginDetail;
use codex_app_server_protocol::PluginInstallParams;
use codex_app_server_protocol::PluginInstallResponse;
use codex_app_server_protocol::PluginInstalledParams;
use codex_app_server_protocol::PluginInstalledResponse;
use codex_app_server_protocol::PluginInterface;
use codex_app_server_protocol::PluginListMarketplaceKind;
use codex_app_server_protocol::PluginListParams;
@@ -6,6 +6,7 @@ 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::remote::RemotePluginScope;
use codex_core_plugins::remote::is_valid_remote_plugin_id;
use codex_core_plugins::remote::validate_remote_plugin_id;
use codex_mcp::McpOAuthLoginSupport;
@@ -122,6 +123,39 @@ fn share_context_for_source(
}
}
fn convert_configured_marketplace_plugin_to_plugin_summary(
plugin: codex_core_plugins::ConfiguredMarketplacePlugin,
shared_plugin_ids_by_local_path: &std::collections::BTreeMap<AbsolutePathBuf, String>,
) -> PluginSummary {
let share_context = share_context_for_source(&plugin.source, shared_plugin_ids_by_local_path);
PluginSummary {
id: plugin.id,
remote_plugin_id: None,
local_version: plugin.local_version,
installed: plugin.installed,
enabled: plugin.enabled,
name: plugin.name,
share_context,
source: marketplace_plugin_source_to_info(plugin.source),
install_policy: plugin.policy.installation.into(),
auth_policy: plugin.policy.authentication.into(),
availability: PluginAvailability::Available,
interface: plugin.interface.map(local_plugin_interface_to_info),
keywords: plugin.keywords,
}
}
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 remote_plugin_share_discoverability(
discoverability: PluginShareDiscoverability,
) -> codex_core_plugins::remote::RemotePluginShareDiscoverability {
@@ -268,6 +302,15 @@ impl PluginRequestProcessor {
.map(|response| Some(response.into()))
}
pub(crate) async fn plugin_installed(
&self,
params: PluginInstalledParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.plugin_installed_response(params)
.await
.map(|response| Some(response.into()))
}
pub(crate) async fn plugin_read(
&self,
params: PluginReadParams,
@@ -487,27 +530,10 @@ impl PluginRequestProcessor {
.plugins
.into_iter()
.map(|plugin| {
let share_context = share_context_for_source(
&plugin.source,
convert_configured_marketplace_plugin_to_plugin_summary(
plugin,
&shared_plugin_ids_by_local_path,
);
PluginSummary {
id: plugin.id,
remote_plugin_id: None,
local_version: plugin.local_version,
installed: plugin.installed,
enabled: plugin.enabled,
name: plugin.name,
share_context,
source: marketplace_plugin_source_to_info(plugin.source),
install_policy: plugin.policy.installation.into(),
auth_policy: plugin.policy.authentication.into(),
availability: PluginAvailability::Available,
interface: plugin
.interface
.map(local_plugin_interface_to_info),
keywords: plugin.keywords,
}
)
})
.collect(),
})
@@ -632,6 +658,193 @@ impl PluginRequestProcessor {
})
}
async fn plugin_installed_response(
&self,
params: PluginInstalledParams,
) -> Result<PluginInstalledResponse, JSONRPCErrorError> {
let plugins_manager = self.thread_manager.plugins_manager();
let PluginInstalledParams {
cwds,
install_suggestion_plugin_names,
} = params;
let roots = cwds.unwrap_or_default();
let install_suggestion_plugin_names = install_suggestion_plugin_names
.unwrap_or_default()
.into_iter()
.collect::<HashSet<_>>();
let empty_response = || PluginInstalledResponse {
marketplaces: Vec::new(),
marketplace_load_errors: Vec::new(),
};
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
if !config.features.enabled(Feature::Plugins) {
return Ok(empty_response());
}
let auth = self.auth_manager.auth().await;
if !self
.workspace_codex_plugins_enabled(&config, auth.as_ref())
.await
{
return Ok(empty_response());
}
let plugins_input = config.plugins_config_input();
let remote_installed_plugin_visible_scopes =
remote_installed_plugin_visible_scopes(&config);
plugins_manager.maybe_start_remote_installed_plugin_bundle_sync(
&plugins_input,
auth.clone(),
Some(self.effective_plugins_changed_callback()),
);
let (mut data, marketplace_load_errors) = self
.load_local_installed_and_suggested_plugins(
plugins_manager.clone(),
&config,
&plugins_input,
roots,
install_suggestion_plugin_names,
)
.await?;
data.extend(
self.load_remote_installed_plugins(
plugins_manager,
&plugins_input,
&remote_installed_plugin_visible_scopes,
auth.as_ref(),
)
.await,
);
Ok(PluginInstalledResponse {
marketplaces: data,
marketplace_load_errors,
})
}
async fn load_local_installed_and_suggested_plugins(
&self,
plugins_manager: Arc<codex_core_plugins::PluginsManager>,
config: &Config,
plugins_input: &codex_core_plugins::PluginsConfigInput,
roots: Vec<AbsolutePathBuf>,
install_suggestion_plugin_names: HashSet<String>,
) -> Result<
(
Vec<PluginMarketplaceEntry>,
Vec<codex_app_server_protocol::MarketplaceLoadErrorInfo>,
),
JSONRPCErrorError,
> {
let config_for_marketplace_listing = plugins_input.clone();
let shared_plugin_ids_by_local_path = load_shared_plugin_ids_by_local_path(config)?;
match tokio::task::spawn_blocking(move || {
let outcome = plugins_manager
.list_marketplaces_for_config(&config_for_marketplace_listing, &roots)?;
Ok::<
(
Vec<PluginMarketplaceEntry>,
Vec<codex_app_server_protocol::MarketplaceLoadErrorInfo>,
),
MarketplaceError,
>((
outcome
.marketplaces
.into_iter()
.filter_map(|marketplace| {
let plugins = marketplace
.plugins
.into_iter()
.filter(|plugin| {
plugin.installed
|| install_suggestion_plugin_names.contains(&plugin.name)
})
.map(|plugin| {
convert_configured_marketplace_plugin_to_plugin_summary(
plugin,
&shared_plugin_ids_by_local_path,
)
})
.collect::<Vec<_>>();
(!plugins.is_empty()).then_some(PluginMarketplaceEntry {
name: marketplace.name,
path: Some(marketplace.path),
interface: marketplace.interface.map(|interface| {
MarketplaceInterface {
display_name: interface.display_name,
}
}),
plugins,
})
})
.collect(),
outcome
.errors
.into_iter()
.map(|err| codex_app_server_protocol::MarketplaceLoadErrorInfo {
marketplace_path: err.path,
message: err.message,
})
.collect(),
))
})
.await
{
Ok(Ok(outcome)) => Ok(outcome),
Ok(Err(err)) => Err(Self::marketplace_error(
err,
"list installed and suggested marketplace plugins",
)),
Err(err) => Err(internal_error(format!(
"failed to list installed and suggested plugins: {err}"
))),
}
}
async fn load_remote_installed_plugins(
&self,
plugins_manager: Arc<codex_core_plugins::PluginsManager>,
plugins_input: &codex_core_plugins::PluginsConfigInput,
visible_scopes: &[RemotePluginScope],
auth: Option<&CodexAuth>,
) -> Vec<PluginMarketplaceEntry> {
let remote_marketplaces = if let Some(remote_marketplaces) =
plugins_manager.build_remote_installed_plugin_marketplaces_from_cache(visible_scopes)
{
Ok(remote_marketplaces)
} else {
plugins_manager
.build_and_cache_remote_installed_plugin_marketplaces(
plugins_input,
auth,
visible_scopes,
Some(self.effective_plugins_changed_callback()),
)
.await
};
match remote_marketplaces {
Ok(remote_marketplaces) => remote_marketplaces
.into_iter()
.map(remote_marketplace_to_info)
.collect(),
Err(
RemotePluginCatalogError::AuthRequired
| RemotePluginCatalogError::UnsupportedAuthMode,
) => Vec::new(),
Err(err) => {
warn!(
error = %err,
"plugin/installed remote installed plugin fetch failed; returning local marketplaces only"
);
Vec::new()
}
}
}
async fn plugin_read_response(
&self,
params: PluginReadParams,