diff --git a/src/auto-reply/command-auth.owner-allow-from-cache.test.ts b/src/auto-reply/command-auth.owner-allow-from-cache.test.ts index 4cd25adb8a3..fb1f3af9414 100644 --- a/src/auto-reply/command-auth.owner-allow-from-cache.test.ts +++ b/src/auto-reply/command-auth.owner-allow-from-cache.test.ts @@ -60,4 +60,106 @@ describe("resolveCommandAuthorization owner allowlist hot path", () => { expect(largeListFormatter).toHaveBeenCalledTimes(1); }); + + it("does not reuse cached ownerAllowFrom across config snapshots sharing the same array", () => { + const sharedOwnerAllowFrom = ["123", "456", "789"]; + const cfgA = { + channels: { discord: {} }, + commands: { ownerAllowFrom: sharedOwnerAllowFrom }, + testVariant: "A", + } as OpenClawConfig; + const cfgB = { + channels: { discord: {} }, + commands: { ownerAllowFrom: sharedOwnerAllowFrom }, + testVariant: "B", + } as OpenClawConfig; + + const plugin = createOutboundTestPlugin({ + id: "discord", + outbound: { deliveryMode: "direct" }, + }); + plugin.config = { + ...plugin.config, + formatAllowFrom: ({ cfg, allowFrom }) => + allowFrom.map((entry) => `${(cfg as { testVariant?: string }).testVariant}:${String(entry)}`), + }; + setActivePluginRegistry( + createTestRegistry([{ pluginId: "discord", plugin, source: "test" }]), + "owner-cache-config-a", + ); + + const ctx = { + Provider: "discord", + Surface: "discord", + From: "discord:123", + SenderId: "123", + } as MsgContext; + + const authA = resolveCommandAuthorization({ + ctx, + cfg: cfgA, + commandAuthorized: true, + }); + const authB = resolveCommandAuthorization({ + ctx, + cfg: cfgB, + commandAuthorized: true, + }); + + expect(authA.ownerList).toEqual(["A:123", "A:456", "A:789"]); + expect(authB.ownerList).toEqual(["B:123", "B:456", "B:789"]); + }); + + it("does not reuse cached ownerAllowFrom across plugin registry reloads", () => { + const sharedOwnerAllowFrom = ["123", "456", "789"]; + const cfg = { + channels: { discord: {} }, + commands: { ownerAllowFrom: sharedOwnerAllowFrom }, + } as OpenClawConfig; + const ctx = { + Provider: "discord", + Surface: "discord", + From: "discord:123", + SenderId: "123", + } as MsgContext; + + const pluginA = createOutboundTestPlugin({ + id: "discord", + outbound: { deliveryMode: "direct" }, + }); + pluginA.config = { + ...pluginA.config, + formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => `A:${String(entry)}`), + }; + setActivePluginRegistry( + createTestRegistry([{ pluginId: "discord", plugin: pluginA, source: "test" }]), + "owner-cache-registry-a", + ); + const authA = resolveCommandAuthorization({ + ctx, + cfg, + commandAuthorized: true, + }); + + const pluginB = createOutboundTestPlugin({ + id: "discord", + outbound: { deliveryMode: "direct" }, + }); + pluginB.config = { + ...pluginB.config, + formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => `B:${String(entry)}`), + }; + setActivePluginRegistry( + createTestRegistry([{ pluginId: "discord", plugin: pluginB, source: "test" }]), + "owner-cache-registry-b", + ); + const authB = resolveCommandAuthorization({ + ctx, + cfg, + commandAuthorized: true, + }); + + expect(authA.ownerList).toEqual(["A:123", "A:456", "A:789"]); + expect(authB.ownerList).toEqual(["B:123", "B:456", "B:789"]); + }); }); diff --git a/src/auto-reply/command-auth.ts b/src/auto-reply/command-auth.ts index 1ccec930e96..75f9d95140c 100644 --- a/src/auto-reply/command-auth.ts +++ b/src/auto-reply/command-auth.ts @@ -2,6 +2,7 @@ import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index. import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js"; import { normalizeAnyChannelId } from "../channels/registry.js"; import type { OpenClawConfig } from "../config/config.js"; +import { getActivePluginRegistryVersion } from "../plugins/runtime.js"; import { normalizeStringEntries } from "../shared/string-normalization.js"; import { INTERNAL_MESSAGE_CHANNEL, @@ -20,7 +21,10 @@ export type CommandAuthorization = { to?: string; }; -const ownerAllowFromListCache = new WeakMap, Map>(); +const ownerAllowFromListCache = new WeakMap< + OpenClawConfig, + WeakMap, Map> +>(); function resolveProviderFromContext(ctx: MsgContext, cfg: OpenClawConfig): ChannelId | undefined { const explicitMessageChannel = @@ -118,9 +122,10 @@ function resolveOwnerAllowFromList(params: { if (!Array.isArray(raw) || raw.length === 0) { return []; } - const cacheKey = `${params.plugin?.id ?? ""}\u0000${params.accountId ?? ""}\u0000${params.providerId ?? ""}`; - const cached = ownerAllowFromListCache.get(raw)?.get(cacheKey); - if (cached) { + const registryVersion = getActivePluginRegistryVersion(); + const cacheKey = `${registryVersion}\u0000${params.plugin?.id ?? ""}\u0000${params.accountId ?? ""}\u0000${params.providerId ?? ""}`; + const cached = ownerAllowFromListCache.get(params.cfg)?.get(raw)?.get(cacheKey); + if (cached !== undefined) { return cached; } const filtered: string[] = []; @@ -152,10 +157,15 @@ function resolveOwnerAllowFromList(params: { accountId: params.accountId, allowFrom: filtered, }); - let cachedByKey = ownerAllowFromListCache.get(raw); + let cachedByConfig = ownerAllowFromListCache.get(params.cfg); + if (!cachedByConfig) { + cachedByConfig = new WeakMap, Map>(); + ownerAllowFromListCache.set(params.cfg, cachedByConfig); + } + let cachedByKey = cachedByConfig.get(raw); if (!cachedByKey) { cachedByKey = new Map(); - ownerAllowFromListCache.set(raw, cachedByKey); + cachedByConfig.set(raw, cachedByKey); } cachedByKey.set(cacheKey, formatted); return formatted;