[codex] Support remote plugin install writes (#18917)

## Summary
- Add a remote plugin install write call that POSTs the selected remote
plugin to the ChatGPT cloud plugin API.
- Align remote install with the latest remote read contract:
`pluginName` carries the backend remote plugin id directly, for example
`plugins~Plugin_linear`, and install no longer synthesizes
`<name>@<marketplace>` ids.
- Validate remote install ids with the same character rules as remote
read, return the same install response shape as local installs, and
include mocked app-server coverage for the write path.

## Validation
- `just fmt`
- `cargo test -p codex-app-server --test all plugin_install`
- `cargo test -p codex-core-plugins`
- `just fix -p codex-app-server`
- `just fix -p codex-core-plugins`
This commit is contained in:
xli-oai
2026-04-23 22:10:15 -07:00
committed by GitHub
Unverified
parent 19badb0be2
commit 33cc135cc3
3 changed files with 449 additions and 75 deletions
+55
View File
@@ -107,6 +107,20 @@ pub enum RemotePluginCatalogError {
expected_marketplace_name: String,
actual_marketplace_name: String,
},
#[error(
"remote plugin install returned unexpected plugin id: expected `{expected}`, got `{actual}`"
)]
UnexpectedPluginId { expected: String, actual: String },
#[error(
"remote plugin install returned unexpected enabled state for `{plugin_id}`: expected {expected_enabled}, got {actual_enabled}"
)]
UnexpectedEnabledState {
plugin_id: String,
expected_enabled: bool,
actual_enabled: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
@@ -258,6 +272,12 @@ struct RemotePluginInstalledResponse {
pagination: RemotePluginPagination,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct RemotePluginInstallResponse {
id: String,
enabled: bool,
}
pub async fn fetch_remote_marketplaces(
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
@@ -418,6 +438,41 @@ pub async fn fetch_remote_plugin_detail(
})
}
pub async fn install_remote_plugin(
config: &RemotePluginServiceConfig,
auth: Option<&CodexAuth>,
marketplace_name: &str,
plugin_id: &str,
) -> Result<(), RemotePluginCatalogError> {
let auth = ensure_chatgpt_auth(auth)?;
if RemotePluginScope::from_marketplace_name(marketplace_name).is_none() {
return Err(RemotePluginCatalogError::UnknownMarketplace {
marketplace_name: marketplace_name.to_string(),
});
}
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/ps/plugins/{plugin_id}/install");
let client = build_reqwest_client();
let request = authenticated_request(client.post(&url), auth)?;
let response: RemotePluginInstallResponse = send_and_decode(request, &url).await?;
if response.id != plugin_id {
return Err(RemotePluginCatalogError::UnexpectedPluginId {
expected: plugin_id.to_string(),
actual: response.id,
});
}
if !response.enabled {
return Err(RemotePluginCatalogError::UnexpectedEnabledState {
plugin_id: plugin_id.to_string(),
expected_enabled: true,
actual_enabled: response.enabled,
});
}
Ok(())
}
fn build_remote_plugin_summary(
plugin: &RemotePluginDirectoryItem,
installed_plugin: Option<&RemotePluginInstalledItem>,