fix(plugins): recover source-only install shadows

This commit is contained in:
Vincent Koc
2026-05-04 03:26:54 -07:00
parent 4c40686f9e
commit 51d3ec7395
5 changed files with 43 additions and 9 deletions

View File

@@ -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.

View File

@@ -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";
}

View File

@@ -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<ReturnType<typeof readConfigFileSnapshot>>,
): Promise<ConfigSnapshotForInstallPersist> {
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;

View File

@@ -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",
);
});

View File

@@ -37,7 +37,7 @@ function shouldAllowInvalidConfigForAction(actionCommand: Command, commandPath:
commandPath,
argv: process.argv,
}),
) === "allow-bundled-recovery"
) === "allow-plugin-recovery"
);
}