Expose hosted plugin extensions in app-server summaries (#47263)

## What changed

- Request installed-plugin metadata with `includeExtensions=true` and carry optional `extensions` through the cache into `PluginSummary`.
- Add typed declarations for global, settings, thread, and file entrypoints, quick actions, settings tools, icons, and search mention providers. Update JSON schemas and TypeScript and Python bindings.
- Accept supported snake_case aliases and emit camelCase fields. Discard incompatible extension declarations without invalidating the installed-plugin inventory.

## Testing

Add an integration test covering extension metadata preservation, field normalization, and unknown entrypoint and quick-action types across live and cached `plugin/installed` reads. Update pagination assertions to require `includeExtensions=true`.

GitOrigin-RevId: b73b0d13c151a30dc1fb9cda70b5cd6d7e3e2441
This commit is contained in:
victor-openai
2026-09-22 11:29:58 +00:00
committed by copyberry
parent d3e5840932
commit a1f40f3f13
35 changed files with 2901 additions and 7 deletions
@@ -857,6 +857,7 @@ fn remote_installed_plugin_in_marketplace(
marketplace_name: &str,
) -> RemoteInstalledPlugin {
RemoteInstalledPlugin {
extensions: None,
canonical_app_id: None,
marketplace_name: marketplace_name.to_string(),
id: format!("plugins~Plugin_{name}"),
+16 -2
View File
@@ -14,6 +14,7 @@ use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::PluginAuthPolicy;
use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginDisabledReason;
use codex_app_server_protocol::PluginExtensions;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginInstallPolicySource;
use codex_app_server_protocol::PluginInterface;
@@ -38,6 +39,7 @@ use http::StatusCode;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
use serde_with::serde_as;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
@@ -242,6 +244,7 @@ pub struct RemoteInstalledPlugin {
pub version: Option<String>,
pub name: String,
pub canonical_app_id: Option<String>,
pub extensions: Option<PluginExtensions>,
pub installed_at: Option<DateTime<Utc>>,
pub enabled: bool,
pub install_policy: PluginInstallPolicy,
@@ -257,6 +260,7 @@ pub struct RemoteInstalledPlugin {
#[derive(Debug, Clone, PartialEq)]
pub struct RemotePluginSummary {
pub extensions: Option<PluginExtensions>,
pub id: String,
pub remote_plugin_id: String,
pub version: Option<String>,
@@ -761,10 +765,15 @@ struct RemotePluginDirectorySharePrincipal {
name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde_as]
#[derive(Debug, Clone, PartialEq, Deserialize)]
struct RemotePluginInstalledItem {
#[serde(flatten)]
plugin: RemotePluginDirectoryItem,
// Unsupported optional declarations must not invalidate the installed inventory.
#[serde_as(deserialize_as = "serde_with::DefaultOnError")]
#[serde(default)]
extensions: Option<PluginExtensions>,
#[serde(default)]
installed_at: Option<DateTime<Utc>>,
enabled: bool,
@@ -791,7 +800,7 @@ struct RecommendedPluginItem {
display_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
struct RemotePluginInstalledResponse {
plugins: Vec<RemotePluginInstalledItem>,
pagination: RemotePluginPagination,
@@ -1312,6 +1321,7 @@ pub fn group_remote_installed_plugins_by_marketplaces(
continue;
};
let plugin_summary = RemotePluginSummary {
extensions: plugin.extensions.clone(),
id: plugin_id.as_key(),
remote_plugin_id: plugin.id.clone(),
version: plugin.version.clone(),
@@ -1811,6 +1821,7 @@ fn build_remote_plugin_summary(
))
})?;
Ok(RemotePluginSummary {
extensions: installed_plugin.and_then(|plugin| plugin.extensions.clone()),
id: plugin_id.as_key(),
remote_plugin_id: plugin.id.clone(),
version: plugin.release.version.clone(),
@@ -1914,6 +1925,7 @@ fn remote_installed_plugin_to_cache_entry(
version: plugin.release.version.clone(),
name: plugin.name.clone(),
canonical_app_id: plugin.canonical_app_id.clone(),
extensions: installed_plugin.extensions.clone(),
installed_at: installed_plugin.installed_at,
enabled: installed_plugin.enabled,
install_policy: plugin.installation_policy,
@@ -2257,6 +2269,8 @@ async fn get_remote_plugin_installed_page(
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let mut url = Url::parse(&format!("{base_url}/ps/plugins/installed"))
.map_err(RemotePluginCatalogError::InvalidBaseUrl)?;
url.query_pairs_mut()
.append_pair("includeExtensions", "true");
match scope {
RemoteInstalledPluginScope::All => {
url.query_pairs_mut()
@@ -757,11 +757,11 @@ mod tests {
crate::test_support::recorded_http_client_urls(&selected_urls),
vec![
format!(
"{}/backend-api/ps/plugins/installed?limit=200&includeDownloadUrls=true",
"{}/backend-api/ps/plugins/installed?includeExtensions=true&limit=200&includeDownloadUrls=true",
server.uri()
),
format!(
"{}/backend-api/ps/plugins/installed?limit=200&includeDownloadUrls=true&pageToken=page-2",
"{}/backend-api/ps/plugins/installed?includeExtensions=true&limit=200&includeDownloadUrls=true&pageToken=page-2",
server.uri()
),
]
@@ -108,6 +108,7 @@ async fn search_remote_plugins_forwards_parameters_and_converts_results() {
result,
RemotePluginSearchPage {
plugins: vec![RemotePluginSummary {
extensions: None,
id: format!("linear@{REMOTE_GLOBAL_MARKETPLACE_NAME}"),
remote_plugin_id: "plugins~Plugin_linear".to_string(),
version: Some("1.2.3".to_string()),
@@ -635,6 +635,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
vec![
RemotePluginShareSummary {
summary: RemotePluginSummary {
extensions: None,
id: "demo-plugin@workspace-shared-with-me".to_string(),
remote_plugin_id: "plugins_123".to_string(),
version: Some("0.1.0".to_string()),
@@ -682,6 +683,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
},
RemotePluginShareSummary {
summary: RemotePluginSummary {
extensions: None,
id: "demo-plugin@workspace-shared-with-me".to_string(),
remote_plugin_id: "plugins_456".to_string(),
version: Some("0.1.0".to_string()),
@@ -14,6 +14,7 @@ fn image_url(renewal: &str) -> String {
fn plugin(renewal: &str) -> RemoteInstalledPlugin {
RemoteInstalledPlugin {
extensions: None,
marketplace_name: REMOTE_GLOBAL_MARKETPLACE_NAME.to_string(),
id: "plugin-test".to_string(),
version: Some("1.0.0".to_string()),
+6 -2
View File
@@ -151,11 +151,11 @@ async fn remote_installed_plugins_paginate_across_all_scopes_without_download_ur
recorded_http_client_urls(&selected_urls),
vec![
format!(
"{}/backend-api/ps/plugins/installed?limit=200",
"{}/backend-api/ps/plugins/installed?includeExtensions=true&limit=200",
server.uri()
),
format!(
"{}/backend-api/ps/plugins/installed?limit=200&pageToken=next+page%2F%2B",
"{}/backend-api/ps/plugins/installed?includeExtensions=true&limit=200&pageToken=next+page%2F%2B",
server.uri()
),
]
@@ -333,6 +333,7 @@ fn build_remote_marketplace_preserves_directory_order_and_appends_installed_only
directory_plugin("plugin-m", "mike"),
];
let installed_plugins = vec![RemotePluginInstalledItem {
extensions: None,
plugin: directory_plugin("plugin-a", "alpha"),
installed_at: None,
enabled: true,
@@ -365,6 +366,7 @@ fn installation_policy_source_is_preserved_across_remote_summary_paths() {
directory_plugin.installation_policy_source =
Some(RemotePluginInstallPolicySource::ImplicitCanonicalApp);
let installed_plugin = RemotePluginInstalledItem {
extensions: None,
plugin: directory_plugin.clone(),
installed_at: None,
enabled: true,
@@ -420,6 +422,7 @@ fn plan_eligibility_is_preserved_across_remote_summary_paths() {
"enterprise_cbp_automation".to_string(),
]);
let installed_plugin = RemotePluginInstalledItem {
extensions: None,
plugin: directory_plugin.clone(),
installed_at: None,
enabled: false,
@@ -513,6 +516,7 @@ fn installation_interstitial_requirement_is_preserved_across_remote_summary_path
directory_plugin.must_show_installation_interstitial = Some(false);
let installed_plugin = remote_installed_plugin_to_cache_entry(&RemotePluginInstalledItem {
extensions: None,
plugin: directory_plugin,
installed_at: None,
enabled: true,