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
@@ -24,6 +24,7 @@ mod model;
mod notification;
mod permissions;
mod plugin;
mod plugin_extensions;
mod plugin_search;
mod process;
mod project;
@@ -63,6 +64,7 @@ pub use model::*;
pub use notification::*;
pub use permissions::*;
pub use plugin::*;
pub use plugin_extensions::*;
pub use plugin_search::*;
pub use process::*;
pub use project::*;
@@ -696,6 +696,8 @@ pub enum PluginDisabledReason {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginSummary {
#[serde(default)]
pub extensions: Option<super::PluginExtensions>,
pub id: String,
/// Backend remote plugin identifier when available.
pub remote_plugin_id: Option<String>,
@@ -0,0 +1,133 @@
//! Hosted plugin extension declarations carried by installed-plugin discovery.
use crate::JsonSchema;
use crate::TS;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginExtensions {
#[serde(default)]
pub entrypoints: Option<Vec<PluginEntrypoint>>,
#[serde(default, alias = "settings_entrypoints")]
pub settings_entrypoints: Vec<PluginEntrypoint>,
#[serde(default)]
pub settings: Vec<PluginSettings>,
#[serde(default, alias = "thread_entrypoints")]
pub thread_entrypoints: Vec<PluginEntrypoint>,
#[serde(default, alias = "file_handlers")]
pub file_handlers: Vec<PluginEntrypoint>,
#[serde(default, alias = "search_mention_providers")]
pub search_mention_providers: Vec<PluginSearchProvider>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type", rename_all = "camelCase", export_to = "v2/")]
pub enum PluginEntrypoint {
Global {
#[serde(flatten)]
presentation: PluginEntrypointPresentation,
#[serde(default, rename = "quickAction", alias = "quick_action")]
#[ts(rename = "quickAction")]
quick_action: Option<PluginQuickAction>,
},
Settings {
#[serde(flatten)]
presentation: PluginEntrypointPresentation,
#[serde(default, rename = "searchTerms", alias = "search_terms")]
#[ts(rename = "searchTerms")]
search_terms: Vec<String>,
},
Thread {
#[serde(flatten)]
presentation: PluginEntrypointPresentation,
},
File {
#[serde(flatten)]
presentation: PluginEntrypointPresentation,
extensions: Vec<String>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginEntrypointPresentation {
#[serde(alias = "app_id")]
pub app_id: String,
#[serde(alias = "tool_name")]
pub tool_name: String,
pub title: String,
#[serde(alias = "resource_uri")]
pub resource_uri: String,
pub icons: Vec<PluginIcon>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginIcon {
pub src: String,
pub mime_type: Option<String>,
pub sizes: Option<Vec<String>>,
pub theme: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginQuickAction {
pub title: String,
pub icons: Vec<PluginIcon>,
pub target: PluginQuickActionTarget,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type", rename_all = "camelCase", export_to = "v2/")]
pub enum PluginQuickActionTarget {
Tool {
name: String,
arguments: Option<serde_json::Value>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginSettings {
#[serde(alias = "app_id")]
pub app_id: String,
#[serde(alias = "read_tool_name")]
pub read_tool_name: String,
#[serde(alias = "update_tool_name")]
pub update_tool_name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginSearchProvider {
#[serde(alias = "app_id")]
pub app_id: String,
#[serde(alias = "tool_name")]
pub tool_name: String,
#[serde(alias = "link_id")]
pub link_id: String,
pub title: String,
pub call: Option<PluginSearchProviderCall>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct PluginSearchProviderCall {
pub name: String,
pub arguments: serde_json::Value,
#[serde(rename = "_meta")]
#[ts(rename = "_meta")]
pub meta: serde_json::Value,
}
@@ -4470,6 +4470,7 @@ fn plugin_share_list_response_serializes_share_items() {
serde_json::to_value(PluginShareListResponse {
data: vec![PluginShareListItem {
plugin: PluginSummary {
extensions: None,
id: "gmail@openai-curated-remote".to_string(),
remote_plugin_id: Some(
"plugins~Plugin_00000000000000000000000000000000".to_string(),
@@ -4499,6 +4500,7 @@ fn plugin_share_list_response_serializes_share_items() {
json!({
"data": [{
"plugin": {
"extensions": null,
"id": "gmail@openai-curated-remote",
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
"version": null,
@@ -4551,6 +4553,7 @@ fn plugin_summary_defaults_missing_availability_to_available() {
#[test]
fn plugin_summary_round_trips_plan_eligibility_metadata() {
let value = json!({
"extensions": null,
"id": "gmail@openai-curated-remote",
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
"version": null,