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 2c75383f6b..3bb5973e8b 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 @@ -14779,6 +14779,13 @@ "null" ] }, + "mustShowInstallationInterstitial": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, "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 165718d7af..c5f3a63f27 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 @@ -11136,6 +11136,13 @@ "null" ] }, + "mustShowInstallationInterstitial": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, "name": { "type": "string" }, 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 f39b0f19b3..cc73ad1e5c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginInstalledResponse.json @@ -534,6 +534,13 @@ "null" ] }, + "mustShowInstallationInterstitial": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, "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 2f0c6e4b40..c5cf432c39 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json @@ -534,6 +534,13 @@ "null" ] }, + "mustShowInstallationInterstitial": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, "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 85d5c59306..b07eb287dd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json @@ -684,6 +684,13 @@ "null" ] }, + "mustShowInstallationInterstitial": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, "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 32bf946e38..d671a6581e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginShareListResponse.json @@ -490,6 +490,13 @@ "null" ] }, + "mustShowInstallationInterstitial": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, "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 10fd5aa25f..b02eba161b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSummary.ts @@ -25,7 +25,7 @@ localVersion: string | null, name: string, /** * Remote sharing context associated with this plugin when available. */ -shareContext: PluginShareContext | null, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, installPolicySource: PluginInstallPolicySource | null, authPolicy: PluginAuthPolicy, +shareContext: PluginShareContext | null, source: PluginSource, installed: boolean, enabled: boolean, installPolicy: PluginInstallPolicy, installPolicySource: PluginInstallPolicySource | null, mustShowInstallationInterstitial: boolean | null, authPolicy: PluginAuthPolicy, /** * Availability state for installing and using the plugin. */ 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 6cec32b68d..70ca587222 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -625,6 +625,8 @@ pub struct PluginSummary { pub enabled: bool, pub install_policy: PluginInstallPolicy, pub install_policy_source: Option, + #[serde(default)] + pub must_show_installation_interstitial: Option, pub auth_policy: PluginAuthPolicy, /// Availability state for installing and using the plugin. #[serde(default)] 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 e61f7ed13e..fd2a28ddde 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3680,6 +3680,7 @@ fn plugin_share_list_response_serializes_share_items() { enabled: false, install_policy: PluginInstallPolicy::Available, install_policy_source: Some(PluginInstallPolicySource::WorkspaceSetting), + must_show_installation_interstitial: None, auth_policy: PluginAuthPolicy::OnUse, availability: PluginAvailability::Available, interface: None, @@ -3703,6 +3704,7 @@ fn plugin_share_list_response_serializes_share_items() { "enabled": false, "installPolicy": "AVAILABLE", "installPolicySource": "WORKSPACE_SETTING", + "mustShowInstallationInterstitial": null, "authPolicy": "ON_USE", "availability": "AVAILABLE", "interface": null, @@ -3731,6 +3733,7 @@ fn plugin_summary_defaults_missing_availability_to_available() { assert_eq!(summary.availability, PluginAvailability::Available); assert_eq!(summary.local_version, None); assert_eq!(summary.share_context, None); + assert_eq!(summary.must_show_installation_interstitial, None); } #[test] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 5c1cf0c2b8..1b6c6ca6c1 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -215,7 +215,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, nullable remote install-policy provenance in `installPolicySource` (`WORKSPACE_SETTING` or `IMPLICIT_CANONICAL_APP`), 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/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, nullable remote install-policy provenance in `installPolicySource` (`WORKSPACE_SETTING` or `IMPLICIT_CANONICAL_APP`), 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. Every `PluginSummary` returned by plugin list, installed, read, and share-list methods includes `mustShowInstallationInterstitial`: remote service values preserve `true` or `false`, while local plugins and remote responses that omit the policy return `null`. Clients should fail closed when the value is `null`. 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. Remote rows include nullable `installPolicySource`; local rows return `null`. 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 can include scheduled task summaries from the catalog; `scheduledTasks: null` means the metadata is unavailable, while an empty array means the catalog found no scheduled tasks. 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 bde0ad585f..b6338030ee 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -167,6 +167,7 @@ fn convert_configured_marketplace_plugin_to_plugin_summary( source: marketplace_plugin_source_to_info(plugin.source), install_policy: plugin.policy.installation.into(), install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: plugin.policy.authentication.into(), availability: PluginAvailability::Available, interface: plugin.interface.map(local_plugin_interface_to_info), @@ -1091,6 +1092,7 @@ impl PluginRequestProcessor { enabled: outcome.plugin.enabled, install_policy: outcome.plugin.policy.installation.into(), install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: outcome.plugin.policy.authentication.into(), availability: PluginAvailability::Available, interface: outcome.plugin.interface.map(local_plugin_interface_to_info), @@ -2181,6 +2183,7 @@ fn remote_plugin_summary_to_info(summary: RemoteCatalogPluginSummary) -> PluginS enabled: summary.enabled, install_policy: summary.install_policy, install_policy_source: summary.install_policy_source, + must_show_installation_interstitial: summary.must_show_installation_interstitial, auth_policy: summary.auth_policy, availability: summary.availability, interface: summary.interface, 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 28dea3c10a..91e409e3e1 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -253,6 +253,8 @@ enabled = true let mut global_installed_body: serde_json::Value = serde_json::from_str( &remote_installed_plugin_body("", "1.2.3", /*enabled*/ true), )?; + global_installed_body["plugins"][0]["must_show_installation_interstitial"] = + serde_json::json!(false); let mut remote_only = global_installed_body["plugins"][0].clone(); remote_only["id"] = serde_json::json!("plugins~Plugin_11111111111111111111111111111111"); remote_only["name"] = serde_json::json!("remote-only"); @@ -310,16 +312,24 @@ enabled = true remote_marketplace .plugins .iter() - .map(|plugin| (plugin.id.clone(), plugin.install_policy_source)) + .map(|plugin| { + ( + plugin.id.clone(), + plugin.install_policy_source, + plugin.must_show_installation_interstitial, + ) + }) .collect::>(), vec![ ( "linear@openai-curated-remote".to_string(), Some(PluginInstallPolicySource::WorkspaceSetting), + Some(false), ), ( "remote-only@openai-curated-remote".to_string(), Some(PluginInstallPolicySource::WorkspaceSetting), + Some(false), ), ] ); @@ -499,6 +509,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_ enabled: false, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: PluginAuthPolicy::OnInstall, availability: codex_app_server_protocol::PluginAvailability::Available, interface: None, @@ -797,6 +808,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab enabled: false, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: PluginAuthPolicy::OnInstall, availability: codex_app_server_protocol::PluginAvailability::Available, interface: Some(codex_app_server_protocol::PluginInterface { @@ -838,6 +850,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab enabled: false, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: PluginAuthPolicy::OnInstall, availability: codex_app_server_protocol::PluginAvailability::Available, interface: None, @@ -1753,6 +1766,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() - "scope": "GLOBAL", "installation_policy": "AVAILABLE", "installation_policy_source": "IMPLICIT_CANONICAL_APP", + "must_show_installation_interstitial": true, "authentication_policy": "ON_USE", "status": "ENABLED", "release": { @@ -1793,6 +1807,7 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() - "scope": "GLOBAL", "installation_policy": "AVAILABLE", "installation_policy_source": "WORKSPACE_SETTING", + "must_show_installation_interstitial": false, "authentication_policy": "ON_USE", "status": "ENABLED", "release": { @@ -1924,6 +1939,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() - remote_marketplace.plugins[0].install_policy_source, Some(PluginInstallPolicySource::ImplicitCanonicalApp) ); + assert_eq!( + remote_marketplace.plugins[0].must_show_installation_interstitial, + Some(true) + ); assert_eq!( remote_marketplace.plugins[0].availability, codex_app_server_protocol::PluginAvailability::Available @@ -1963,6 +1982,10 @@ async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() - cached_catalog["plugins"][0]["installation_policy_source"], serde_json::json!("IMPLICIT_CANONICAL_APP") ); + assert_eq!( + cached_catalog["plugins"][0]["must_show_installation_interstitial"], + serde_json::json!(true) + ); assert_eq!( cached_catalog["plugins"][0]["release"]["interface"]["default_prompts"], serde_json::json!(["Create a Linear issue", "Review my Linear projects"]) @@ -2057,6 +2080,10 @@ async fn plugin_list_uses_cached_global_remote_catalog_and_refreshes_it() -> Res remote_marketplace.plugins[0].id, "linear@openai-curated-remote" ); + assert_eq!( + remote_marketplace.plugins[0].must_show_installation_interstitial, + None + ); wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 1).await?; wait_for_cached_remote_catalog_plugin_ids(codex_home.path(), &[cached_remote_plugin_id]) .await?; 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 b76158c172..aa0870ce71 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -157,6 +157,7 @@ apps = true "scope": "GLOBAL", "installation_policy": "AVAILABLE", "installation_policy_source": "IMPLICIT_CANONICAL_APP", + "must_show_installation_interstitial": true, "authentication_policy": "ON_USE", "release": { "version": "1.2.1", @@ -309,6 +310,10 @@ apps = true response.plugin.summary.install_policy_source, Some(PluginInstallPolicySource::ImplicitCanonicalApp) ); + assert_eq!( + response.plugin.summary.must_show_installation_interstitial, + Some(true) + ); assert_eq!( response .plugin 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 493e6ae4e3..b8350536ee 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_share.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_share.rs @@ -184,6 +184,7 @@ async fn plugin_share_save_uploads_local_plugin() -> Result<()> { enabled: true, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: Some(false), auth_policy: PluginAuthPolicy::OnUse, availability: codex_app_server_protocol::PluginAvailability::Available, interface: Some(expected_plugin_interface()), @@ -613,6 +614,7 @@ async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()> { enabled: true, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: Some(false), auth_policy: PluginAuthPolicy::OnUse, availability: codex_app_server_protocol::PluginAvailability::Available, interface: Some(expected_plugin_interface()), @@ -1230,6 +1232,7 @@ async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()> { enabled: true, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: Some(false), auth_policy: PluginAuthPolicy::OnUse, availability: codex_app_server_protocol::PluginAvailability::Available, interface: Some(expected_plugin_interface()), @@ -1357,6 +1360,7 @@ fn remote_plugin_json(plugin_id: &str) -> serde_json::Value { } ], "installation_policy": "AVAILABLE", + "must_show_installation_interstitial": false, "authentication_policy": "ON_USE", "release": { "version": "0.1.0", diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index fbb518e91f..a975623620 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -782,6 +782,7 @@ fn remote_installed_plugin_in_marketplace( enabled: true, install_policy: codex_app_server_protocol::PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: codex_app_server_protocol::PluginAuthPolicy::OnUse, availability: codex_app_server_protocol::PluginAvailability::Available, interface: None, diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index 52a57330c8..3a907afa22 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -156,6 +156,7 @@ pub struct RemoteInstalledPlugin { pub enabled: bool, pub install_policy: PluginInstallPolicy, pub install_policy_source: Option, + pub must_show_installation_interstitial: Option, pub auth_policy: PluginAuthPolicy, pub availability: PluginAvailability, pub interface: Option, @@ -174,6 +175,7 @@ pub struct RemotePluginSummary { pub enabled: bool, pub install_policy: PluginInstallPolicy, pub install_policy_source: Option, + pub must_show_installation_interstitial: Option, pub auth_policy: PluginAuthPolicy, pub availability: PluginAvailability, pub interface: Option, @@ -572,6 +574,8 @@ struct RemotePluginDirectoryItem { share_principals: Option>, installation_policy: PluginInstallPolicy, installation_policy_source: Option, + #[serde(default)] + must_show_installation_interstitial: Option, authentication_policy: PluginAuthPolicy, #[serde(rename = "status", default)] availability: PluginAvailability, @@ -1079,6 +1083,7 @@ pub fn group_remote_installed_plugins_by_marketplaces( enabled: plugin.enabled, install_policy: plugin.install_policy, install_policy_source: plugin.install_policy_source, + must_show_installation_interstitial: plugin.must_show_installation_interstitial, auth_policy: plugin.auth_policy, availability: plugin.availability, interface: plugin.interface.clone(), @@ -1516,6 +1521,7 @@ fn build_remote_plugin_summary( install_policy_source: plugin .installation_policy_source .and_then(RemotePluginInstallPolicySource::into_protocol), + must_show_installation_interstitial: plugin.must_show_installation_interstitial, auth_policy: plugin.authentication_policy, availability: plugin.availability, interface: remote_plugin_interface_to_info(plugin), @@ -1598,6 +1604,7 @@ fn remote_installed_plugin_to_cache_entry( install_policy_source: plugin .installation_policy_source .and_then(RemotePluginInstallPolicySource::into_protocol), + must_show_installation_interstitial: plugin.must_show_installation_interstitial, auth_policy: plugin.authentication_policy, availability: plugin.availability, interface: remote_plugin_interface_to_info(plugin), diff --git a/codex-rs/core-plugins/src/remote/share/tests.rs b/codex-rs/core-plugins/src/remote/share/tests.rs index e9887370c5..d9b036889a 100644 --- a/codex-rs/core-plugins/src/remote/share/tests.rs +++ b/codex-rs/core-plugins/src/remote/share/tests.rs @@ -649,6 +649,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() { enabled: false, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: PluginAuthPolicy::OnUse, availability: PluginAvailability::Available, interface: Some(expected_plugin_interface()), @@ -689,6 +690,7 @@ async fn list_remote_plugin_shares_fetches_created_workspace_plugins() { enabled: true, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: PluginAuthPolicy::OnUse, availability: PluginAvailability::Available, interface: Some(expected_plugin_interface()), diff --git a/codex-rs/core-plugins/src/remote_tests.rs b/codex-rs/core-plugins/src/remote_tests.rs index ca34a86b50..cd6831a2ea 100644 --- a/codex-rs/core-plugins/src/remote_tests.rs +++ b/codex-rs/core-plugins/src/remote_tests.rs @@ -81,6 +81,64 @@ fn installation_policy_source_is_preserved_across_remote_summary_paths() { ); } +#[test] +fn installation_interstitial_requirement_is_preserved_across_remote_summary_paths() { + let mut directory_plugin = directory_plugin("plugin-linear", "linear"); + directory_plugin.must_show_installation_interstitial = Some(true); + let marketplace = build_remote_marketplace( + REMOTE_GLOBAL_MARKETPLACE_NAME, + REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME, + vec![directory_plugin.clone()], + Vec::new(), + /*include_installed_only*/ false, + ) + .expect("marketplace should be valid") + .expect("marketplace should not be empty"); + assert_eq!( + marketplace + .plugins + .into_iter() + .map(|plugin| plugin.must_show_installation_interstitial) + .collect::>(), + vec![Some(true)] + ); + + directory_plugin.must_show_installation_interstitial = Some(false); + let installed_plugin = remote_installed_plugin_to_cache_entry(&RemotePluginInstalledItem { + plugin: directory_plugin, + enabled: true, + disabled_skill_names: Vec::new(), + }) + .expect("installed plugin should be valid"); + let marketplaces = group_remote_installed_plugins_by_marketplaces( + &[installed_plugin], + &[REMOTE_GLOBAL_MARKETPLACE_NAME], + ); + assert_eq!( + marketplaces + .into_iter() + .flat_map(|marketplace| marketplace.plugins) + .map(|plugin| plugin.must_show_installation_interstitial) + .collect::>(), + vec![Some(false)] + ); +} + +#[test] +fn missing_installation_interstitial_requirement_deserializes_to_none() { + let plugin = directory_plugin("plugin-linear", "linear"); + let mut plugin_json = serde_json::to_value(plugin).expect("plugin should serialize"); + plugin_json + .as_object_mut() + .expect("plugin should serialize to an object") + .remove("must_show_installation_interstitial"); + + let plugin: RemotePluginDirectoryItem = + serde_json::from_value(plugin_json).expect("missing requirement should deserialize"); + + assert_eq!(plugin.must_show_installation_interstitial, None); +} + #[test] fn unknown_installation_policy_source_maps_to_none() { let plugin = directory_plugin("plugin-linear", "linear"); @@ -126,6 +184,7 @@ fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem { share_principals: None, installation_policy: PluginInstallPolicy::Available, installation_policy_source: None, + must_show_installation_interstitial: None, authentication_policy: PluginAuthPolicy::OnUse, availability: PluginAvailability::Available, release: RemotePluginReleaseResponse { diff --git a/codex-rs/tui/src/app/plugin_mentions.rs b/codex-rs/tui/src/app/plugin_mentions.rs index cae0caa8f0..b59c142e3d 100644 --- a/codex-rs/tui/src/app/plugin_mentions.rs +++ b/codex-rs/tui/src/app/plugin_mentions.rs @@ -174,6 +174,7 @@ mod tests { enabled: true, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: PluginAuthPolicy::OnInstall, availability: PluginAvailability::Available, interface: None, diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index a4bd15eaeb..918c00c40d 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -1342,6 +1342,7 @@ pub(super) fn plugins_test_summary( enabled, install_policy, install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: PluginAuthPolicy::OnInstall, availability: PluginAvailability::Available, interface: Some(plugins_test_interface( @@ -1372,6 +1373,7 @@ pub(super) fn plugins_test_remote_summary( enabled: true, install_policy: PluginInstallPolicy::Available, install_policy_source: None, + must_show_installation_interstitial: None, auth_policy: PluginAuthPolicy::OnInstall, availability: PluginAvailability::Available, interface: Some(plugins_test_interface(