[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
@@ -625,6 +625,11 @@ client_request_definitions! {
serialization: None,
response: v2::PluginListResponse,
},
PluginInstalled => "plugin/installed" {
params: v2::PluginInstalledParams,
serialization: None,
response: v2::PluginInstalledResponse,
},
PluginRead => "plugin/read" {
params: v2::PluginReadParams,
serialization: None,
@@ -1719,6 +1724,15 @@ mod tests {
};
assert_eq!(plugin_read.serialization_scope(), None);
let plugin_installed = ClientRequest::PluginInstalled {
request_id: request_id(),
params: v2::PluginInstalledParams {
cwds: None,
install_suggestion_plugin_names: None,
},
};
assert_eq!(plugin_installed.serialization_scope(), None);
let plugin_uninstall = ClientRequest::PluginUninstall {
request_id: request_id(),
params: v2::PluginUninstallParams {
@@ -125,6 +125,19 @@ pub struct PluginListParams {
pub marketplace_kinds: Option<Vec<PluginListMarketplaceKind>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginInstalledParams {
/// Optional working directories used to discover repo marketplaces.
#[ts(optional = nullable)]
pub cwds: Option<Vec<AbsolutePathBuf>>,
/// Additional uninstalled plugin names that should be returned when present locally.
/// This is used by mention surfaces that intentionally expose install entrypoints.
#[ts(optional = nullable)]
pub install_suggestion_plugin_names: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[ts(export_to = "v2/")]
pub enum PluginListMarketplaceKind {
@@ -150,6 +163,15 @@ pub struct PluginListResponse {
pub featured_plugin_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginInstalledResponse {
pub marketplaces: Vec<PluginMarketplaceEntry>,
#[serde(default)]
pub marketplace_load_errors: Vec<MarketplaceLoadErrorInfo>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -2790,6 +2790,27 @@ fn plugin_list_params_serializes_marketplace_kind_filter() {
);
}
#[test]
fn plugin_installed_params_serializes_install_suggestion_names() {
assert_eq!(
serde_json::to_value(PluginInstalledParams {
cwds: None,
install_suggestion_plugin_names: Some(vec![
"computer-use".to_string(),
"chrome".to_string(),
]),
})
.unwrap(),
json!({
"cwds": null,
"installSuggestionPluginNames": [
"computer-use",
"chrome",
],
}),
);
}
#[test]
fn plugin_read_params_serialization_uses_install_source_fields() {
let marketplace_path = if cfg!(windows) {