feat: support remote_sync for plugin install/uninstall. (#14878)

- Added forceRemoteSync to plugin/install and plugin/uninstall.
- With forceRemoteSync=true, we update the remote plugin status first,
then apply the local change only if the backend call succeeds.
- Kept plugin/list(forceRemoteSync=true) as the main recon path, and for
now it treats remote enabled=false as uninstall. We
will eventually migrate to plugin/installed for more precise state
handling.
This commit is contained in:
xl-openai
2026-03-16 21:37:27 -07:00
committed by GitHub
parent 49c2b66ece
commit 1d85fe79ed
17 changed files with 743 additions and 101 deletions

View File

@@ -3396,6 +3396,9 @@ pub struct SkillsConfigWriteResponse {
pub struct PluginInstallParams {
pub marketplace_path: AbsolutePathBuf,
pub plugin_name: String,
/// When true, apply the remote plugin change before the local install flow.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub force_remote_sync: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -3411,6 +3414,9 @@ pub struct PluginInstallResponse {
#[ts(export_to = "v2/")]
pub struct PluginUninstallParams {
pub plugin_id: String,
/// When true, apply the remote plugin change before the local uninstall flow.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub force_remote_sync: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -7571,6 +7577,69 @@ mod tests {
);
}
#[test]
fn plugin_install_params_serialization_uses_force_remote_sync() {
let marketplace_path = if cfg!(windows) {
r"C:\plugins\marketplace.json"
} else {
"/plugins/marketplace.json"
};
let marketplace_path = AbsolutePathBuf::try_from(PathBuf::from(marketplace_path)).unwrap();
let marketplace_path_json = marketplace_path.as_path().display().to_string();
assert_eq!(
serde_json::to_value(PluginInstallParams {
marketplace_path: marketplace_path.clone(),
plugin_name: "gmail".to_string(),
force_remote_sync: false,
})
.unwrap(),
json!({
"marketplacePath": marketplace_path_json,
"pluginName": "gmail",
}),
);
assert_eq!(
serde_json::to_value(PluginInstallParams {
marketplace_path,
plugin_name: "gmail".to_string(),
force_remote_sync: true,
})
.unwrap(),
json!({
"marketplacePath": marketplace_path_json,
"pluginName": "gmail",
"forceRemoteSync": true,
}),
);
}
#[test]
fn plugin_uninstall_params_serialization_uses_force_remote_sync() {
assert_eq!(
serde_json::to_value(PluginUninstallParams {
plugin_id: "gmail@openai-curated".to_string(),
force_remote_sync: false,
})
.unwrap(),
json!({
"pluginId": "gmail@openai-curated",
}),
);
assert_eq!(
serde_json::to_value(PluginUninstallParams {
plugin_id: "gmail@openai-curated".to_string(),
force_remote_sync: true,
})
.unwrap(),
json!({
"pluginId": "gmail@openai-curated",
"forceRemoteSync": true,
}),
);
}
#[test]
fn codex_error_info_serializes_http_status_code_in_camel_case() {
let value = CodexErrorInfo::ResponseTooManyFailedAttempts {