From 46f631c8c6d4e754c4ebb90414b558c70d095034 Mon Sep 17 00:00:00 2001 From: Mason Huang Date: Tue, 14 Apr 2026 09:33:12 +0800 Subject: [PATCH] plugins: support bundled setup-entry contract in loader --- src/plugins/loader.test.ts | 49 +++++++++++++++++++++++++++++++++++++- src/plugins/loader.ts | 15 ++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index f25ec8e78fe..79ff3d72bce 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -522,6 +522,7 @@ function createSetupEntryChannelPluginFixture(params: { setupBlurb: string; configured: boolean; startupDeferConfiguredChannelFullLoadUntilAfterListen?: boolean; + useBundledSetupEntryContract?: boolean; }) { useNoBundledPlugins(); const pluginDir = makeTempDir(); @@ -597,7 +598,28 @@ module.exports = { ); fs.writeFileSync( path.join(pluginDir, "setup-entry.cjs"), - `require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8"); + params.useBundledSetupEntryContract + ? `require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8"); +module.exports = { + kind: "bundled-channel-setup-entry", + loadSetupPlugin: () => ({ + id: ${JSON.stringify(params.id)}, + meta: { + id: ${JSON.stringify(params.id)}, + label: ${JSON.stringify(params.label)}, + selectionLabel: ${JSON.stringify(params.label)}, + docsPath: ${JSON.stringify(`/channels/${params.id}`)}, + blurb: ${JSON.stringify(params.setupBlurb)}, + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => ${listAccountIds}, + resolveAccount: () => ${resolveAccount}, + }, + outbound: { deliveryMode: "direct" }, + }), +};` + : `require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8"); module.exports = { plugin: { id: ${JSON.stringify(params.id)}, @@ -3168,6 +3190,31 @@ module.exports = { expectSetupLoaded: true, expectedChannels: 1, }, + { + name: "uses package setupEntry bundled contract for setup-runtime channel loads", + fixture: { + id: "setup-runtime-bundled-contract-test", + label: "Setup Runtime Bundled Contract Test", + packageName: "@openclaw/setup-runtime-bundled-contract-test", + fullBlurb: "full entry should not run while unconfigured", + setupBlurb: "setup runtime bundled contract", + configured: false, + useBundledSetupEntryContract: true, + }, + load: ({ pluginDir }: { pluginDir: string }) => + loadOpenClawPlugins({ + cache: false, + config: { + plugins: { + load: { paths: [pluginDir] }, + allow: ["setup-runtime-bundled-contract-test"], + }, + }, + }), + expectFullLoaded: false, + expectSetupLoaded: true, + expectedChannels: 1, + }, { name: "does not prefer setupEntry for configured channel loads without startup opt-in", fixture: { diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 5194c4c6224..6de0a4e9415 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -651,6 +651,21 @@ function resolveSetupChannelRegistration(moduleExport: unknown): { if (!resolved || typeof resolved !== "object") { return {}; } + const setupEntryRecord = resolved as { + kind?: unknown; + loadSetupPlugin?: unknown; + }; + if ( + setupEntryRecord.kind === "bundled-channel-setup-entry" && + typeof setupEntryRecord.loadSetupPlugin === "function" + ) { + const loadedPlugin = setupEntryRecord.loadSetupPlugin(); + if (loadedPlugin && typeof loadedPlugin === "object") { + return { + plugin: loadedPlugin as ChannelPlugin, + }; + } + } const setup = resolved as { plugin?: unknown; };