diff --git a/CHANGELOG.md b/CHANGELOG.md index 21321b1308b..285ff042434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai - Plugins/update: include beta-channel fallback details in plugin update outcomes when `@beta` is unavailable and OpenClaw uses the recorded default/latest plugin spec, making mixed beta/latest plugin cohorts visible in update summaries. Fixes #80689. Thanks @BKF-Gitty. - Control UI/performance: scope Nodes polling to the active Nodes tab, debounce stale session-list reconciliation, and bound chat-side session refreshes so long-running dashboards avoid background reload churn. Thanks @BunsDev. +- Plugins/channels: explain bundled channel entry files that reach the legacy plugin loader as setup-runtime loader mismatches instead of generic missing-register failures. Thanks @chinar-amrutkar. - Bonjour/Gateway: treat active ciao probing and fresh name-conflict renames as in-progress so the mDNS watchdog waits for probe settlement before retrying, preventing rapid re-advertise loops on Windows, WSL, and other multicast-hostile hosts. (#74778) Refs #74242. Thanks @fuller-stack-dev. - Providers/MiniMax: send a minimal Anthropic-compatible user fallback when message conversion filters a turn to an empty payload, so MiniMax M2.7 no longer returns `chat content is empty` after tool-heavy sessions. Fixes #74589. Thanks @neeravmakwana and @DerekEXS. - Tools/media: preserve implicit allow-all semantics from `tools.alsoAllow`-only policies when preconstructing built-in media generation and PDF tools, so configured media tools become live without forcing `tools.allow: ["*", ...]`. Fixes #77841. Thanks @trialanderrorstudios. diff --git a/src/plugins/loader.cli-metadata.test.ts b/src/plugins/loader.cli-metadata.test.ts index d0b170ddb86..236841664e3 100644 --- a/src/plugins/loader.cli-metadata.test.ts +++ b/src/plugins/loader.cli-metadata.test.ts @@ -26,6 +26,58 @@ afterAll(() => { }); describe("plugin loader CLI metadata", () => { + it.each([ + { + id: "wrong-cli-channel-entry", + kind: "bundled-channel-entry", + error: "bundled channel entry requires setup-runtime loader", + }, + { + id: "wrong-cli-channel-setup-entry", + kind: "bundled-channel-setup-entry", + error: "bundled channel setup entry requires setup-runtime loader", + }, + ])( + "reports $kind loaded through CLI metadata legacy plugin path", + async ({ id, kind, error }) => { + useNoBundledPlugins(); + const plugin = writePlugin({ + id, + filename: `${id}.cjs`, + body: `module.exports = { id: ${JSON.stringify(id)}, kind: ${JSON.stringify(kind)} };`, + }); + const errors: string[] = []; + + const registry = await loadOpenClawPluginCliRegistry({ + cache: false, + logger: { + info: () => {}, + warn: () => {}, + error: (msg: string) => errors.push(msg), + debug: () => {}, + }, + config: { + plugins: { + load: { paths: [plugin.file] }, + allow: [id], + }, + }, + }); + + const loaded = registry.plugins.find((entry) => entry.id === id); + expect(loaded?.status).toBe("error"); + expect(loaded?.error).toBe(error); + expect( + registry.diagnostics.some( + (diag) => diag.level === "error" && diag.pluginId === id && diag.message === error, + ), + ).toBe(true); + expect(errors).toEqual([ + `[plugins] ${id} ${error}; ensure plugin is loaded via bundled channel discovery, not legacy plugin loader`, + ]); + }, + ); + it("suppresses trust warning logs during CLI metadata loads", async () => { useNoBundledPlugins(); const stateDir = makeTempDir(); diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 583f5db9884..3508e6dd10e 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -3994,6 +3994,45 @@ module.exports = { id: "throws-after-import", register() {} };`, expect(loaded?.error).toContain("export.default:object keys=default"); }); + it.each([ + { + id: "wrong-channel-entry", + kind: "bundled-channel-entry", + error: "bundled channel entry requires setup-runtime loader", + }, + { + id: "wrong-channel-setup-entry", + kind: "bundled-channel-setup-entry", + error: "bundled channel setup entry requires setup-runtime loader", + }, + ])("reports $kind loaded through the legacy plugin loader", ({ id, kind, error }) => { + useNoBundledPlugins(); + const plugin = writePlugin({ + id, + filename: `${id}.cjs`, + body: `module.exports = { id: ${JSON.stringify(id)}, kind: ${JSON.stringify(kind)} };`, + }); + const errors: string[] = []; + + const registry = loadRegistryFromSinglePlugin({ + plugin, + pluginConfig: { + allow: [id], + }, + options: { + logger: createErrorLogger(errors), + }, + }); + + const loaded = registry.plugins.find((entry) => entry.id === id); + expect(loaded?.status).toBe("error"); + expect(loaded?.error).toBe(error); + expectRegistryErrorDiagnostic({ registry, pluginId: id, message: error }); + expect(errors).toEqual([ + `[plugins] ${id} ${error}; ensure plugin is loaded via bundled channel discovery, not legacy plugin loader`, + ]); + }); + it("handles single-plugin channel, context engine, and cli validation", () => { useNoBundledPlugins(); const scenarios = [ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 22d43931a1b..19a6b5d8fb1 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -1439,6 +1439,20 @@ function resolvePluginModuleExport(moduleExport: unknown): { return {}; } +function kindIncludes(kind: unknown, target: string): boolean { + return kind === target || (Array.isArray(kind) && kind.includes(target)); +} + +function formatBundledChannelWrongLoaderError(kind: unknown): string | null { + if (kindIncludes(kind, "bundled-channel-setup-entry")) { + return "bundled channel setup entry requires setup-runtime loader"; + } + if (kindIncludes(kind, "bundled-channel-entry")) { + return "bundled channel entry requires setup-runtime loader"; + } + return null; +} + function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnostic[]) { diagnostics.push(...append); } @@ -2340,8 +2354,16 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi } if (typeof register !== "function") { - logger.error(`[plugins] ${record.id} missing register/activate export`); - pushPluginLoadError(formatMissingPluginRegisterError(mod, env)); + const bundledChannelWrongLoaderError = formatBundledChannelWrongLoaderError(record.kind); + if (bundledChannelWrongLoaderError) { + logger.error( + `[plugins] ${record.id} ${bundledChannelWrongLoaderError}; ensure plugin is loaded via bundled channel discovery, not legacy plugin loader`, + ); + pushPluginLoadError(bundledChannelWrongLoaderError); + } else { + logger.error(`[plugins] ${record.id} missing register/activate export`); + pushPluginLoadError(formatMissingPluginRegisterError(mod, env)); + } continue; } @@ -2793,8 +2815,16 @@ export async function loadOpenClawPluginCliRegistry( } if (typeof register !== "function") { - logger.error(`[plugins] ${record.id} missing register/activate export`); - pushPluginLoadError(formatMissingPluginRegisterError(mod, env)); + const bundledChannelWrongLoaderError = formatBundledChannelWrongLoaderError(record.kind); + if (bundledChannelWrongLoaderError) { + logger.error( + `[plugins] ${record.id} ${bundledChannelWrongLoaderError}; ensure plugin is loaded via bundled channel discovery, not legacy plugin loader`, + ); + pushPluginLoadError(bundledChannelWrongLoaderError); + } else { + logger.error(`[plugins] ${record.id} missing register/activate export`); + pushPluginLoadError(formatMissingPluginRegisterError(mod, env)); + } continue; }