From 6863694a2273adca9043fb8908d230a3109e1d9b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 30 Apr 2026 01:57:53 +0100 Subject: [PATCH] fix(status): resolve packaged channel setup loader --- CHANGELOG.md | 1 + src/channels/plugins/read-only.test.ts | 20 +++++++++++- src/channels/plugins/read-only.ts | 43 +++++++++++++++++++++++--- tsdown.config.ts | 1 + 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ac21d92a7..778ab081919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/status: resolve read-only channel setup runtime fallback from the packaged OpenClaw dist root, so `status --all`, `status --deep`, channel, and doctor paths do not crash when an external channel plugin needs setup metadata. Fixes #74693. Thanks @giangthb. - CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash. - Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc. - Channels/Microsoft Teams: treat configured `19:...@thread.tacv2` and legacy `19:...@thread.skype` team/channel IDs as already resolved during startup, avoiding false `channels unresolved` warnings while preserving Graph name lookup for display-name entries. Fixes #74683. Thanks @dseravalli. diff --git a/src/channels/plugins/read-only.test.ts b/src/channels/plugins/read-only.test.ts index a134de4cf37..957c13f8a9d 100644 --- a/src/channels/plugins/read-only.test.ts +++ b/src/channels/plugins/read-only.test.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { cleanupPluginLoaderFixturesForTest, @@ -8,7 +9,10 @@ import { resetPluginLoaderTestStateForTest, useNoBundledPlugins, } from "../../plugins/loader.test-fixtures.js"; -import { listReadOnlyChannelPluginsForConfig } from "./read-only.js"; +import { + listPluginLoaderModuleCandidateUrls, + listReadOnlyChannelPluginsForConfig, +} from "./read-only.js"; vi.mock("../../plugins/bundled-dir.js", async (importOriginal) => { const actual = await importOriginal(); @@ -423,6 +427,20 @@ afterAll(() => { }); describe("listReadOnlyChannelPluginsForConfig", () => { + it("keeps built plugin loader candidates inside the installed package dist root", () => { + const packageRoot = path.join(makeTempDir(), "node_modules", "openclaw"); + const importerPath = path.join(packageRoot, "dist", "read-only-B4EkEtUx.js"); + const candidates = listPluginLoaderModuleCandidateUrls(pathToFileURL(importerPath).href).map( + (candidate) => fileURLToPath(candidate), + ); + + expect(candidates).toEqual([ + path.join(packageRoot, "dist", "plugins", "loader.js"), + path.join(packageRoot, "dist", "plugins", "build-smoke-entry.js"), + ]); + expect(candidates).not.toContain(path.join(packageRoot, "..", "plugins", "loader.js")); + }); + it("does not load setup-only channel plugin runtime by default", () => { const { pluginDir, fullMarker, setupMarker } = writeExternalSetupChannelPlugin(); const plugins = listReadOnlyChannelPluginsForConfig( diff --git a/src/channels/plugins/read-only.ts b/src/channels/plugins/read-only.ts index fe831462136..8278595160b 100644 --- a/src/channels/plugins/read-only.ts +++ b/src/channels/plugins/read-only.ts @@ -1,4 +1,5 @@ -import { fileURLToPath } from "node:url"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { isBlockedObjectKey } from "../../infra/prototype-keys.js"; @@ -25,9 +26,13 @@ import { import { listChannelPlugins } from "./registry.js"; import type { ChannelPlugin } from "./types.plugin.js"; -const LOADER_MODULE_CANDIDATES = [ - new URL("../../plugins/loader.js", import.meta.url), - new URL("../../plugins/loader.ts", import.meta.url), +const SOURCE_PLUGIN_LOADER_MODULE_CANDIDATES = [ + "../../plugins/loader.js", + "../../plugins/loader.ts", +] as const; +const BUILT_PLUGIN_LOADER_MODULE_CANDIDATES = [ + "plugins/loader.js", + "plugins/build-smoke-entry.js", ] as const; const jitiLoaders: PluginJitiLoaderCache = new Map(); @@ -53,11 +58,39 @@ type PluginLoaderModule = { let pluginLoaderModule: PluginLoaderModule | undefined; +function listBuiltPluginLoaderModuleCandidateUrls(importerUrl: string): URL[] { + let importerPath: string; + try { + importerPath = fileURLToPath(importerUrl); + } catch { + return []; + } + const distMarker = `${path.sep}dist${path.sep}`; + const distMarkerIndex = importerPath.lastIndexOf(distMarker); + if (distMarkerIndex < 0) { + return []; + } + // Bundled read-only chunks live under dist/ with hashed names. Source-relative + // ../../plugins candidates would escape the installed openclaw package there. + const distRoot = importerPath.slice(0, distMarkerIndex + distMarker.length - 1); + return BUILT_PLUGIN_LOADER_MODULE_CANDIDATES.map((candidate) => + pathToFileURL(path.join(distRoot, candidate)), + ); +} + +export function listPluginLoaderModuleCandidateUrls(importerUrl = import.meta.url): URL[] { + const builtCandidates = listBuiltPluginLoaderModuleCandidateUrls(importerUrl); + if (builtCandidates.length > 0) { + return builtCandidates; + } + return SOURCE_PLUGIN_LOADER_MODULE_CANDIDATES.map((candidate) => new URL(candidate, importerUrl)); +} + function loadPluginLoaderModule(): PluginLoaderModule { if (pluginLoaderModule) { return pluginLoaderModule; } - for (const candidate of LOADER_MODULE_CANDIDATES) { + for (const candidate of listPluginLoaderModuleCandidateUrls()) { const modulePath = fileURLToPath(candidate); try { const jiti = getCachedPluginJitiLoader({ diff --git a/tsdown.config.ts b/tsdown.config.ts index ebd894782be..6631d699729 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -226,6 +226,7 @@ function buildCoreDistEntries(): Record { "plugins/provider-discovery.runtime": "src/plugins/provider-discovery.runtime.ts", "plugins/provider-runtime.runtime": "src/plugins/provider-runtime.runtime.ts", "plugins/public-surface-runtime": "src/plugins/public-surface-runtime.ts", + "plugins/loader": "src/plugins/loader.ts", "plugins/sdk-alias": "src/plugins/sdk-alias.ts", "facade-activation-check.runtime": "src/plugin-sdk/facade-activation-check.runtime.ts", extensionAPI: "src/extensionAPI.ts",