diff --git a/extensions/discord/src/approval-native.test.ts b/extensions/discord/src/approval-native.test.ts index 4c4d0e1f0f1..6c8b3517bb2 100644 --- a/extensions/discord/src/approval-native.test.ts +++ b/extensions/discord/src/approval-native.test.ts @@ -143,4 +143,29 @@ describe("createDiscordNativeApprovalAdapter", () => { expect(target).toEqual({ to: "987654321" }); }); + + it("rejects origin delivery for requests bound to another Discord account", async () => { + const adapter = createDiscordNativeApprovalAdapter(); + + const target = await adapter.native?.resolveOriginTarget?.({ + cfg: {} as never, + accountId: "main", + approvalKind: "plugin", + request: { + id: "abc", + request: { + title: "Plugin approval", + description: "Let plugin proceed", + turnSourceChannel: "discord", + turnSourceTo: "channel:123456789", + turnSourceAccountId: "other", + sessionKey: "agent:main:missing", + }, + createdAtMs: 1, + expiresAtMs: 2, + }, + }); + + expect(target).toBeNull(); + }); }); diff --git a/extensions/discord/src/approval-native.ts b/extensions/discord/src/approval-native.ts index e2472f8bc9a..c832bcc3e41 100644 --- a/extensions/discord/src/approval-native.ts +++ b/extensions/discord/src/approval-native.ts @@ -1,11 +1,14 @@ -import { createApproverRestrictedNativeApprovalAdapter, resolveExecApprovalSessionTarget } from "openclaw/plugin-sdk/approval-runtime"; +import { + createApproverRestrictedNativeApprovalAdapter, + doesApprovalRequestMatchChannelAccount, + resolveApprovalRequestSessionTarget, +} from "openclaw/plugin-sdk/approval-runtime"; import type { DiscordExecApprovalConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ExecApprovalRequest, ExecApprovalSessionTarget, PluginApprovalRequest, } from "openclaw/plugin-sdk/infra-runtime"; -import { normalizeAccountId } from "openclaw/plugin-sdk/routing"; import { listDiscordAccountIds, resolveDiscordAccount } from "./accounts.js"; import { getDiscordExecApprovalApprovers, @@ -34,29 +37,6 @@ function extractDiscordSessionKind(sessionKey?: string | null): "channel" | "gro return match[1] as "channel" | "group" | "dm"; } -function isExecApprovalRequest(request: ApprovalRequest): request is ExecApprovalRequest { - return "command" in request.request; -} - -function toExecLikeRequest(request: ApprovalRequest): ExecApprovalRequest { - if (isExecApprovalRequest(request)) { - return request; - } - return { - id: request.id, - request: { - command: request.request.title, - sessionKey: request.request.sessionKey ?? undefined, - turnSourceChannel: request.request.turnSourceChannel ?? undefined, - turnSourceTo: request.request.turnSourceTo ?? undefined, - turnSourceAccountId: request.request.turnSourceAccountId ?? undefined, - turnSourceThreadId: request.request.turnSourceThreadId ?? undefined, - }, - createdAtMs: request.createdAtMs, - expiresAtMs: request.expiresAtMs, - }; -} - function normalizeDiscordOriginChannelId(value?: string | null): string | null { if (!value) { return null; @@ -76,15 +56,7 @@ function resolveRequestSessionTarget(params: { cfg: OpenClawConfig; request: ApprovalRequest; }): ExecApprovalSessionTarget | null { - const execLikeRequest = toExecLikeRequest(params.request); - return resolveExecApprovalSessionTarget({ - cfg: params.cfg, - request: execLikeRequest, - turnSourceChannel: execLikeRequest.request.turnSourceChannel ?? undefined, - turnSourceTo: execLikeRequest.request.turnSourceTo ?? undefined, - turnSourceAccountId: execLikeRequest.request.turnSourceAccountId ?? undefined, - turnSourceThreadId: execLikeRequest.request.turnSourceThreadId ?? undefined, - }); + return resolveApprovalRequestSessionTarget(params); } function resolveDiscordOriginTarget(params: { @@ -92,11 +64,21 @@ function resolveDiscordOriginTarget(params: { accountId?: string | null; request: ApprovalRequest; }) { + if ( + !doesApprovalRequestMatchChannelAccount({ + cfg: params.cfg, + request: params.request, + channel: "discord", + accountId: params.accountId, + }) + ) { + return null; + } + const sessionKind = extractDiscordSessionKind(params.request.request.sessionKey?.trim() || null); const turnSourceChannel = params.request.request.turnSourceChannel?.trim().toLowerCase() || ""; const rawTurnSourceTo = params.request.request.turnSourceTo?.trim() || ""; const turnSourceTo = normalizeDiscordOriginChannelId(rawTurnSourceTo); - const turnSourceAccountId = params.request.request.turnSourceAccountId?.trim() || ""; const hasExplicitOriginTarget = /^(?:channel|group):/i.test(rawTurnSourceTo); const turnSourceTarget = turnSourceChannel === "discord" && @@ -105,26 +87,10 @@ function resolveDiscordOriginTarget(params: { (hasExplicitOriginTarget || sessionKind === "channel" || sessionKind === "group") ? { to: turnSourceTo, - accountId: turnSourceAccountId || undefined, } : null; - if ( - turnSourceTarget?.accountId && - params.accountId && - normalizeAccountId(turnSourceTarget.accountId) !== normalizeAccountId(params.accountId) - ) { - return null; - } const sessionTarget = resolveRequestSessionTarget(params); - if ( - sessionTarget?.channel === "discord" && - sessionTarget.accountId && - params.accountId && - normalizeAccountId(sessionTarget.accountId) !== normalizeAccountId(params.accountId) - ) { - return null; - } if ( turnSourceTarget && sessionTarget?.channel === "discord" && diff --git a/extensions/discord/src/monitor/exec-approvals.test.ts b/extensions/discord/src/monitor/exec-approvals.test.ts index 16359131187..62f3a73ce20 100644 --- a/extensions/discord/src/monitor/exec-approvals.test.ts +++ b/extensions/discord/src/monitor/exec-approvals.test.ts @@ -1,3 +1,4 @@ +import fs from "node:fs"; import type { ButtonInteraction, ComponentData } from "@buape/carbon"; import { Routes } from "discord-api-types/v10"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -13,6 +14,8 @@ const { STORE_PATH, mockSessionStoreEntries } = vi.hoisted(() => ({ const writeStore = (store: Record) => { mockSessionStoreEntries.value = JSON.parse(JSON.stringify(store)) as Record; + fs.writeFileSync(STORE_PATH, `${JSON.stringify(store, null, 2)}\n`, "utf8"); + clearSessionStoreCacheForTest(); }; beforeEach(() => { @@ -524,6 +527,42 @@ describe("DiscordExecApprovalHandler.shouldHandle", () => { expect(matching.shouldHandle(createRequest())).toBe(true); }); + it("filters by discord account from explicit turn-source bindings when the session store misses", () => { + const handler = createHandler({ enabled: true, approvers: ["123"] }, "default"); + const matching = createHandler({ enabled: true, approvers: ["123"] }, "secondary"); + + expect( + handler.shouldHandle( + createRequest({ + sessionKey: "agent:test-agent:missing", + turnSourceChannel: "discord", + turnSourceAccountId: "secondary", + }), + ), + ).toBe(false); + expect( + matching.shouldHandle( + createRequest({ + sessionKey: "agent:test-agent:missing", + turnSourceChannel: "discord", + turnSourceAccountId: "secondary", + }), + ), + ).toBe(true); + }); + + it("rejects requests bound to another channel before account-specific handling", () => { + const handler = createHandler({ enabled: true, approvers: ["123"] }, "default"); + expect( + handler.shouldHandle( + createRequest({ + turnSourceChannel: "slack", + turnSourceAccountId: "default", + }), + ), + ).toBe(false); + }); + it("combines agent and session filters", () => { const handler = createHandler({ enabled: true, diff --git a/extensions/discord/src/monitor/exec-approvals.ts b/extensions/discord/src/monitor/exec-approvals.ts index ea30fe6fe9d..80c86930fb4 100644 --- a/extensions/discord/src/monitor/exec-approvals.ts +++ b/extensions/discord/src/monitor/exec-approvals.ts @@ -11,10 +11,10 @@ import { } from "@buape/carbon"; import { ButtonStyle, Routes } from "discord-api-types/v10"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/config-runtime"; import type { DiscordExecApprovalConfig } from "openclaw/plugin-sdk/config-runtime"; import { createExecApprovalChannelRuntime, + doesApprovalRequestMatchChannelAccount, type ExecApprovalChannelRuntime, resolveChannelNativeApprovalDeliveryPlan, } from "openclaw/plugin-sdk/infra-runtime"; @@ -29,11 +29,6 @@ import type { PluginApprovalRequest, PluginApprovalResolved, } from "openclaw/plugin-sdk/infra-runtime"; -import { - normalizeAccountId, - normalizeMessageChannel, - resolveAgentIdFromSessionKey, -} from "openclaw/plugin-sdk/routing"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { compileSafeRegex, testRegexWithBoundedInput } from "openclaw/plugin-sdk/security-runtime"; import { logDebug, logError } from "openclaw/plugin-sdk/text-runtime"; @@ -195,61 +190,6 @@ class ExecApprovalActionRow extends Row