diff --git a/src/plugin-sdk/runtime-store.test.ts b/src/plugin-sdk/runtime-store.test.ts index 07ee2b1dd0f..c961008421c 100644 --- a/src/plugin-sdk/runtime-store.test.ts +++ b/src/plugin-sdk/runtime-store.test.ts @@ -40,7 +40,7 @@ describe("createPluginRuntimeStore", () => { expect(rightStore.tryGetRuntime()).toBeNull(); }); - test("keeps legacy string callers working", () => { + test("keeps legacy string callers isolated per store", () => { const firstStore = createPluginRuntimeStore<{ value: string }>( "legacy runtime not initialized", ); @@ -51,7 +51,8 @@ describe("createPluginRuntimeStore", () => { firstStore.clearRuntime(); firstStore.setRuntime({ value: "legacy" }); - expect(secondStore.getRuntime()).toEqual({ value: "legacy" }); + expect(firstStore.getRuntime()).toEqual({ value: "legacy" }); + expect(secondStore.tryGetRuntime()).toBeNull(); }); test("still supports explicit custom store keys", () => { diff --git a/src/plugin-sdk/runtime-store.ts b/src/plugin-sdk/runtime-store.ts index 7f872f4ad92..98a84f686d6 100644 --- a/src/plugin-sdk/runtime-store.ts +++ b/src/plugin-sdk/runtime-store.ts @@ -64,12 +64,18 @@ export function createPluginRuntimeStore(options: string | PluginRuntimeStore getRuntime: () => T; } { const resolved = resolvePluginRuntimeStoreOptions(options); - const registry = getPluginRuntimeStoreRegistry(); - let slot = registry.get(resolved.key); - if (!slot) { - slot = { runtime: null }; - registry.set(resolved.key, slot); - } + const slot = + typeof options === "string" + ? { runtime: null } + : (() => { + const registry = getPluginRuntimeStoreRegistry(); + let existingSlot = registry.get(resolved.key); + if (!existingSlot) { + existingSlot = { runtime: null }; + registry.set(resolved.key, existingSlot); + } + return existingSlot; + })(); return { setRuntime(next: T) { diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index b9ef5eec132..c2178e54523 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -523,6 +523,7 @@ function createSetupEntryChannelPluginFixture(params: { configured: boolean; startupDeferConfiguredChannelFullLoadUntilAfterListen?: boolean; useBundledFullEntryContract?: boolean; + bundledFullEntryId?: string; useBundledSetupEntryContract?: boolean; splitBundledSetupSecrets?: boolean; bundledSetupRuntimeMarker?: string; @@ -580,7 +581,7 @@ function createSetupEntryChannelPluginFixture(params: { ? `require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8"); module.exports = { kind: "bundled-channel-entry", - id: ${JSON.stringify(params.id)}, + id: ${JSON.stringify(params.bundledFullEntryId ?? params.id)}, name: ${JSON.stringify(params.label)}, description: ${JSON.stringify(params.fullBlurb)}, loadChannelPlugin: () => { @@ -592,12 +593,12 @@ module.exports = { : "" } return { - id: ${JSON.stringify(params.id)}, + id: ${JSON.stringify(params.bundledFullEntryId ?? params.id)}, meta: { - id: ${JSON.stringify(params.id)}, + id: ${JSON.stringify(params.bundledFullEntryId ?? params.id)}, label: ${JSON.stringify(params.label)}, selectionLabel: ${JSON.stringify(params.label)}, - docsPath: ${JSON.stringify(`/channels/${params.id}`)}, + docsPath: ${JSON.stringify(`/channels/${params.bundledFullEntryId ?? params.id}`)}, blurb: ${JSON.stringify(params.fullBlurb)}, }, capabilities: { chatTypes: ["direct"] }, @@ -3589,6 +3590,39 @@ module.exports = { ); }); + it("rejects mismatched bundled runtime plugin ids during setup-runtime merge", () => { + const built = createSetupEntryChannelPluginFixture({ + id: "setup-runtime-mismatch-test", + bundledFullEntryId: "wrong-runtime-id", + label: "Setup Runtime Mismatch Test", + packageName: "@openclaw/setup-runtime-mismatch-test", + fullBlurb: "full runtime plugin", + setupBlurb: "setup runtime override", + configured: false, + useBundledFullEntryContract: true, + useBundledSetupEntryContract: true, + bundledFullRuntimeMarker: path.join(makeTempDir(), "setup-runtime-mismatch.txt"), + }); + + const registry = loadOpenClawPlugins({ + cache: false, + config: { + plugins: { + load: { paths: [built.pluginDir] }, + allow: ["setup-runtime-mismatch-test"], + }, + }, + }); + + expect( + registry.plugins.find((entry) => entry.id === "setup-runtime-mismatch-test")?.status, + ).toBe("error"); + expect( + registry.plugins.find((entry) => entry.id === "setup-runtime-mismatch-test")?.error, + ).toContain('runtime export uses "wrong-runtime-id"'); + expect(registry.channels).toHaveLength(0); + }); + it("isolates loadSetupPlugin errors as per-plugin diagnostics instead of crashing registry load", () => { useNoBundledPlugins(); const pluginDir = makeTempDir(); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 726a7187d4e..70a990c8635 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -1889,6 +1889,15 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi continue; } if (runtimePluginRegistration.plugin) { + if ( + runtimePluginRegistration.plugin.id && + runtimePluginRegistration.plugin.id !== record.id + ) { + pushPluginLoadError( + `plugin id mismatch (config uses "${record.id}", runtime export uses "${runtimePluginRegistration.plugin.id}")`, + ); + continue; + } mergedSetupRegistration = { ...setupRegistration, plugin: mergeSetupRuntimeChannelPlugin(