From 49b42b4a4547c818ec8ebc97e41930a1474a6da2 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:33:29 -0500 Subject: [PATCH] fix(release): handle nested default-wrapped bundled channel entries --- src/channels/plugins/bundled.ts | 15 +--- src/plugins/bundled-capability-runtime.ts | 8 +- src/plugins/loader.test.ts | 93 +++++++++++++++++++++++ src/plugins/loader.ts | 15 +--- src/plugins/module-export.ts | 16 ++++ 5 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 src/plugins/module-export.ts diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts index 357c4fe05dd..d1746e680a5 100644 --- a/src/channels/plugins/bundled.ts +++ b/src/channels/plugins/bundled.ts @@ -12,6 +12,7 @@ import { resolveBundledChannelGeneratedPath, type BundledChannelPluginMetadata, } from "../../plugins/bundled-channel-runtime.js"; +import { unwrapDefaultModuleExport } from "../../plugins/module-export.js"; import type { PluginRuntime } from "../../plugins/runtime/types.js"; import { isJavaScriptModulePath, loadChannelPluginModule } from "./module-loader.js"; import type { ChannelPlugin } from "./types.plugin.js"; @@ -37,12 +38,7 @@ const OPENCLAW_PACKAGE_ROOT = function resolveChannelPluginModuleEntry( moduleExport: unknown, ): BundledChannelEntryContract | null { - const resolved = - moduleExport && - typeof moduleExport === "object" && - "default" in (moduleExport as Record) - ? (moduleExport as { default: unknown }).default - : moduleExport; + const resolved = unwrapDefaultModuleExport(moduleExport); if (!resolved || typeof resolved !== "object") { return null; } @@ -65,12 +61,7 @@ function resolveChannelPluginModuleEntry( function resolveChannelSetupModuleEntry( moduleExport: unknown, ): BundledChannelSetupEntryContract | null { - const resolved = - moduleExport && - typeof moduleExport === "object" && - "default" in (moduleExport as Record) - ? (moduleExport as { default: unknown }).default - : moduleExport; + const resolved = unwrapDefaultModuleExport(moduleExport); if (!resolved || typeof resolved !== "object") { return null; } diff --git a/src/plugins/bundled-capability-runtime.ts b/src/plugins/bundled-capability-runtime.ts index 49e9f69bc77..02bb8244947 100644 --- a/src/plugins/bundled-capability-runtime.ts +++ b/src/plugins/bundled-capability-runtime.ts @@ -12,6 +12,7 @@ import { createCapturedPluginRegistration } from "./captured-registration.js"; import { discoverOpenClawPlugins } from "./discovery.js"; import type { PluginLoadOptions } from "./loader.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; +import { unwrapDefaultModuleExport } from "./module-export.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import type { PluginRecord, PluginRegistry } from "./registry.js"; import { @@ -95,12 +96,7 @@ function resolvePluginModuleExport(moduleExport: unknown): { definition?: OpenClawPluginDefinition; register?: OpenClawPluginDefinition["register"]; } { - const resolved = - moduleExport && - typeof moduleExport === "object" && - "default" in (moduleExport as Record) - ? (moduleExport as { default: unknown }).default - : moduleExport; + const resolved = unwrapDefaultModuleExport(moduleExport); if (typeof resolved === "function") { return { register: resolved as OpenClawPluginDefinition["register"], diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 65209c9988c..3006ab7f3f9 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -2781,6 +2781,99 @@ module.exports = { id: "throws-after-import", register() {} };`, expect(disabled?.status).toBe("disabled"); }); + it("loads bundled channel entries through nested default export wrappers", () => { + useNoBundledPlugins(); + const pluginDir = makeTempDir(); + const fullMarker = path.join(pluginDir, "full-loaded.txt"); + + fs.writeFileSync( + path.join(pluginDir, "package.json"), + JSON.stringify( + { + name: "@openclaw/nested-default-channel", + openclaw: { + extensions: ["./index.cjs"], + }, + }, + null, + 2, + ), + "utf-8", + ); + fs.writeFileSync( + path.join(pluginDir, "openclaw.plugin.json"), + JSON.stringify( + { + id: "nested-default-channel", + configSchema: EMPTY_PLUGIN_SCHEMA, + channels: ["nested-default-channel"], + }, + null, + 2, + ), + "utf-8", + ); + fs.writeFileSync( + path.join(pluginDir, "index.cjs"), + `module.exports = { + default: { + default: { + id: "nested-default-channel", + kind: "bundled-channel-entry", + name: "Nested Default Channel", + description: "interop-wrapped bundled channel entry", + register(api) { + require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8"); + api.registerChannel({ + plugin: { + id: "nested-default-channel", + meta: { + id: "nested-default-channel", + label: "Nested Default Channel", + selectionLabel: "Nested Default Channel", + docsPath: "/channels/nested-default-channel", + blurb: "interop-wrapped bundled channel entry", + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({ accountId: "default", token: "configured" }), + }, + outbound: { deliveryMode: "direct" }, + }, + }); + }, + }, + }, +};`, + "utf-8", + ); + + const registry = loadOpenClawPlugins({ + cache: false, + config: { + channels: { + "nested-default-channel": { + enabled: true, + token: "configured", + }, + }, + plugins: { + load: { paths: [pluginDir] }, + allow: ["nested-default-channel"], + }, + }, + }); + + expect(fs.existsSync(fullMarker)).toBe(true); + expect(registry.plugins.find((entry) => entry.id === "nested-default-channel")?.status).toBe( + "loaded", + ); + expect(registry.channels.some((entry) => entry.plugin.id === "nested-default-channel")).toBe( + true, + ); + }); + it("does not treat manifest channel ids as scoped plugin id matches", () => { useNoBundledPlugins(); const target = writePlugin({ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 1bf68807586..fd60f4b1c9d 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -55,6 +55,7 @@ import { listMemoryPromptSupplements, restoreMemoryPluginState, } from "./memory-state.js"; +import { unwrapDefaultModuleExport } from "./module-export.js"; import { isPathInside, safeStatSync } from "./path-safety.js"; import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js"; import { resolvePluginCacheInputs } from "./roots.js"; @@ -602,12 +603,7 @@ function resolvePluginModuleExport(moduleExport: unknown): { definition?: OpenClawPluginDefinition; register?: OpenClawPluginDefinition["register"]; } { - const resolved = - moduleExport && - typeof moduleExport === "object" && - "default" in (moduleExport as Record) - ? (moduleExport as { default: unknown }).default - : moduleExport; + const resolved = unwrapDefaultModuleExport(moduleExport); if (typeof resolved === "function") { return { register: resolved as OpenClawPluginDefinition["register"], @@ -624,12 +620,7 @@ function resolvePluginModuleExport(moduleExport: unknown): { function resolveSetupChannelRegistration(moduleExport: unknown): { plugin?: ChannelPlugin; } { - const resolved = - moduleExport && - typeof moduleExport === "object" && - "default" in (moduleExport as Record) - ? (moduleExport as { default: unknown }).default - : moduleExport; + const resolved = unwrapDefaultModuleExport(moduleExport); if (!resolved || typeof resolved !== "object") { return {}; } diff --git a/src/plugins/module-export.ts b/src/plugins/module-export.ts new file mode 100644 index 00000000000..2627fb6c3cf --- /dev/null +++ b/src/plugins/module-export.ts @@ -0,0 +1,16 @@ +export function unwrapDefaultModuleExport(moduleExport: unknown): unknown { + let resolved = moduleExport; + const seen = new Set(); + + while ( + resolved && + typeof resolved === "object" && + "default" in (resolved as Record) && + !seen.has(resolved) + ) { + seen.add(resolved); + resolved = (resolved as { default: unknown }).default; + } + + return resolved; +}