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 a717ec342852..53139c82d89a 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 @@ -14350,6 +14350,14 @@ }, "source": { "$ref": "#/definitions/v2/PluginSource" + }, + "version": { + "default": null, + "description": "Version advertised by the remote marketplace backend when available.", + "type": [ + "string", + "null" + ] } }, "required": [ 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 6155844a8b8a..961dd14f3eea 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 @@ -10754,6 +10754,14 @@ }, "source": { "$ref": "#/definitions/PluginSource" + }, + "version": { + "default": null, + "description": "Version advertised by the remote marketplace backend when available.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json index 4c5b205538d8..55ad95be2e7b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json @@ -540,6 +540,14 @@ }, "source": { "$ref": "#/definitions/PluginSource" + }, + "version": { + "default": null, + "description": "Version advertised by the remote marketplace backend when available.", + "type": [ + "string", + "null" + ] } }, "required": [ 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 62b9ac19ea8e..25d12e50ddfb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json @@ -540,6 +540,14 @@ }, "source": { "$ref": "#/definitions/PluginSource" + }, + "version": { + "default": null, + "description": "Version advertised by the remote marketplace backend when available.", + "type": [ + "string", + "null" + ] } }, "required": [ 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 ad63129bf8e3..b4b917602a98 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json @@ -680,6 +680,14 @@ }, "source": { "$ref": "#/definitions/PluginSource" + }, + "version": { + "default": null, + "description": "Version advertised by the remote marketplace backend when available.", + "type": [ + "string", + "null" + ] } }, "required": [ 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 7c6f48624080..751a6c5e7f93 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json @@ -496,6 +496,14 @@ }, "source": { "$ref": "#/definitions/PluginSource" + }, + "version": { + "default": null, + "description": "Version advertised by the remote marketplace backend when available.", + "type": [ + "string", + "null" + ] } }, "required": [ 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 268349cb9b81..d12f55b3a3d1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts @@ -13,6 +13,10 @@ export type PluginSummary = { id: string, * Backend remote plugin identifier when available. */ remotePluginId: string | null, +/** + * Version advertised by the remote marketplace backend when available. + */ +version: string | null, /** * Version of the locally materialized plugin package when available. */ 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 148fe2f2a4f6..19a4ccd8892d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -600,6 +600,9 @@ pub struct PluginSummary { pub id: String, /// Backend remote plugin identifier when available. pub remote_plugin_id: Option, + /// Version advertised by the remote marketplace backend when available. + #[serde(default)] + pub version: Option, /// Version of the locally materialized plugin package when available. #[serde(default)] pub local_version: Option, 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 48f92bf785bc..77d8a1439b2f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3642,6 +3642,7 @@ fn plugin_share_list_response_serializes_share_items() { remote_plugin_id: Some( "plugins~Plugin_00000000000000000000000000000000".to_string(), ), + version: None, local_version: None, name: "gmail".to_string(), share_context: None, @@ -3663,6 +3664,7 @@ fn plugin_share_list_response_serializes_share_items() { "plugin": { "id": "gmail@openai-curated-remote", "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", + "version": null, "localVersion": null, "name": "gmail", "shareContext": null, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c9ac2f5fb27c..43e7dbd3eb9c 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -213,7 +213,7 @@ Example with notification opt-out: - `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present. - `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists. - `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors. -- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**). +- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, the remote marketplace `version` and locally materialized `localVersion` when available, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**). - `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**). - `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote plugin details expose the canonical `shareUrl` supplied by the remote catalog when available; it is `null` for local plugins or when the catalog omits it. This field is separate from `summary.shareContext`, which continues to describe user and workspace sharing state. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Use `plugin/install`'s `appsNeedingAuth` to drive post-install authentication and `app/list`'s `isAccessible` to determine current connector accessibility (**under development; do not call from production clients yet**). - `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle. diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 020fdc7cbd2c..35c12d49c1aa 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -155,6 +155,7 @@ fn convert_configured_marketplace_plugin_to_plugin_summary( PluginSummary { id: plugin.id, remote_plugin_id: None, + version: None, local_version: plugin.local_version, installed: plugin.installed, enabled: plugin.enabled, @@ -1095,6 +1096,7 @@ impl PluginRequestProcessor { summary: PluginSummary { id: outcome.plugin.id, remote_plugin_id: None, + version: None, local_version: outcome.plugin.local_version, name: outcome.plugin.name, share_context, @@ -2168,6 +2170,7 @@ fn remote_plugin_summary_to_info(summary: RemoteCatalogPluginSummary) -> PluginS PluginSummary { id: summary.id, remote_plugin_id: Some(summary.remote_plugin_id), + version: summary.version, local_version: summary.local_version, name: summary.name, share_context: summary 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 974bb8a2c5b8..ae03b3a1a9c9 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -452,6 +452,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_ plugins: vec![PluginSummary { id: "valid-plugin@valid-marketplace".to_string(), remote_plugin_id: None, + version: None, local_version: None, name: "valid-plugin".to_string(), share_context: None, @@ -743,6 +744,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab PluginSummary { id: "valid-plugin@alternate-marketplace".to_string(), remote_plugin_id: None, + version: None, local_version: None, name: "valid-plugin".to_string(), share_context: None, @@ -780,6 +782,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab PluginSummary { id: "missing-plugin@alternate-marketplace".to_string(), remote_plugin_id: None, + version: None, local_version: None, name: "missing-plugin".to_string(), share_context: None, @@ -1683,6 +1686,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() - "authentication_policy": "ON_USE", "status": "ENABLED", "release": { + "version": "1.2.3", "display_name": "Linear", "description": "Track work in Linear", "app_ids": [], @@ -1831,6 +1835,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() - ); assert_eq!(remote_marketplace.plugins[0].name, "linear"); assert_eq!(remote_marketplace.plugins[0].source, PluginSource::Remote); + assert_eq!( + remote_marketplace.plugins[0].version.as_deref(), + Some("1.2.3") + ); assert_eq!( remote_marketplace.plugins[0].local_version.as_deref(), Some("1.2.3") @@ -2030,6 +2038,7 @@ async fn plugin_list_includes_openai_curated_remote_collection_when_remote_plugi "authentication_policy": "ON_USE", "status": "ENABLED", "release": { + "version": "1.2.3", "display_name": "Linear", "description": "Track work in Linear", "app_ids": [], @@ -2090,6 +2099,7 @@ async fn plugin_list_includes_openai_curated_remote_collection_when_remote_plugi ); assert_eq!(plugin.name, "linear"); assert_eq!(plugin.source, PluginSource::Remote); + assert_eq!(plugin.version.as_deref(), Some("1.2.3")); assert_eq!(plugin.installed, false); assert_eq!(plugin.enabled, false); @@ -2492,16 +2502,25 @@ plugin_sharing = true marketplace .plugins .iter() - .map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled)) + .map(|plugin| { + ( + plugin.id.clone(), + plugin.version.clone(), + plugin.installed, + plugin.enabled, + ) + }) .collect::>(), vec![ ( "shared-linear@workspace-shared-with-me".to_string(), + Some("1.2.3".to_string()), true, true ), ( "unlisted-linear@workspace-shared-with-me".to_string(), + Some("1.2.3".to_string()), true, false ) 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 80fd56515c40..a82b363806f1 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs @@ -171,6 +171,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> { plugin: PluginSummary { id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: Some("plugins_123".to_string()), + version: Some("0.1.0".to_string()), local_version: Some("0.1.0".to_string()), name: "demo-plugin".to_string(), share_context: Some(expected_share_context("plugins_123")), @@ -574,6 +575,7 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> { plugin: PluginSummary { id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: Some("plugins_123".to_string()), + version: Some("0.1.0".to_string()), local_version: Some("0.1.0".to_string()), name: "demo-plugin".to_string(), share_context: Some(expected_share_context("plugins_123")), @@ -1174,6 +1176,7 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> { plugin: PluginSummary { id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: Some("plugins_123".to_string()), + version: Some("0.1.0".to_string()), local_version: Some("0.1.0".to_string()), name: "demo-plugin".to_string(), share_context: Some(expected_share_context("plugins_123")), diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index da7bc0b5a5f6..469a9a7fc51f 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -777,6 +777,7 @@ fn remote_installed_plugin_in_marketplace( RemoteInstalledPlugin { marketplace_name: marketplace_name.to_string(), id: format!("plugins~Plugin_{name}"), + version: None, name: name.to_string(), enabled: true, install_policy: codex_app_server_protocol::PluginInstallPolicy::Available, diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 515bbb71c00a..4c9bd2e6724b 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -148,6 +148,7 @@ pub enum RemoteMarketplaceSource { pub struct RemoteInstalledPlugin { pub marketplace_name: String, pub id: String, + pub version: Option, pub name: String, pub enabled: bool, pub install_policy: PluginInstallPolicy, @@ -161,6 +162,7 @@ pub struct RemoteInstalledPlugin { pub struct RemotePluginSummary { pub id: String, pub remote_plugin_id: String, + pub version: Option, pub local_version: Option, pub name: String, pub share_context: Option, @@ -1041,6 +1043,7 @@ pub fn group_remote_installed_plugins_by_marketplaces( let plugin_summary = RemotePluginSummary { id: plugin_id.as_key(), remote_plugin_id: plugin.id.clone(), + version: plugin.version.clone(), local_version: None, name: plugin.name.clone(), share_context: None, @@ -1472,6 +1475,7 @@ fn build_remote_plugin_summary( Ok(RemotePluginSummary { id: plugin_id.as_key(), remote_plugin_id: plugin.id.clone(), + version: plugin.release.version.clone(), local_version: installed_plugin .and_then(|installed| installed.plugin.release.version.clone()), name: plugin.name.clone(), @@ -1554,6 +1558,7 @@ fn remote_installed_plugin_to_cache_entry( Ok(RemoteInstalledPlugin { marketplace_name: remote_plugin_canonical_marketplace_name(plugin)?.to_string(), id: plugin.id.clone(), + version: plugin.release.version.clone(), name: plugin.name.clone(), enabled: installed_plugin.enabled, install_policy: plugin.installation_policy, diff --git a/codex-rs/core-plugins/src/remote/share/tests.rs b/codex-rs/core-plugins/src/remote/share/tests.rs index c8d06b234dec..d7744681b9e0 100644 --- a/codex-rs/core-plugins/src/remote/share/tests.rs +++ b/codex-rs/core-plugins/src/remote/share/tests.rs @@ -618,6 +618,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() { summary: RemotePluginSummary { id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: "plugins_123".to_string(), + version: Some("0.1.0".to_string()), local_version: None, name: "demo-plugin".to_string(), share_context: Some(RemotePluginShareContext { @@ -658,6 +659,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() { summary: RemotePluginSummary { id: "demo-plugin@workspace-shared-with-me".to_string(), remote_plugin_id: "plugins_456".to_string(), + version: Some("0.1.0".to_string()), local_version: Some("0.1.0".to_string()), name: "demo-plugin".to_string(), share_context: Some(RemotePluginShareContext { diff --git a/codex-rs/tui/src/app/plugin_mentions.rs b/codex-rs/tui/src/app/plugin_mentions.rs index f12dee72cc43..ae2d25a4dce4 100644 --- a/codex-rs/tui/src/app/plugin_mentions.rs +++ b/codex-rs/tui/src/app/plugin_mentions.rs @@ -165,6 +165,7 @@ mod tests { PluginSummary { id: format!("{name}@server-marketplace"), remote_plugin_id: Some(format!("plugins~{name}")), + version: None, local_version: None, name: name.to_string(), share_context: None, diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 5e23e9f96a0b..1f9374bbc358 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -1331,6 +1331,7 @@ pub(super) fn plugins_test_summary( PluginSummary { id: id.to_string(), remote_plugin_id: None, + version: None, local_version: None, name: name.to_string(), share_context: None, @@ -1361,6 +1362,7 @@ pub(super) fn plugins_test_remote_summary( PluginSummary { id: remote_plugin_id.to_string(), remote_plugin_id: Some(remote_plugin_id.to_string()), + version: None, local_version: None, name: name.to_string(), share_context: None,