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 4e061f8591..9934e36940 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 @@ -12708,6 +12708,13 @@ "name": { "type": "string" }, + "remotePluginId": { + "description": "Backend remote plugin identifier when available.", + "type": [ + "string", + "null" + ] + }, "shareContext": { "anyOf": [ { 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 c7130126f9..1926b22091 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 @@ -9257,6 +9257,13 @@ "name": { "type": "string" }, + "remotePluginId": { + "description": "Backend remote plugin identifier when available.", + "type": [ + "string", + "null" + ] + }, "shareContext": { "anyOf": [ { 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 473d1a2a5d..72b1f7c9aa 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json @@ -452,6 +452,13 @@ "name": { "type": "string" }, + "remotePluginId": { + "description": "Backend remote plugin identifier when available.", + "type": [ + "string", + "null" + ] + }, "shareContext": { "anyOf": [ { 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 d4473d96ac..945b4897cb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json @@ -506,6 +506,13 @@ "name": { "type": "string" }, + "remotePluginId": { + "description": "Backend remote plugin identifier when available.", + "type": [ + "string", + "null" + ] + }, "shareContext": { "anyOf": [ { 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 ae70ed5cdd..ed2beacada 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json @@ -408,6 +408,13 @@ "name": { "type": "string" }, + "remotePluginId": { + "description": "Backend remote plugin identifier when available.", + "type": [ + "string", + "null" + ] + }, "shareContext": { "anyOf": [ { 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 d855f3d31c..8fae7ae865 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts @@ -8,7 +8,11 @@ import type { PluginInterface } from "./PluginInterface"; import type { PluginShareContext } from "./PluginShareContext"; import type { PluginSource } from "./PluginSource"; -export type PluginSummary = { id: string, name: string, +export type PluginSummary = { id: string, +/** + * Backend remote plugin identifier when available. + */ +remotePluginId: string | null, name: string, /** * Remote sharing context associated with this plugin 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 ed03cc6ff3..82568446b7 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -538,6 +538,8 @@ pub enum PluginAvailability { #[ts(export_to = "v2/")] pub struct PluginSummary { pub id: String, + /// Backend remote plugin identifier when available. + pub remote_plugin_id: Option, pub name: String, /// Remote sharing context associated with this plugin when available. pub share_context: 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 30599776ae..73b22cecb4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3003,7 +3003,10 @@ fn plugin_share_list_response_serializes_share_items() { serde_json::to_value(PluginShareListResponse { data: vec![PluginShareListItem { plugin: PluginSummary { - id: "plugins~Plugin_00000000000000000000000000000000".to_string(), + id: "gmail@chatgpt-global".to_string(), + remote_plugin_id: Some( + "plugins~Plugin_00000000000000000000000000000000".to_string(), + ), name: "gmail".to_string(), share_context: None, source: PluginSource::Remote, @@ -3022,7 +3025,8 @@ fn plugin_share_list_response_serializes_share_items() { json!({ "data": [{ "plugin": { - "id": "plugins~Plugin_00000000000000000000000000000000", + "id": "gmail@chatgpt-global", + "remotePluginId": "plugins~Plugin_00000000000000000000000000000000", "name": "gmail", "shareContext": null, "source": { "type": "remote" }, diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 23d76d55b7..125accad82 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -482,6 +482,7 @@ impl PluginRequestProcessor { ); PluginSummary { id: plugin.id, + remote_plugin_id: None, installed: plugin.installed, enabled: plugin.enabled, name: plugin.name, @@ -723,6 +724,7 @@ impl PluginRequestProcessor { marketplace_path: outcome.marketplace_path, summary: PluginSummary { id: outcome.plugin.id, + remote_plugin_id: None, name: outcome.plugin.name, share_context, source: marketplace_plugin_source_to_info(outcome.plugin.source), @@ -1107,7 +1109,6 @@ impl PluginRequestProcessor { ) })?; if remote_detail.summary.availability == PluginAvailability::DisabledByAdmin { - let remote_plugin_id = &remote_detail.summary.id; return Err(invalid_request(format!( "remote plugin {remote_plugin_id} is disabled by admin" ))); @@ -1587,6 +1588,7 @@ fn remote_marketplace_to_info(marketplace: RemoteMarketplace) -> PluginMarketpla fn remote_plugin_summary_to_info(summary: RemoteCatalogPluginSummary) -> PluginSummary { PluginSummary { id: summary.id, + remote_plugin_id: Some(summary.remote_plugin_id), name: summary.name, share_context: summary .share_context 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 8b8e104a03..6576ba87b9 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -240,6 +240,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_ interface: None, plugins: vec![PluginSummary { id: "valid-plugin@valid-marketplace".to_string(), + remote_plugin_id: None, name: "valid-plugin".to_string(), share_context: None, source: PluginSource::Local { @@ -529,6 +530,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab plugins: vec![ PluginSummary { id: "valid-plugin@alternate-marketplace".to_string(), + remote_plugin_id: None, name: "valid-plugin".to_string(), share_context: None, source: PluginSource::Local { @@ -562,6 +564,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab }, PluginSummary { id: "missing-plugin@alternate-marketplace".to_string(), + remote_plugin_id: None, name: "missing-plugin".to_string(), share_context: None, source: PluginSource::Local { @@ -688,6 +691,7 @@ async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<( .flat_map(|marketplace| marketplace.plugins.iter()) .find(|plugin| plugin.name == "demo-plugin") .expect("expected demo-plugin entry"); + assert_eq!(plugin.remote_plugin_id, None); let share_context = plugin .share_context .as_ref() @@ -1464,11 +1468,7 @@ async fn plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles() .into_iter() .map(|plugin| (plugin.id, plugin.installed, plugin.enabled)) .collect::>(), - vec![( - "plugins~Plugin_00000000000000000000000000000000".to_string(), - true, - true - )] + vec![("linear@chatgpt-global".to_string(), true, true)] ); wait_for_path_exists(&new_path.join(".codex-plugin/plugin.json")).await?; @@ -1629,9 +1629,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() - Some("ChatGPT Plugins") ); assert_eq!(remote_marketplace.plugins.len(), 1); + assert_eq!(remote_marketplace.plugins[0].id, "linear@chatgpt-global"); assert_eq!( - remote_marketplace.plugins[0].id, - "plugins~Plugin_00000000000000000000000000000000" + remote_marketplace.plugins[0].remote_plugin_id.as_deref(), + Some("plugins~Plugin_00000000000000000000000000000000") ); assert_eq!(remote_marketplace.plugins[0].name, "linear"); assert_eq!(remote_marketplace.plugins[0].source, PluginSource::Remote); @@ -1725,12 +1726,14 @@ async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag "plugins~Plugin_11111111111111111111111111111111", "workspace-linear", "Workspace Linear", + "LISTED", /*enabled*/ None, ); let workspace_installed_body = workspace_remote_plugin_page_body( "plugins~Plugin_11111111111111111111111111111111", "workspace-linear", "Workspace Linear", + "LISTED", /*enabled*/ Some(false), ); mount_remote_plugin_list(&server, "WORKSPACE", &workspace_plugin_body).await; @@ -1764,6 +1767,14 @@ async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag Some("Workspace Directory") ); assert_eq!(marketplace.plugins.len(), 1); + assert_eq!( + marketplace.plugins[0].id, + "workspace-linear@workspace-directory" + ); + assert_eq!( + marketplace.plugins[0].remote_plugin_id.as_deref(), + Some("plugins~Plugin_11111111111111111111111111111111") + ); assert_eq!(marketplace.plugins[0].name, "workspace-linear"); assert_eq!(marketplace.plugins[0].installed, true); assert_eq!(marketplace.plugins[0].enabled, false); @@ -1803,6 +1814,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> { "plugins~Plugin_22222222222222222222222222222222", "shared-linear", "Shared Linear", + "PRIVATE", /*enabled*/ None, ))?; shared_plugin_body["plugins"][0]["share_principals"] = serde_json::Value::Null; @@ -1811,6 +1823,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> { "plugins~Plugin_22222222222222222222222222222222", "shared-linear", "Shared Linear", + "PRIVATE", /*enabled*/ Some(true), ); mount_shared_workspace_plugins(&server, &shared_plugin_body).await; @@ -1844,6 +1857,11 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> { Some("Shared with me") ); assert_eq!(marketplace.plugins.len(), 1); + assert_eq!(marketplace.plugins[0].id, "shared-linear@shared-with-me"); + assert_eq!( + marketplace.plugins[0].remote_plugin_id.as_deref(), + Some("plugins~Plugin_22222222222222222222222222222222") + ); assert_eq!(marketplace.plugins[0].name, "shared-linear"); assert_eq!(marketplace.plugins[0].installed, true); assert_eq!(marketplace.plugins[0].enabled, true); @@ -2381,6 +2399,7 @@ fn workspace_remote_plugin_page_body( remote_plugin_id: &str, plugin_name: &str, display_name: &str, + discoverability: &str, enabled: Option, ) -> String { let enabled_field = enabled @@ -2393,7 +2412,7 @@ fn workspace_remote_plugin_page_body( "id": "{remote_plugin_id}", "name": "{plugin_name}", "scope": "WORKSPACE", - "discoverability": "PRIVATE", + "discoverability": "{discoverability}", "creator_account_user_id": "user-gavin__account-123", "share_url": "https://chatgpt.example/plugins/share/share-key-1", "installation_policy": "AVAILABLE", 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 769749b34b..62294b2f95 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -209,9 +209,10 @@ plugins = true let response: PluginReadResponse = to_response(response)?; assert_eq!(response.plugin.marketplace_name, "chatgpt-global"); + assert_eq!(response.plugin.summary.id, "linear@chatgpt-global"); assert_eq!( - response.plugin.summary.id, - "plugins~Plugin_00000000000000000000000000000000" + response.plugin.summary.remote_plugin_id.as_deref(), + Some("plugins~Plugin_00000000000000000000000000000000") ); assert_eq!(response.plugin.summary.name, "linear"); assert_eq!(response.plugin.summary.source, PluginSource::Remote); @@ -313,6 +314,12 @@ async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result< .await??; let response: PluginReadResponse = to_response(response)?; + assert_eq!(response.plugin.marketplace_name, "shared-with-me"); + assert_eq!(response.plugin.summary.id, "shared-linear@shared-with-me"); + assert_eq!( + response.plugin.summary.remote_plugin_id.as_deref(), + Some("plugins~Plugin_11111111111111111111111111111111") + ); let share_context = response .plugin .summary @@ -482,9 +489,10 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() -> assert_eq!(response.plugin.marketplace_name, "chatgpt-global"); assert_eq!(response.plugin.marketplace_path, None); assert_eq!(response.plugin.summary.source, PluginSource::Remote); + assert_eq!(response.plugin.summary.id, "linear@chatgpt-global"); assert_eq!( - response.plugin.summary.id, - "plugins~Plugin_00000000000000000000000000000000" + response.plugin.summary.remote_plugin_id.as_deref(), + Some("plugins~Plugin_00000000000000000000000000000000") ); assert_eq!(response.plugin.summary.name, "linear"); assert_eq!(response.plugin.summary.installed, true); @@ -856,6 +864,7 @@ async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<( .await??; let response: PluginReadResponse = to_response(response)?; + assert_eq!(response.plugin.summary.remote_plugin_id, None); let share_context = response .plugin .summary @@ -931,6 +940,7 @@ async fn plugin_read_falls_back_to_local_share_context_without_remote_auth() -> .await??; let response: PluginReadResponse = to_response(response)?; + assert_eq!(response.plugin.summary.remote_plugin_id, None); let share_context = response .plugin .summary 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 b081017ad4..46f3dceb5e 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs @@ -162,7 +162,8 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> { PluginShareListResponse { data: vec![PluginShareListItem { plugin: PluginSummary { - id: "plugins_123".to_string(), + id: "demo-plugin@shared-with-me".to_string(), + remote_plugin_id: Some("plugins_123".to_string()), name: "demo-plugin".to_string(), share_context: Some(expected_share_context("plugins_123")), source: PluginSource::Remote, @@ -506,7 +507,8 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> { PluginShareListResponse { data: vec![PluginShareListItem { plugin: PluginSummary { - id: "plugins_123".to_string(), + id: "demo-plugin@shared-with-me".to_string(), + remote_plugin_id: Some("plugins_123".to_string()), name: "demo-plugin".to_string(), share_context: Some(expected_share_context("plugins_123")), source: PluginSource::Remote, @@ -725,7 +727,8 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> { PluginShareListResponse { data: vec![PluginShareListItem { plugin: PluginSummary { - id: "plugins_123".to_string(), + id: "demo-plugin@shared-with-me".to_string(), + remote_plugin_id: Some("plugins_123".to_string()), name: "demo-plugin".to_string(), share_context: Some(expected_share_context("plugins_123")), source: PluginSource::Remote, diff --git a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs index 5679234d2b..8fdf149a01 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs @@ -617,11 +617,17 @@ async fn mount_remote_plugin_detail_with_name( release_version: &str, scope: &str, ) { + let discoverability = if scope == "WORKSPACE" { + r#" + "discoverability": "LISTED","# + } else { + "" + }; let detail_body = format!( r#"{{ "id": "{remote_plugin_id}", "name": "{plugin_name}", - "scope": "{scope}", + "scope": "{scope}",{discoverability} "installation_policy": "AVAILABLE", "authentication_policy": "ON_USE", "release": {{ diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index ed05053045..bb0e13a79d 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -87,6 +87,7 @@ pub struct RemoteInstalledPlugin { #[derive(Debug, Clone, PartialEq)] pub struct RemotePluginSummary { pub id: String, + pub remote_plugin_id: String, pub name: String, pub share_context: Option, pub installed: bool, @@ -379,6 +380,32 @@ struct RemotePluginDirectoryItem { release: RemotePluginReleaseResponse, } +fn remote_plugin_canonical_marketplace_name( + plugin: &RemotePluginDirectoryItem, +) -> Result<&'static str, RemotePluginCatalogError> { + match plugin.scope { + RemotePluginScope::Global => Ok(REMOTE_GLOBAL_MARKETPLACE_NAME), + RemotePluginScope::Workspace => match workspace_plugin_discoverability(plugin)? { + RemotePluginShareDiscoverability::Listed => Ok(REMOTE_WORKSPACE_MARKETPLACE_NAME), + RemotePluginShareDiscoverability::Unlisted + | RemotePluginShareDiscoverability::Private => { + Ok(REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME) + } + }, + } +} + +fn workspace_plugin_discoverability( + plugin: &RemotePluginDirectoryItem, +) -> Result { + plugin.discoverability.ok_or_else(|| { + RemotePluginCatalogError::UnexpectedResponse(format!( + "workspace plugin `{}` did not include discoverability", + plugin.id + )) + }) +} + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] struct RemotePluginDirectorySharePrincipal { principal_type: RemotePluginSharePrincipalType, @@ -550,12 +577,9 @@ pub async fn fetch_remote_installed_plugins( let (global, workspace) = tokio::try_join!(global, workspace)?; let mut installed_plugins = [global, workspace] .into_iter() - .flat_map(|(scope, plugins)| { - plugins - .into_iter() - .map(move |plugin| remote_installed_plugin_to_info(scope, &plugin)) - }) - .collect::>(); + .flat_map(|(_scope, plugins)| plugins) + .map(|plugin| remote_installed_plugin_to_info(&plugin)) + .collect::, _>>()?; installed_plugins.sort_by(|left, right| { left.marketplace_name .cmp(&right.marketplace_name) @@ -655,7 +679,7 @@ async fn fetch_remote_plugin_detail_with_download_url_option( let auth = ensure_chatgpt_auth(auth)?; let plugin = fetch_plugin_detail(config, auth, plugin_id, include_download_urls).await?; let scope = plugin.scope; - let marketplace_name = scope.marketplace_name().to_string(); + let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string(); // Remote plugin IDs uniquely identify remote plugins, so the caller-provided // marketplace name is not validated here. The backend detail response is the // source of truth for the plugin's actual scope/marketplace. @@ -756,7 +780,7 @@ pub async fn uninstall_remote_plugin( config, auth, plugin_id, /*include_download_urls*/ false, ) .await?; - let marketplace_name = plugin.scope.marketplace_name().to_string(); + let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string(); let plugin_name = plugin.name; let base_url = config.chatgpt_base_url.trim_end_matches('/'); @@ -841,8 +865,17 @@ fn build_remote_plugin_summary( plugin: &RemotePluginDirectoryItem, installed_plugin: Option<&RemotePluginInstalledItem>, ) -> Result { + let marketplace_name = remote_plugin_canonical_marketplace_name(plugin)?; + let plugin_id = + PluginId::new(plugin.name.clone(), marketplace_name.to_string()).map_err(|err| { + RemotePluginCatalogError::UnexpectedResponse(format!( + "invalid remote plugin config id for `{}` in `{marketplace_name}`: {err}", + plugin.name + )) + })?; Ok(RemotePluginSummary { - id: plugin.id.clone(), + id: plugin_id.as_key(), + remote_plugin_id: plugin.id.clone(), name: plugin.name.clone(), share_context: remote_plugin_share_context(plugin)?, installed: installed_plugin.is_some(), @@ -861,12 +894,7 @@ fn remote_plugin_share_context( match plugin.scope { RemotePluginScope::Global => Ok(None), RemotePluginScope::Workspace => { - let discoverability = plugin.discoverability.ok_or_else(|| { - RemotePluginCatalogError::UnexpectedResponse(format!( - "workspace plugin `{}` did not include discoverability", - plugin.id - )) - })?; + let discoverability = workspace_plugin_discoverability(plugin)?; Ok(Some(RemotePluginShareContext { remote_plugin_id: plugin.id.clone(), discoverability, @@ -890,19 +918,18 @@ fn remote_plugin_share_context( } fn remote_installed_plugin_to_info( - scope: RemotePluginScope, installed_plugin: &RemotePluginInstalledItem, -) -> RemoteInstalledPlugin { +) -> Result { let plugin = &installed_plugin.plugin; // Remote per-skill disabled state (`disabled_skill_names`) is intentionally // not projected into skills/list yet; local skills.config remains the // supported source for skill enablement. - RemoteInstalledPlugin { - marketplace_name: scope.marketplace_name().to_string(), + Ok(RemoteInstalledPlugin { + marketplace_name: remote_plugin_canonical_marketplace_name(plugin)?.to_string(), id: plugin.id.clone(), name: plugin.name.clone(), enabled: installed_plugin.enabled, - } + }) } fn remote_plugin_interface_to_info(plugin: &RemotePluginDirectoryItem) -> Option { diff --git a/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs b/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs index 4e5caca294..26cdce343a 100644 --- a/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs +++ b/codex-rs/core-plugins/src/remote/remote_installed_plugin_sync.rs @@ -1,10 +1,12 @@ use super::REMOTE_GLOBAL_MARKETPLACE_NAME; +use super::REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME; use super::REMOTE_WORKSPACE_MARKETPLACE_NAME; use super::RemotePluginCatalogError; use super::RemotePluginScope; use super::RemotePluginServiceConfig; use super::ensure_chatgpt_auth; use super::fetch_installed_plugins_for_scope_with_download_url; +use super::remote_plugin_canonical_marketplace_name; use crate::store::PLUGINS_CACHE_DIR; use crate::store::PluginStore; use crate::store::PluginStoreError; @@ -150,14 +152,18 @@ pub async fn sync_remote_installed_plugin_bundles_once( REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(), BTreeSet::new(), ), + ( + REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(), + BTreeSet::new(), + ), ]); let mut installed_plugin_ids = BTreeSet::new(); let mut failed_remote_plugin_ids = BTreeSet::new(); - for (scope, installed_plugins) in [global, workspace] { - let marketplace_name = scope.marketplace_name().to_string(); + for (_scope, installed_plugins) in [global, workspace] { for installed_plugin in installed_plugins { let plugin = installed_plugin.plugin; + let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string(); installed_plugin_names_by_marketplace .entry(marketplace_name.clone()) .or_default() @@ -292,6 +298,7 @@ fn remove_stale_remote_plugin_caches( for marketplace_name in [ REMOTE_GLOBAL_MARKETPLACE_NAME, REMOTE_WORKSPACE_MARKETPLACE_NAME, + REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME, ] { let marketplace_root = codex_home.join(PLUGINS_CACHE_DIR).join(marketplace_name); if !marketplace_root.exists() { @@ -449,6 +456,10 @@ mod tests { REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(), BTreeSet::new(), ), + ( + REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(), + BTreeSet::new(), + ), ]); let guard = mark_remote_plugin_cache_mutation_in_flight( @@ -487,4 +498,42 @@ mod tests { assert_eq!(removed, vec!["linear@chatgpt-global".to_string()]); assert!(!cached_manifest.exists()); } + + #[test] + fn stale_remote_plugin_cleanup_removes_shared_with_me_cache() { + let codex_home = tempfile::tempdir().expect("create codex home"); + let cached_manifest = codex_home + .path() + .join(PLUGINS_CACHE_DIR) + .join(REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME) + .join("private-plugin") + .join("1.2.3") + .join(".codex-plugin") + .join("plugin.json"); + std::fs::create_dir_all(cached_manifest.parent().expect("manifest parent")) + .expect("create cached plugin manifest parent"); + std::fs::write(&cached_manifest, r#"{"name":"private-plugin"}"#) + .expect("write cached plugin manifest"); + let installed_plugin_names_by_marketplace = + BTreeMap::>::from_iter([ + (REMOTE_GLOBAL_MARKETPLACE_NAME.to_string(), BTreeSet::new()), + ( + REMOTE_WORKSPACE_MARKETPLACE_NAME.to_string(), + BTreeSet::new(), + ), + ( + REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME.to_string(), + BTreeSet::new(), + ), + ]); + + let removed = remove_stale_remote_plugin_caches( + codex_home.path(), + &installed_plugin_names_by_marketplace, + ) + .expect("cleanup shared-with-me cache"); + + assert_eq!(removed, vec!["private-plugin@shared-with-me".to_string()]); + assert!(!cached_manifest.exists()); + } } diff --git a/codex-rs/core-plugins/src/remote/share/tests.rs b/codex-rs/core-plugins/src/remote/share/tests.rs index 8791cde597..8b27b28c3f 100644 --- a/codex-rs/core-plugins/src/remote/share/tests.rs +++ b/codex-rs/core-plugins/src/remote/share/tests.rs @@ -93,6 +93,7 @@ fn remote_plugin_json(plugin_id: &str) -> serde_json::Value { "id": plugin_id, "name": "demo-plugin", "scope": "WORKSPACE", + "discoverability": "PRIVATE", "installation_policy": "AVAILABLE", "authentication_policy": "ON_USE", "release": { @@ -584,7 +585,8 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() { vec![ RemotePluginShareSummary { summary: RemotePluginSummary { - id: "plugins_123".to_string(), + id: "demo-plugin@shared-with-me".to_string(), + remote_plugin_id: "plugins_123".to_string(), name: "demo-plugin".to_string(), share_context: Some(RemotePluginShareContext { remote_plugin_id: "plugins_123".to_string(), @@ -621,7 +623,8 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() { }, RemotePluginShareSummary { summary: RemotePluginSummary { - id: "plugins_456".to_string(), + id: "demo-plugin@shared-with-me".to_string(), + remote_plugin_id: "plugins_456".to_string(), name: "demo-plugin".to_string(), share_context: Some(RemotePluginShareContext { remote_plugin_id: "plugins_456".to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index f11cd64843..f5007e3faf 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -1314,6 +1314,7 @@ pub(super) fn plugins_test_summary( ) -> PluginSummary { PluginSummary { id: id.to_string(), + remote_plugin_id: None, name: name.to_string(), share_context: None, source: PluginSource::Local {