diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index a4d3194e6b..b618a66310 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2179,6 +2179,13 @@ }, "PluginInstallParams": { "properties": { + "installAttemptId": { + "description": "Client-generated identifier used to correlate one installation attempt.", + "type": [ + "string", + "null" + ] + }, "marketplacePath": { "anyOf": [ { 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 8edefd5ae1..07566dceb8 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 @@ -14507,6 +14507,13 @@ "PluginInstallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "installAttemptId": { + "description": "Client-generated identifier used to correlate one installation attempt.", + "type": [ + "string", + "null" + ] + }, "marketplacePath": { "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 697968b485..4915f919cc 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 @@ -10844,6 +10844,13 @@ "PluginInstallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "installAttemptId": { + "description": "Client-generated identifier used to correlate one installation attempt.", + "type": [ + "string", + "null" + ] + }, "marketplacePath": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginInstallParams.json b/codex-rs/app-server-protocol/schema/json/v2/PluginInstallParams.json index ad3c0c1079..9dae6eb1db 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginInstallParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginInstallParams.json @@ -7,6 +7,13 @@ } }, "properties": { + "installAttemptId": { + "description": "Client-generated identifier used to correlate one installation attempt.", + "type": [ + "string", + "null" + ] + }, "marketplacePath": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 1500e1b27d..0da5f9db67 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index f718a720a2..c231609cc3 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallParams.ts index 257dc47a1e..fae3fbdbf2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallParams.ts @@ -3,4 +3,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -export type PluginInstallParams = { marketplacePath?: AbsolutePathBuf | null, remoteMarketplaceName?: string | null, pluginName: string, }; +export type PluginInstallParams = { marketplacePath?: AbsolutePathBuf | null, remoteMarketplaceName?: string | null, +/** + * Client-generated identifier used to correlate one installation attempt. + */ +installAttemptId?: string | null, pluginName: string, }; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 2644966b3a..7e283e3a9f 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -2118,6 +2118,7 @@ mod tests { params: v2::PluginInstallParams { marketplace_path: Some(absolute_path("/tmp/marketplace")), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "plugin-a".to_string(), }, }; 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 6d0e011342..8c2e11df2a 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -893,6 +893,9 @@ pub struct PluginInstallParams { pub marketplace_path: Option, #[ts(optional = nullable)] pub remote_marketplace_name: Option, + /// Client-generated identifier used to correlate one installation attempt. + #[ts(optional = nullable)] + pub install_attempt_id: Option, pub plugin_name: String, } 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 4210907dab..3476094e46 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -3758,12 +3758,14 @@ fn plugin_install_params_serialization_omits_force_remote_sync() { serde_json::to_value(PluginInstallParams { marketplace_path: Some(marketplace_path.clone()), remote_marketplace_name: None, + install_attempt_id: Some("94c79f7b-cceb-4415-9a3e-b51b2f718d43".to_string()), plugin_name: "gmail".to_string(), }) .unwrap(), json!({ "marketplacePath": marketplace_path_json, "remoteMarketplaceName": null, + "installAttemptId": "94c79f7b-cceb-4415-9a3e-b51b2f718d43", "pluginName": "gmail", }), ); @@ -3778,6 +3780,7 @@ fn plugin_install_params_serialization_omits_force_remote_sync() { PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "gmail".to_string(), }, ); @@ -3792,6 +3795,7 @@ fn plugin_install_params_serialization_omits_force_remote_sync() { PluginInstallParams { marketplace_path: None, remote_marketplace_name: Some("openai-curated-remote".to_string()), + install_attempt_id: None, plugin_name: "gmail".to_string(), }, ); diff --git a/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs b/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs index c9f9653155..94ab863b20 100644 --- a/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs +++ b/codex-rs/app-server-test-client/src/plugin_analytics_mutation_smoke.rs @@ -349,6 +349,7 @@ fn install_remote_plugin(client: &mut CodexClient, plugin: &RemotePluginExpectat params: PluginInstallParams { marketplace_path: None, remote_marketplace_name: Some(plugin.marketplace_name.clone()), + install_attempt_id: None, plugin_name: plugin.remote_plugin_id.clone(), }, }, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 6f9eeddfc9..035fcffbb7 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -262,7 +262,7 @@ Example with notification opt-out: - `remoteControl/client/revoke` — experimental; revoke one controller device's grant for an environment. Pass `environmentId` and `clientId`; returns an empty object. This signed-in account-management operation works while the local relay is disabled or unenrolled. - `remoteControl/status/changed` — notification emitted when the remote-control status or client-visible environment id changes. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `serverName` is the local machine name used by this app-server process; `environmentId` is a string when the app-server has a current enrollment and `null` when that enrollment is cleared, invalidated, or remote control is disabled. Newly initialized app-server clients always receive the current status snapshot. - `skills/config/write` — write user-level skill config by name or absolute path. -- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**). +- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth. For remote installs, clients may include an optional `installAttemptId`; app-server forwards it unchanged as `install_attempt_id` in the backend POST body, while omission preserves the legacy empty-body request (**under development; do not call from production clients yet**). - `plugin/uninstall` — uninstall a local plugin by `pluginId` in `@` form by removing its cached files and clearing its user-level config entry, or uninstall a remote ChatGPT plugin by backend `pluginId` by forwarding the uninstall to the ChatGPT plugin backend and removing any downloaded remote-plugin cache (**under development; do not call from production clients yet**). - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; pass `threadId` to resolve servers from that thread's selected plugins and executor, and receive an `authorization_url` followed by `mcpServer/oauthLogin/completed` once the browser flow finishes. - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index d0f4f82458..ed9a224bf6 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -1479,13 +1479,18 @@ impl PluginRequestProcessor { let PluginInstallParams { marketplace_path, remote_marketplace_name, + install_attempt_id, plugin_name, } = params; let marketplace_path = match (marketplace_path, remote_marketplace_name) { (Some(marketplace_path), None) => marketplace_path, (None, Some(remote_marketplace_name)) => { return self - .remote_plugin_install_response(remote_marketplace_name, plugin_name) + .remote_plugin_install_response( + remote_marketplace_name, + plugin_name, + install_attempt_id, + ) .await; } (Some(_), Some(_)) | (None, None) => { @@ -1580,6 +1585,7 @@ impl PluginRequestProcessor { &self, remote_marketplace_name: String, remote_plugin_id: String, + install_attempt_id: Option, ) -> Result { let config = self.load_latest_config(/*fallback_cwd*/ None).await?; if !config.features.enabled(Feature::Plugins) { @@ -1701,13 +1707,24 @@ impl PluginRequestProcessor { // Cache first so a backend install cannot succeed when local materialization fails. // If this backend call fails, the cache entry is harmless because remote installed state // is still backend-gated. - let install_result = codex_core_plugins::remote::install_remote_plugin( - &remote_plugin_service_config, - auth.as_ref(), - &actual_remote_marketplace_name, - &remote_plugin_id, - ) - .await + let install_result = if let Some(install_attempt_id) = install_attempt_id.as_deref() { + codex_core_plugins::remote::install_remote_plugin_with_install_attempt_id( + &remote_plugin_service_config, + auth.as_ref(), + &actual_remote_marketplace_name, + &remote_plugin_id, + install_attempt_id, + ) + .await + } else { + codex_core_plugins::remote::install_remote_plugin( + &remote_plugin_service_config, + auth.as_ref(), + &actual_remote_marketplace_name, + &remote_plugin_id, + ) + .await + } .map_err(|err| { let error_type = remote_plugin_catalog_error_type(&err); let sub_error_type = err.sub_error_type(); diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index 38b85ee9cb..b08311fca5 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -66,6 +66,7 @@ use wiremock::matchers::query_param; // starts, which is noticeably slower on Windows CI. const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); const REMOTE_PLUGIN_ID: &str = "plugins~Plugin_00000000000000000000000000000000"; +const INSTALL_ATTEMPT_ID: &str = "94c79f7b-cceb-4415-9a3e-b51b2f718d43"; const TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS: &str = "CODEX_TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS"; @@ -112,6 +113,7 @@ async fn plugin_install_rejects_missing_install_source() -> Result<()> { .send_plugin_install_request(PluginInstallParams { marketplace_path: None, remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -146,6 +148,7 @@ async fn plugin_install_rejects_multiple_install_sources() -> Result<()> { codex_home.path().join("marketplace.json"), )?), remote_marketplace_name: Some("openai-curated-remote".to_string()), + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -184,6 +187,7 @@ plugins = false .send_plugin_install_request(PluginInstallParams { marketplace_path: None, remote_marketplace_name: Some("openai-curated-remote".to_string()), + install_attempt_id: None, plugin_name: "plugins~Plugin_22222222222222222222222222222222".to_string(), }) .await?; @@ -268,6 +272,10 @@ async fn plugin_install_writes_remote_plugin_to_cloud_and_cache() -> Result<()> /*expected_count*/ 1, ) .await?; + assert_eq!( + wait_for_remote_plugin_install_request_body(&server, REMOTE_PLUGIN_ID).await?, + Vec::::new() + ); wait_for_remote_plugin_request_count( &server, "GET", @@ -535,6 +543,7 @@ async fn plugin_install_rejects_invalid_remote_plugin_name() -> Result<()> { .send_plugin_install_request(PluginInstallParams { marketplace_path: None, remote_marketplace_name: Some("openai-curated-remote".to_string()), + install_attempt_id: None, plugin_name: "linear/../../oops".to_string(), }) .await?; @@ -840,6 +849,7 @@ async fn plugin_install_rejects_when_workspace_codex_plugins_disabled() -> Resul .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -874,6 +884,7 @@ async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() - codex_home.path().join("missing-marketplace.json"), )?), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "missing-plugin".to_string(), }) .await?; @@ -974,6 +985,7 @@ async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Re .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1027,6 +1039,7 @@ async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin() .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1079,6 +1092,7 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> { .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1144,6 +1158,7 @@ async fn plugin_install_failure_tracks_analytics_event() -> Result<()> { .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1196,10 +1211,21 @@ async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()> { .build_initialized_with_timeout(DEFAULT_TIMEOUT) .await?; - let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?; + let request_id = send_remote_plugin_install_request_with_attempt_id( + &mut mcp, + REMOTE_PLUGIN_ID, + INSTALL_ATTEMPT_ID, + ) + .await?; let response: PluginInstallResponse = timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; assert_eq!(response.apps_needing_auth, Vec::::new()); + let request_body = + wait_for_remote_plugin_install_request_body(&server, REMOTE_PLUGIN_ID).await?; + assert_eq!( + serde_json::from_slice::(&request_body)?, + json!({"install_attempt_id": INSTALL_ATTEMPT_ID}) + ); let payload = wait_for_plugin_analytics_payload(&server).await?; assert_eq!( @@ -1382,6 +1408,7 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> { .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1474,6 +1501,7 @@ async fn plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin() -> Res .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1527,6 +1555,7 @@ url = "https://example.com/allowed-mcp" .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1581,6 +1610,7 @@ enabled = false .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1681,6 +1711,7 @@ url = {executor_url} .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1742,6 +1773,7 @@ async fn plugin_install_starts_mcp_oauth_with_formerly_disallowed_plugin_app() - .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1850,6 +1882,7 @@ async fn plugin_install_starts_mcp_oauth_through_configured_http_proxy() -> Resu .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -1913,6 +1946,7 @@ connectors = true .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -2165,6 +2199,7 @@ async fn plugin_install_includes_formerly_disallowed_apps_needing_auth() -> Resu .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -2249,6 +2284,7 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests .send_plugin_install_request(PluginInstallParams { marketplace_path: Some(marketplace_path), remote_marketplace_name: None, + install_attempt_id: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -2890,11 +2926,47 @@ async fn send_remote_plugin_install_request( mcp.send_plugin_install_request(PluginInstallParams { marketplace_path: None, remote_marketplace_name: Some("caller-marketplace-is-ignored".to_string()), + install_attempt_id: None, plugin_name: remote_plugin_id.to_string(), }) .await } +async fn send_remote_plugin_install_request_with_attempt_id( + mcp: &mut TestAppServer, + remote_plugin_id: &str, + install_attempt_id: &str, +) -> Result { + mcp.send_plugin_install_request(PluginInstallParams { + marketplace_path: None, + remote_marketplace_name: Some("caller-marketplace-is-ignored".to_string()), + install_attempt_id: Some(install_attempt_id.to_string()), + plugin_name: remote_plugin_id.to_string(), + }) + .await +} + +async fn wait_for_remote_plugin_install_request_body( + server: &MockServer, + remote_plugin_id: &str, +) -> Result> { + let path_suffix = format!("/ps/plugins/{remote_plugin_id}/install"); + timeout(DEFAULT_TIMEOUT, async { + loop { + let Some(requests) = server.received_requests().await else { + bail!("wiremock did not record requests"); + }; + if let Some(request) = requests.iter().find(|request| { + request.method == "POST" && request.url.path().ends_with(&path_suffix) + }) { + return Ok::, anyhow::Error>(request.body.clone()); + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await? +} + async fn wait_for_remote_plugin_request_count( server: &MockServer, method_name: &str, diff --git a/codex-rs/core-plugins/src/remote.rs b/codex-rs/core-plugins/src/remote.rs index f55fcf5f86..b7d4c176d0 100644 --- a/codex-rs/core-plugins/src/remote.rs +++ b/codex-rs/core-plugins/src/remote.rs @@ -1475,11 +1475,50 @@ fn app_declarations_from_remote_app_ids(app_ids: &[String]) -> Vec { + install_attempt_id: &'a str, +} + pub async fn install_remote_plugin( + config: &RemotePluginServiceConfig, + auth: Option<&CodexAuth>, + marketplace_name: &str, + plugin_id: &str, +) -> Result { + install_remote_plugin_inner( + config, + auth, + marketplace_name, + plugin_id, + /*install_attempt_id*/ None, + ) + .await +} + +pub async fn install_remote_plugin_with_install_attempt_id( + config: &RemotePluginServiceConfig, + auth: Option<&CodexAuth>, + marketplace_name: &str, + plugin_id: &str, + install_attempt_id: &str, +) -> Result { + install_remote_plugin_inner( + config, + auth, + marketplace_name, + plugin_id, + Some(install_attempt_id), + ) + .await +} + +async fn install_remote_plugin_inner( config: &RemotePluginServiceConfig, auth: Option<&CodexAuth>, _marketplace_name: &str, plugin_id: &str, + install_attempt_id: Option<&str>, ) -> Result { let auth = ensure_chatgpt_auth(auth)?; // Remote plugin IDs uniquely identify remote plugins, so the caller-provided @@ -1492,6 +1531,11 @@ pub async fn install_remote_plugin( .append_pair("includeAppsNeedingAuth", "true"); let url = url.to_string(); let request = authenticated_request(config.http_request(Method::POST, &url), auth); + let request = if let Some(install_attempt_id) = install_attempt_id { + request.json(&RemotePluginInstallRequest { install_attempt_id }) + } else { + request + }; let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?; if response.id != plugin_id { return Err(RemotePluginCatalogError::UnexpectedPluginId { diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index d2b2c075d9..c590acfd5f 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -1134,6 +1134,7 @@ pub(super) async fn fetch_plugin_install( params: PluginInstallParams { marketplace_path, remote_marketplace_name, + install_attempt_id: None, plugin_name, }, })