Expose plugin manifest keywords in app server (#21271)

## Summary
- Add plugin manifest keywords to core plugin marketplace/detail models
- Expose keywords on app-server v2 PluginSummary and generated
schema/types
- Populate keywords in plugin/list and plugin/read responses for local
plugins

Depends on https://github.com/openai/openai/pull/891087

## Validation
- just fmt
- just write-app-server-schema
- cargo test -p codex-app-server-protocol
- cargo test -p codex-core-plugins
- cargo test -p codex-app-server
plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_load
- cargo test -p codex-app-server
plugin_read_returns_plugin_details_with_bundle_contents
This commit is contained in:
Abdulrahman Alfozan
2026-05-05 22:09:05 -04:00
committed by GitHub
parent 136e442e95
commit 94db03d5af
20 changed files with 140 additions and 2 deletions

View File

@@ -12710,6 +12710,13 @@
}
]
},
"keywords": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},

View File

@@ -9321,6 +9321,13 @@
}
]
},
"keywords": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},

View File

@@ -347,6 +347,13 @@
}
]
},
"keywords": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},

View File

@@ -366,6 +366,13 @@
}
]
},
"keywords": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},

View File

@@ -307,6 +307,13 @@
}
]
},
"keywords": {
"default": [],
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},

View File

@@ -11,4 +11,4 @@ export type PluginSummary = { id: string, name: string, source: PluginSource, in
/**
* Availability state for installing and using the plugin.
*/
availability: PluginAvailability, interface: PluginInterface | null, };
availability: PluginAvailability, interface: PluginInterface | null, keywords: Array<string>, };

View File

@@ -446,6 +446,8 @@ pub struct PluginSummary {
#[serde(default)]
pub availability: PluginAvailability,
pub interface: Option<PluginInterface>,
#[serde(default)]
pub keywords: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]

View File

@@ -3044,6 +3044,7 @@ fn plugin_share_list_response_serializes_share_items() {
auth_policy: PluginAuthPolicy::OnUse,
availability: PluginAvailability::Available,
interface: None,
keywords: Vec::new(),
},
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
local_plugin_path: None,
@@ -3062,6 +3063,7 @@ fn plugin_share_list_response_serializes_share_items() {
"authPolicy": "ON_USE",
"availability": "AVAILABLE",
"interface": null,
"keywords": [],
},
"shareUrl": "https://chatgpt.example/plugins/share/share-key-1",
"localPluginPath": null,

View File

@@ -310,6 +310,7 @@ impl PluginRequestProcessor {
auth_policy: plugin.policy.authentication.into(),
availability: PluginAvailability::Available,
interface: plugin.interface.map(local_plugin_interface_to_info),
keywords: plugin.keywords,
})
.collect(),
})
@@ -465,6 +466,7 @@ impl PluginRequestProcessor {
auth_policy: outcome.plugin.policy.authentication.into(),
availability: PluginAvailability::Available,
interface: outcome.plugin.interface.map(local_plugin_interface_to_info),
keywords: outcome.plugin.keywords,
},
description: outcome.plugin.description,
skills: plugin_skills_to_info(
@@ -1270,6 +1272,7 @@ fn remote_plugin_summary_to_info(summary: RemoteCatalogPluginSummary) -> PluginS
auth_policy: summary.auth_policy,
availability: summary.availability,
interface: summary.interface,
keywords: summary.keywords,
}
}

View File

@@ -197,7 +197,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_
valid_repo_root
.path()
.join("plugins/valid-plugin/.codex-plugin/plugin.json"),
r#"{"name":"valid-plugin"}"#,
r#"{"name":"valid-plugin","keywords":["api-key","developer tools"]}"#,
)?;
std::fs::write(invalid_marketplace_path.as_path(), "{not json")?;
@@ -246,6 +246,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_
auth_policy: PluginAuthPolicy::OnInstall,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: None,
keywords: vec!["api-key".to_string(), "developer tools".to_string()],
}],
}]
);
@@ -548,6 +549,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
screenshots: Vec::new(),
screenshot_urls: Vec::new(),
}),
keywords: Vec::new(),
},
PluginSummary {
id: "missing-plugin@alternate-marketplace".to_string(),
@@ -563,6 +565,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab
auth_policy: PluginAuthPolicy::OnInstall,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: None,
keywords: Vec::new(),
},
],
}]
@@ -1295,6 +1298,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
"display_name": "Linear",
"description": "Track work in Linear",
"app_ids": [],
"keywords": ["issue-tracking", "project management"],
"interface": {
"short_description": "Plan and track work",
"capabilities": ["Read", "Write"],
@@ -1430,6 +1434,13 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -
.and_then(|interface| interface.display_name.as_deref()),
Some("Linear")
);
assert_eq!(
remote_marketplace.plugins[0].keywords,
vec![
"issue-tracking".to_string(),
"project management".to_string()
]
);
assert_eq!(response.featured_plugin_ids, Vec::<String>::new());
Ok(())
}

View File

@@ -172,6 +172,7 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
"display_name": "Linear",
"description": "Track work in Linear",
"app_ids": [],
"keywords": ["issue-tracking", "project management"],
"interface": {
"short_description": "Plan and track work",
"capabilities": ["Read", "Write"],
@@ -281,6 +282,13 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
response.plugin.description.as_deref(),
Some("Track work in Linear")
);
assert_eq!(
response.plugin.summary.keywords,
vec![
"issue-tracking".to_string(),
"project management".to_string()
]
);
assert_eq!(response.plugin.skills.len(), 1);
assert_eq!(response.plugin.skills[0].name, "plan-work");
assert_eq!(response.plugin.skills[0].path, None);
@@ -580,6 +588,7 @@ async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()>
r##"{
"name": "demo-plugin",
"description": "Longer manifest description",
"keywords": ["api-key", "developer tools"],
"interface": {
"displayName": "Plugin Display Name",
"shortDescription": "Short description for subtitle",
@@ -740,6 +749,10 @@ enabled = true
"Find my next action".to_string()
])
);
assert_eq!(
response.plugin.summary.keywords,
vec!["api-key".to_string(), "developer tools".to_string()]
);
assert_eq!(response.plugin.skills.len(), 1);
assert_eq!(
response.plugin.skills[0].name,

View File

@@ -164,6 +164,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> {
auth_policy: PluginAuthPolicy::OnUse,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
keywords: Vec::new(),
},
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
local_plugin_path: Some(expected_plugin_path),
@@ -239,6 +240,7 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> {
auth_policy: PluginAuthPolicy::OnUse,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
keywords: Vec::new(),
},
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
local_plugin_path: None,
@@ -342,6 +344,7 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> {
auth_policy: PluginAuthPolicy::OnUse,
availability: codex_app_server_protocol::PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
keywords: Vec::new(),
},
share_url: "https://chatgpt.example/plugins/share/share-key-1".to_string(),
local_plugin_path: None,

View File

@@ -223,6 +223,7 @@ pub struct PluginDetail {
pub source: MarketplacePluginSource,
pub policy: MarketplacePluginPolicy,
pub interface: Option<PluginManifestInterface>,
pub keywords: Vec<String>,
pub installed: bool,
pub enabled: bool,
pub skills: Vec<SkillMetadata>,
@@ -252,6 +253,7 @@ pub struct ConfiguredMarketplacePlugin {
pub source: MarketplacePluginSource,
pub policy: MarketplacePluginPolicy,
pub interface: Option<PluginManifestInterface>,
pub keywords: Vec<String>,
pub installed: bool,
pub enabled: bool,
}
@@ -1196,6 +1198,7 @@ impl PluginsManager {
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
keywords: plugin.keywords,
})
})
.collect::<Vec<_>>();
@@ -1245,6 +1248,11 @@ impl PluginsManager {
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
keywords: plugin
.manifest
.as_ref()
.map(|manifest| manifest.keywords.clone())
.unwrap_or_default(),
installed: installed_plugins.contains(&plugin_key),
enabled: enabled_plugins.contains(&plugin_key),
},
@@ -1287,6 +1295,7 @@ impl PluginsManager {
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
keywords: plugin.keywords,
installed: plugin.installed,
enabled: plugin.enabled,
skills: Vec::new(),
@@ -1363,6 +1372,7 @@ impl PluginsManager {
source: plugin.source,
policy: plugin.policy,
interface,
keywords: manifest.keywords,
installed: plugin.installed,
enabled: plugin.enabled,
skills: resolved_skills.skills,

View File

@@ -1550,6 +1550,7 @@ enabled = false
products: None,
},
interface: None,
keywords: Vec::new(),
installed: true,
enabled: true,
},
@@ -1566,6 +1567,7 @@ enabled = false
products: None,
},
interface: None,
keywords: Vec::new(),
installed: true,
enabled: false,
},
@@ -1684,6 +1686,7 @@ plugins = true
products: None,
},
interface: None,
keywords: Vec::new(),
installed: false,
enabled: false,
}]
@@ -2068,6 +2071,7 @@ plugins = true
products: None,
},
interface: None,
keywords: Vec::new(),
installed: false,
enabled: false,
}],
@@ -2361,6 +2365,7 @@ enabled = false
products: None,
},
interface: None,
keywords: Vec::new(),
installed: false,
enabled: true,
}]
@@ -2390,6 +2395,7 @@ enabled = false
products: None,
},
interface: None,
keywords: Vec::new(),
installed: false,
enabled: false,
}]
@@ -2473,6 +2479,7 @@ enabled = true
products: None,
},
interface: None,
keywords: Vec::new(),
installed: false,
enabled: true,
}],

View File

@@ -18,6 +18,8 @@ struct RawPluginManifest {
version: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
keywords: Vec<String>,
// Keep manifest paths as raw strings so we can validate the required `./...` syntax before
// resolving them under the plugin root.
#[serde(default)]
@@ -37,6 +39,7 @@ pub struct PluginManifest {
pub name: String,
pub version: Option<String>,
pub description: Option<String>,
pub keywords: Vec<String>,
pub paths: PluginManifestPaths,
pub interface: Option<PluginManifestInterface>,
}
@@ -143,6 +146,7 @@ pub fn load_plugin_manifest(plugin_root: &Path) -> Option<PluginManifest> {
name: raw_name,
version,
description,
keywords,
skills,
mcp_servers,
apps,
@@ -232,6 +236,7 @@ pub fn load_plugin_manifest(plugin_root: &Path) -> Option<PluginManifest> {
name,
version,
description,
keywords,
paths: PluginManifestPaths {
skills: resolve_manifest_path(plugin_root, "skills", skills.as_deref()),
mcp_servers: resolve_manifest_path(
@@ -568,6 +573,28 @@ mod tests {
assert_eq!(manifest.version, Some("1.2.3-beta+7".to_string()));
}
#[test]
fn plugin_manifest_reads_keywords() {
let tmp = tempdir().expect("tempdir");
let plugin_root = tmp.path().join("demo-plugin");
fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create manifest dir");
fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{
"name": "demo-plugin",
"keywords": ["api-key", "developer tools"]
}"#,
)
.expect("write manifest");
let manifest = load_manifest(&plugin_root);
assert_eq!(
manifest.keywords,
vec!["api-key".to_string(), "developer tools".to_string()]
);
}
#[test]
fn plugin_manifest_uses_alternate_discoverable_path() {
let tmp = tempdir().expect("tempdir");

View File

@@ -62,6 +62,7 @@ pub struct MarketplacePlugin {
pub source: MarketplacePluginSource,
pub policy: MarketplacePluginPolicy,
pub interface: Option<PluginManifestInterface>,
pub keywords: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -293,6 +294,10 @@ pub fn load_marketplace(path: &AbsolutePathBuf) -> Result<Marketplace, Marketpla
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
keywords: plugin
.manifest
.map(|manifest| manifest.keywords)
.unwrap_or_default(),
});
}

View File

@@ -413,6 +413,7 @@ fn list_marketplaces_supports_alternate_manifest_layout() {
logo: None,
screenshots: Vec::new(),
}),
keywords: Vec::new(),
}],
}]
);
@@ -462,6 +463,7 @@ fn list_marketplaces_includes_plugins_without_discoverable_manifest() {
products: None,
},
interface: None,
keywords: Vec::new(),
}],
}]
);
@@ -602,6 +604,7 @@ fn list_marketplaces_returns_home_and_repo_marketplaces() {
products: None,
},
interface: None,
keywords: Vec::new(),
},
MarketplacePlugin {
name: "home-only".to_string(),
@@ -614,6 +617,7 @@ fn list_marketplaces_returns_home_and_repo_marketplaces() {
products: None,
},
interface: None,
keywords: Vec::new(),
},
],
},
@@ -635,6 +639,7 @@ fn list_marketplaces_returns_home_and_repo_marketplaces() {
products: None,
},
interface: None,
keywords: Vec::new(),
},
MarketplacePlugin {
name: "repo-only".to_string(),
@@ -647,6 +652,7 @@ fn list_marketplaces_returns_home_and_repo_marketplaces() {
products: None,
},
interface: None,
keywords: Vec::new(),
},
],
},
@@ -724,6 +730,7 @@ fn list_marketplaces_keeps_distinct_entries_for_same_name() {
products: None,
},
interface: None,
keywords: Vec::new(),
}],
},
Marketplace {
@@ -741,6 +748,7 @@ fn list_marketplaces_keeps_distinct_entries_for_same_name() {
products: None,
},
interface: None,
keywords: Vec::new(),
}],
},
]
@@ -814,6 +822,7 @@ fn list_marketplaces_dedupes_multiple_roots_in_same_repo() {
products: None,
},
interface: None,
keywords: Vec::new(),
}],
}]
);
@@ -976,6 +985,7 @@ fn list_marketplaces_skips_plugins_with_invalid_names_but_keeps_marketplace() {
products: None,
},
interface: None,
keywords: Vec::new(),
}],
}]
);
@@ -1093,6 +1103,7 @@ fn list_marketplaces_keeps_remote_and_local_plugin_sources() {
products: None,
},
interface: None,
keywords: Vec::new(),
},
MarketplacePlugin {
name: "url-plugin".to_string(),
@@ -1108,6 +1119,7 @@ fn list_marketplaces_keeps_remote_and_local_plugin_sources() {
products: None,
},
interface: None,
keywords: Vec::new(),
},
MarketplacePlugin {
name: "git-subdir-plugin".to_string(),
@@ -1123,6 +1135,7 @@ fn list_marketplaces_keeps_remote_and_local_plugin_sources() {
products: None,
},
interface: None,
keywords: Vec::new(),
},
]
);

View File

@@ -74,6 +74,7 @@ pub struct RemotePluginSummary {
pub auth_policy: PluginAuthPolicy,
pub availability: PluginAvailability,
pub interface: Option<PluginInterface>,
pub keywords: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
@@ -321,6 +322,8 @@ struct RemotePluginReleaseResponse {
bundle_download_url: Option<String>,
#[serde(default)]
app_ids: Vec<String>,
#[serde(default)]
keywords: Vec<String>,
interface: RemotePluginReleaseInterfaceResponse,
#[serde(default)]
skills: Vec<RemotePluginSkillResponse>,
@@ -771,6 +774,7 @@ fn build_remote_plugin_summary(
auth_policy: plugin.authentication_policy,
availability: plugin.availability,
interface: remote_plugin_interface_to_info(plugin),
keywords: plugin.release.keywords.clone(),
}
}

View File

@@ -442,6 +442,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
auth_policy: PluginAuthPolicy::OnUse,
availability: PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
keywords: Vec::new(),
},
share_url: Some("https://chatgpt.example/plugins/share/share-key-1".to_string()),
local_plugin_path: Some(local_plugin_path),
@@ -456,6 +457,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() {
auth_policy: PluginAuthPolicy::OnUse,
availability: PluginAvailability::Available,
interface: Some(expected_plugin_interface()),
keywords: Vec::new(),
},
share_url: None,
local_plugin_path: None,

View File

@@ -1448,6 +1448,7 @@ pub(super) fn plugins_test_summary(
description,
/*long_description*/ None,
)),
keywords: Vec::new(),
}
}