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
@@ -1280,6 +1280,10 @@
},
"PluginInstallParams": {
"properties": {
"forceRemoteSync": {
"description": "When true, apply the remote plugin change before the local install flow.",
"type": "boolean"
},
"marketplacePath": {
"$ref": "#/definitions/AbsolutePathBuf"
},
@@ -1329,6 +1333,10 @@
},
"PluginUninstallParams": {
"properties": {
"forceRemoteSync": {
"description": "When true, apply the remote plugin change before the local uninstall flow.",
"type": "boolean"
},
"pluginId": {
"type": "string"
}
@@ -9180,6 +9180,10 @@
"PluginInstallParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"forceRemoteSync": {
"description": "When true, apply the remote plugin change before the local install flow.",
"type": "boolean"
},
"marketplacePath": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
@@ -9500,6 +9504,10 @@
"PluginUninstallParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"forceRemoteSync": {
"description": "When true, apply the remote plugin change before the local uninstall flow.",
"type": "boolean"
},
"pluginId": {
"type": "string"
}
@@ -5968,6 +5968,10 @@
"PluginInstallParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"forceRemoteSync": {
"description": "When true, apply the remote plugin change before the local install flow.",
"type": "boolean"
},
"marketplacePath": {
"$ref": "#/definitions/AbsolutePathBuf"
},
@@ -6288,6 +6292,10 @@
"PluginUninstallParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"forceRemoteSync": {
"description": "When true, apply the remote plugin change before the local uninstall flow.",
"type": "boolean"
},
"pluginId": {
"type": "string"
}
@@ -7,6 +7,10 @@
}
},
"properties": {
"forceRemoteSync": {
"description": "When true, apply the remote plugin change before the local install flow.",
"type": "boolean"
},
"marketplacePath": {
"$ref": "#/definitions/AbsolutePathBuf"
},
@@ -1,6 +1,10 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"forceRemoteSync": {
"description": "When true, apply the remote plugin change before the local uninstall flow.",
"type": "boolean"
},
"pluginId": {
"type": "string"
}
@@ -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, pluginName: string, };
export type PluginInstallParams = { marketplacePath: AbsolutePathBuf, pluginName: string,
/**
* When true, apply the remote plugin change before the local install flow.
*/
forceRemoteSync?: boolean, };
@@ -2,4 +2,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PluginUninstallParams = { pluginId: string, };
export type PluginUninstallParams = { pluginId: string,
/**
* When true, apply the remote plugin change before the local uninstall flow.
*/
forceRemoteSync?: boolean, };
@@ -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 {