diff --git a/src/plugin-sdk/channel-entry-contract.ts b/src/plugin-sdk/channel-entry-contract.ts index 8674d489f7b..5c732a049ad 100644 --- a/src/plugin-sdk/channel-entry-contract.ts +++ b/src/plugin-sdk/channel-entry-contract.ts @@ -487,11 +487,11 @@ export function defineBundledChannelEntry({ profile("bundled-register:registerChannel", () => api.registerChannel({ plugin: channelPlugin as ChannelPlugin }), ); + profile("bundled-register:setChannelRuntime", () => setChannelRuntime?.(api.runtime)); if (api.registrationMode === "discovery") { profile("bundled-register:registerCliMetadata", () => registerCliMetadata?.(api)); return; } - profile("bundled-register:setChannelRuntime", () => setChannelRuntime?.(api.runtime)); if (api.registrationMode !== "full") { return; } diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 6d0cd751a5e..937244a73b6 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -504,11 +504,11 @@ export function defineChannelPluginEntry({ return; } api.registerChannel({ plugin: plugin as ChannelPlugin }); + setRuntime?.(api.runtime); if (api.registrationMode === "discovery") { registerCliMetadata?.(api); return; } - setRuntime?.(api.runtime); if (api.registrationMode !== "full") { return; } diff --git a/src/plugins/loader.cli-metadata.test.ts b/src/plugins/loader.cli-metadata.test.ts index 96bc2e7e20a..5496651ca0a 100644 --- a/src/plugins/loader.cli-metadata.test.ts +++ b/src/plugins/loader.cli-metadata.test.ts @@ -1,6 +1,11 @@ import fs from "node:fs"; import path from "node:path"; +import { pathToFileURL } from "node:url"; import { afterAll, afterEach, describe, expect, it } from "vitest"; +import { + defineBundledChannelEntry, + type OpenClawPluginApi, +} from "../plugin-sdk/channel-entry-contract.js"; import { loadOpenClawPluginCliRegistry, loadOpenClawPlugins } from "./loader.js"; import { cleanupPluginLoaderFixturesForTest, @@ -650,12 +655,93 @@ module.exports = { expect(fs.readFileSync(modeMarker, "utf-8")).toBe("discovery"); expect(fs.existsSync(fullMarker)).toBe(false); - expect(fs.existsSync(runtimeMarker)).toBe(false); + expect(fs.existsSync(runtimeMarker)).toBe(true); expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain( "discovery-cli-metadata-channel", ); }); + it("sets bundled channel runtime before discovery CLI metadata registration", () => { + const pluginDir = makeTempDir(); + const runtimeMarker = path.join(pluginDir, "runtime-set.txt"); + const channelPluginPath = path.join(pluginDir, "channel.cjs"); + const runtimePath = path.join(pluginDir, "runtime.cjs"); + fs.writeFileSync( + channelPluginPath, + `exports.plugin = { + id: "bundled-discovery-cli", + meta: { + id: "bundled-discovery-cli", + label: "Bundled Discovery CLI", + selectionLabel: "Bundled Discovery CLI", + docsPath: "/channels/bundled-discovery-cli", + blurb: "bundled discovery cli", + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => [], + resolveAccount: () => ({ accountId: "default" }), + }, + outbound: { deliveryMode: "direct" }, +};`, + "utf-8", + ); + fs.writeFileSync( + runtimePath, + `exports.setRuntime = () => { + require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "loaded", "utf-8"); +};`, + "utf-8", + ); + + const commands: string[] = []; + const channels: string[] = []; + const entry = defineBundledChannelEntry({ + id: "bundled-discovery-cli", + name: "Bundled Discovery CLI", + description: "bundled discovery cli", + importMetaUrl: pathToFileURL(path.join(pluginDir, "index.cjs")).href, + plugin: { + specifier: "./channel.cjs", + exportName: "plugin", + }, + runtime: { + specifier: "./runtime.cjs", + exportName: "setRuntime", + }, + registerCliMetadata(api) { + api.registerCli(() => {}, { + descriptors: [ + { + name: "bundled-discovery-cli", + description: "Bundled discovery CLI metadata", + hasSubcommands: true, + }, + ], + }); + }, + registerFull() { + throw new Error("full registration should not run during discovery"); + }, + }); + + entry.register({ + registrationMode: "discovery", + runtime: {} as OpenClawPluginApi["runtime"], + registerChannel: (registration) => { + const plugin = "plugin" in registration ? registration.plugin : registration; + channels.push(plugin.id); + }, + registerCli: (_register, options) => { + commands.push(...(options?.descriptors ?? []).map((descriptor) => descriptor.name)); + }, + } as OpenClawPluginApi); + + expect(channels).toEqual(["bundled-discovery-cli"]); + expect(fs.existsSync(runtimeMarker)).toBe(true); + expect(commands).toEqual(["bundled-discovery-cli"]); + }); + it("sanitizes plugin CLI descriptor descriptions and rejects unsafe command names", async () => { useNoBundledPlugins(); const unsafeDescription = diff --git a/src/plugins/loader.test-fixtures.ts b/src/plugins/loader.test-fixtures.ts index 0db1be83b50..e692a244133 100644 --- a/src/plugins/loader.test-fixtures.ts +++ b/src/plugins/loader.test-fixtures.ts @@ -56,11 +56,11 @@ export function inlineChannelPluginEntryFactorySource(): string { return; } api.registerChannel({ plugin: options.plugin }); + options.setRuntime?.(api.runtime); if (api.registrationMode === "discovery") { options.registerCliMetadata?.(api); return; } - options.setRuntime?.(api.runtime); if (api.registrationMode !== "full") { return; }