diff --git a/src/gateway/server-methods/plugins.test.ts b/src/gateway/server-methods/plugins.test.ts index 18c228f9ce39..0e0d226c14d0 100644 --- a/src/gateway/server-methods/plugins.test.ts +++ b/src/gateway/server-methods/plugins.test.ts @@ -7,6 +7,7 @@ import type { InstallPolicyWarningOccurrence, } from "../../plugins/install-security-scan.types.js"; import { parseInstallPolicyResponse } from "../../security/install-policy-response.js"; +import { drainGlobalSingletonLifecycleState } from "../../shared/global-singleton.js"; type InstallPolicyWarningScanIdentity = InstallPolicyWarningOccurrence["scan"]; type ManagementServiceModule = typeof import("../../plugins/management-service.js"); @@ -559,6 +560,47 @@ describe("plugin management Gateway handlers", () => { expect(managementMocks.install).toHaveBeenCalledOnce(); }); + it("revokes install-policy acknowledgements when the Gateway restarts", async () => { + managementMocks.install.mockRejectedValueOnce( + new ManagedPluginLifecycleError("Install requires approval", { + installPolicyResolvedRequest: { + source: "official", + spec: "@openclaw/diffs@1.0.0", + pluginId: "diffs", + mode: "install", + }, + installPolicyWarning: warningOccurrence({ + targetName: "diffs", + targetType: "plugin", + requestMode: "install", + reason: "Review required", + }), + }), + ); + const warning = await callHandler("plugins.install", { + source: "official", + pluginId: "diffs", + }); + const acknowledgementToken = expectDefined( + (warning.error as { details?: { acknowledgementToken?: unknown } }).details + ?.acknowledgementToken, + "expected install-policy acknowledgement token", + ); + + await drainGlobalSingletonLifecycleState("restart"); + const retry = await callHandler("plugins.install", { + source: "official", + pluginId: "diffs", + installPolicyWarningAcknowledgement: acknowledgementToken, + }); + + expect(retry.error).toMatchObject({ + code: "INVALID_REQUEST", + message: expect.stringContaining("expired or does not match this plugin"), + }); + expect(managementMocks.install).toHaveBeenCalledOnce(); + }); + it("carries earlier approvals into a token for a later scan-stage warning", async () => { const warning: InstallPolicyWarningDetails = { targetName: "demo-plugin", diff --git a/src/gateway/server-methods/plugins.ts b/src/gateway/server-methods/plugins.ts index 1be23617ac80..ad643ae7d715 100644 --- a/src/gateway/server-methods/plugins.ts +++ b/src/gateway/server-methods/plugins.ts @@ -27,6 +27,7 @@ import { type ManagedPluginInstallRequest, type ManagedPluginSourceInstallRequest, } from "../../plugins/management-service.js"; +import { resolveGlobalMap } from "../../shared/global-singleton.js"; import { buildGatewayReloadPlan } from "../config-reload-plan.js"; import { resolveGatewayReloadSettings } from "../config-reload-settings.js"; import { readInstallPolicyWarningErrorDetails } from "../install-policy-warning-error-details.js"; @@ -43,7 +44,10 @@ type InstallPolicyAcknowledgement = { warnings: InstallPolicyWarningOccurrence[]; }; -const installPolicyAcknowledgements = new Map(); +const installPolicyAcknowledgements = resolveGlobalMap( + Symbol.for("openclaw.installPolicyAcknowledgements"), + "close-and-restart", +); function installPolicyRequestKey(request: PluginsInstallParams): string { return request.source === "clawhub"