mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
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:
@@ -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}"),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user