diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 8e0e2c39b4..a1519e4240 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12710,6 +12710,13 @@ } ] }, + "keywords": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, "name": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 67aaf9e389..5cecb71ed5 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -9321,6 +9321,13 @@ } ] }, + "keywords": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, "name": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json index dc383608f2..bf1ce45755 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json @@ -347,6 +347,13 @@ } ] }, + "keywords": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, "name": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json index 2762807c7d..c1736cdcc1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json @@ -366,6 +366,13 @@ } ] }, + "keywords": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, "name": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json index adb5021be8..bae3eb034f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json @@ -307,6 +307,13 @@ } ] }, + "keywords": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, "name": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts index fe9e63703d..8a760e2e5a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts @@ -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, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs index 09f9325317..a6c22bfc9f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -446,6 +446,8 @@ pub struct PluginSummary { #[serde(default)] pub availability: PluginAvailability, pub interface: Option, + #[serde(default)] + pub keywords: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 5314c73b00..719a933f22 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -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, diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 40f2a412ac..d9f66d65ce 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -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, } } diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 86fb78bae1..e82ebe9fe9 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -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::::new()); Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index fd082ab412..bb42714d3d 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -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, diff --git a/codex-rs/app-server/tests/suite/v2/plugin_share.rs b/codex-rs/app-server/tests/suite/v2/plugin_share.rs index a44a64be7c..604935b6e5 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs @@ -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, diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index ba56646649..2c974ef966 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -223,6 +223,7 @@ pub struct PluginDetail { pub source: MarketplacePluginSource, pub policy: MarketplacePluginPolicy, pub interface: Option, + pub keywords: Vec, pub installed: bool, pub enabled: bool, pub skills: Vec, @@ -252,6 +253,7 @@ pub struct ConfiguredMarketplacePlugin { pub source: MarketplacePluginSource, pub policy: MarketplacePluginPolicy, pub interface: Option, + pub keywords: Vec, 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::>(); @@ -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, diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 8abff7700b..50d0d46258 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -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, }], diff --git a/codex-rs/core-plugins/src/manifest.rs b/codex-rs/core-plugins/src/manifest.rs index 12b738f537..6de7f820b8 100644 --- a/codex-rs/core-plugins/src/manifest.rs +++ b/codex-rs/core-plugins/src/manifest.rs @@ -18,6 +18,8 @@ struct RawPluginManifest { version: Option, #[serde(default)] description: Option, + #[serde(default)] + keywords: Vec, // 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, pub description: Option, + pub keywords: Vec, pub paths: PluginManifestPaths, pub interface: Option, } @@ -143,6 +146,7 @@ pub fn load_plugin_manifest(plugin_root: &Path) -> Option { name: raw_name, version, description, + keywords, skills, mcp_servers, apps, @@ -232,6 +236,7 @@ pub fn load_plugin_manifest(plugin_root: &Path) -> Option { 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"); diff --git a/codex-rs/core-plugins/src/marketplace.rs b/codex-rs/core-plugins/src/marketplace.rs index dc6d01adf9..f66b5d1b22 100644 --- a/codex-rs/core-plugins/src/marketplace.rs +++ b/codex-rs/core-plugins/src/marketplace.rs @@ -62,6 +62,7 @@ pub struct MarketplacePlugin { pub source: MarketplacePluginSource, pub policy: MarketplacePluginPolicy, pub interface: Option, + pub keywords: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -293,6 +294,10 @@ pub fn load_marketplace(path: &AbsolutePathBuf) -> Result, + pub keywords: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -321,6 +322,8 @@ struct RemotePluginReleaseResponse { bundle_download_url: Option, #[serde(default)] app_ids: Vec, + #[serde(default)] + keywords: Vec, interface: RemotePluginReleaseInterfaceResponse, #[serde(default)] skills: Vec, @@ -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(), } } diff --git a/codex-rs/core-plugins/src/remote/share/tests.rs b/codex-rs/core-plugins/src/remote/share/tests.rs index efdecdbbbc..65a989c26e 100644 --- a/codex-rs/core-plugins/src/remote/share/tests.rs +++ b/codex-rs/core-plugins/src/remote/share/tests.rs @@ -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, diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 3f7c9bd5b2..1db968f7e0 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -1448,6 +1448,7 @@ pub(super) fn plugins_test_summary( description, /*long_description*/ None, )), + keywords: Vec::new(), } }