diff --git a/CHANGELOG.md b/CHANGELOG.md index e76933d2d16..82dd2cc87f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/install: let official plugin reinstall recovery repair source-only installed runtime shadows, so `openclaw plugins install npm:@openclaw/discord --force` can replace the bad package instead of stopping at stale config validation. Thanks @vincentkoc. - Plugins/commands: allow the official ClawHub Codex plugin package to keep reserved `/codex` command ownership, matching the existing npm-managed Codex package behavior. Thanks @vincentkoc. - Plugins/commands: scope QQBot framework slash commands to the QQBot channel so `/bot-*` command handlers and native specs do not leak onto unrelated chat surfaces. Thanks @vincentkoc. - fix: harden backend message action gateway routing [AI]. (#76374) Thanks @pgondhi987. diff --git a/src/cli/plugin-install-config-policy.ts b/src/cli/plugin-install-config-policy.ts index aad7fcd8352..7deeab95ecb 100644 --- a/src/cli/plugin-install-config-policy.ts +++ b/src/cli/plugin-install-config-policy.ts @@ -11,7 +11,7 @@ import { import { resolveUserPath } from "../utils.js"; import { parseNpmPrefixSpec, resolveFileNpmSpecToLocalPath } from "./plugins-command-helpers.js"; -type PluginInstallInvalidConfigPolicy = "deny" | "allow-bundled-recovery"; +type PluginInstallInvalidConfigPolicy = "deny" | "allow-plugin-recovery"; export type PluginInstallRequestContext = { rawSpec: string; @@ -264,5 +264,5 @@ export function resolvePluginInstallInvalidConfigPolicy( if (!request) { return "deny"; } - return request.allowInvalidConfigRecovery === true ? "allow-bundled-recovery" : "deny"; + return request.allowInvalidConfigRecovery === true ? "allow-plugin-recovery" : "deny"; } diff --git a/src/cli/plugins-install-command.ts b/src/cli/plugins-install-command.ts index 32f29f0e310..ee7593898cd 100644 --- a/src/cli/plugins-install-command.ts +++ b/src/cli/plugins-install-command.ts @@ -421,7 +421,7 @@ function isTerminalPluginInstallSecurityFailure(code?: string): boolean { ); } -function isAllowedBundledRecoveryIssue( +function isAllowedPluginRecoveryIssue( issue: { path?: string; message?: string }, request: PluginInstallRequestContext, ): boolean { @@ -434,7 +434,10 @@ function isAllowedBundledRecoveryIssue( issue.message === `unknown channel id: ${pluginId}`) || (issue.path === "plugins.load.paths" && typeof issue.message === "string" && - issue.message.includes("plugin path not found")) + issue.message.includes("plugin path not found")) || + (issue.path === "plugins" && + typeof issue.message === "string" && + issue.message.includes("requires compiled runtime output")) ); } @@ -448,7 +451,7 @@ async function loadConfigFromSnapshotForInstall( request: PluginInstallRequestContext, snapshot: Awaited>, ): Promise { - if (resolvePluginInstallInvalidConfigPolicy(request) !== "allow-bundled-recovery") { + if (resolvePluginInstallInvalidConfigPolicy(request) !== "allow-plugin-recovery") { throw buildInvalidPluginInstallConfigError( "Config invalid; run `openclaw doctor --fix` before installing plugins.", ); @@ -462,11 +465,11 @@ async function loadConfigFromSnapshotForInstall( if ( snapshot.legacyIssues.length > 0 || snapshot.issues.length === 0 || - snapshot.issues.some((issue) => !isAllowedBundledRecoveryIssue(issue, request)) + snapshot.issues.some((issue) => !isAllowedPluginRecoveryIssue(issue, request)) ) { const pluginLabel = request.bundledPluginId ?? "the requested plugin"; throw buildInvalidPluginInstallConfigError( - `Config invalid outside the bundled recovery path for ${pluginLabel}; run \`openclaw doctor --fix\` before reinstalling it.`, + `Config invalid outside the plugin recovery path for ${pluginLabel}; run \`openclaw doctor --fix\` before reinstalling it.`, ); } let nextConfig = snapshot.config; diff --git a/src/cli/plugins-install-config.test.ts b/src/cli/plugins-install-config.test.ts index 4005f4d1365..c6c6831e00f 100644 --- a/src/cli/plugins-install-config.test.ts +++ b/src/cli/plugins-install-config.test.ts @@ -151,6 +151,36 @@ describe("loadConfigForInstall", () => { expect(result).toEqual({ config: snapshotCfg, baseHash: "abc" }); }); + it("allows official plugin reinstall recovery from source-only runtime shadows", async () => { + const snapshotCfg = { + plugins: { installs: { discord: { source: "npm", installPath: "/bad/discord" } } }, + } as unknown as OpenClawConfig; + readConfigFileSnapshotMock.mockResolvedValue( + makeSnapshot({ + parsed: { plugins: { installs: { discord: {} } } }, + config: snapshotCfg, + issues: [ + { + path: "plugins", + message: + "plugin: installed plugin package requires compiled runtime output for TypeScript entry index.ts: expected ./dist/index.js, ./dist/index.mjs, ./dist/index.cjs, index.js, index.mjs, index.cjs", + }, + ], + }), + ); + + const request = resolvePluginInstallRequestContext({ + rawSpec: "npm:@openclaw/discord", + }); + if (!request.ok) { + throw new Error(request.error); + } + + const result = await loadConfigForInstall(request.request); + expect(collectChannelDoctorStaleConfigMutationsMock).toHaveBeenCalledWith(snapshotCfg); + expect(result).toEqual({ config: snapshotCfg, baseHash: "abc" }); + }); + it("allows explicit repo-checkout bundled-plugin reinstall recovery", async () => { const snapshotCfg = { plugins: {} } as OpenClawConfig; readConfigFileSnapshotMock.mockResolvedValue( @@ -182,7 +212,7 @@ describe("loadConfigForInstall", () => { ); await expect(loadConfigForInstall(discordNpmRequest)).rejects.toThrow( - "Config invalid outside the bundled recovery path for discord", + "Config invalid outside the plugin recovery path for discord", ); }); diff --git a/src/cli/program/preaction.ts b/src/cli/program/preaction.ts index 0b733e4f18a..7ad621a1db6 100644 --- a/src/cli/program/preaction.ts +++ b/src/cli/program/preaction.ts @@ -37,7 +37,7 @@ function shouldAllowInvalidConfigForAction(actionCommand: Command, commandPath: commandPath, argv: process.argv, }), - ) === "allow-bundled-recovery" + ) === "allow-plugin-recovery" ); }