diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e55306dc22..178b995df90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Telegram/Discord extensions: propagate trusted `mediaLocalRoots` through extension outbound `sendMedia` options so extension direct-send media paths honor agent-scoped local-media allowlists. (#20029, #21903, #23227) - Plugins/Media sandbox: propagate trusted `mediaLocalRoots` through plugin action dispatch (including Discord/Telegram action adapters) so plugin send paths enforce the same agent-scoped local-media sandbox roots as core outbound sends. (#20258, #22718) - Agents/Workspace guard: map sandbox container-workdir file-tool paths (for example `/workspace/...` and `file:///workspace/...`) to host workspace roots before workspace-only validation, preventing false `Path escapes sandbox root` rejections for sandbox file tools. (#9560) - Gateway/Exec approvals: expire approval requests immediately when no approval-capable gateway clients are connected and no forwarding targets are available, avoiding delayed approvals after restarts/offline approver windows. (#22144) diff --git a/extensions/discord/src/channel.test.ts b/extensions/discord/src/channel.test.ts new file mode 100644 index 00000000000..b5981e77d93 --- /dev/null +++ b/extensions/discord/src/channel.test.ts @@ -0,0 +1,36 @@ +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; +import { describe, expect, it, vi } from "vitest"; +import { discordPlugin } from "./channel.js"; +import { setDiscordRuntime } from "./runtime.js"; + +describe("discordPlugin outbound", () => { + it("forwards mediaLocalRoots to sendMessageDiscord", async () => { + const sendMessageDiscord = vi.fn(async () => ({ messageId: "m1" })); + setDiscordRuntime({ + channel: { + discord: { + sendMessageDiscord, + }, + }, + } as unknown as PluginRuntime); + + const result = await discordPlugin.outbound!.sendMedia!({ + cfg: {} as OpenClawConfig, + to: "channel:123", + text: "hi", + mediaUrl: "/tmp/image.png", + mediaLocalRoots: ["/tmp/agent-root"], + accountId: "work", + }); + + expect(sendMessageDiscord).toHaveBeenCalledWith( + "channel:123", + "hi", + expect.objectContaining({ + mediaUrl: "/tmp/image.png", + mediaLocalRoots: ["/tmp/agent-root"], + }), + ); + expect(result).toMatchObject({ channel: "discord", messageId: "m1" }); + }); +}); diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 815dafbf667..446f8747b89 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -311,11 +311,21 @@ export const discordPlugin: ChannelPlugin = { }); return { channel: "discord", ...result }; }, - sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId, silent }) => { + sendMedia: async ({ + to, + text, + mediaUrl, + mediaLocalRoots, + accountId, + deps, + replyToId, + silent, + }) => { const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord; const result = await send(to, text, { verbose: false, mediaUrl, + mediaLocalRoots, replyTo: replyToId ?? undefined, accountId: accountId ?? undefined, silent: silent ?? undefined, diff --git a/extensions/telegram/src/channel.test.ts b/extensions/telegram/src/channel.test.ts index 07399ef2ba7..ffe4ce58fb7 100644 --- a/extensions/telegram/src/channel.test.ts +++ b/extensions/telegram/src/channel.test.ts @@ -162,4 +162,34 @@ describe("telegramPlugin duplicate token guard", () => { }), ); }); + + it("forwards mediaLocalRoots to sendMessageTelegram for outbound media sends", async () => { + const sendMessageTelegram = vi.fn(async () => ({ messageId: "tg-1" })); + setTelegramRuntime({ + channel: { + telegram: { + sendMessageTelegram, + }, + }, + } as unknown as PluginRuntime); + + const result = await telegramPlugin.outbound!.sendMedia!({ + cfg: createCfg(), + to: "12345", + text: "hello", + mediaUrl: "/tmp/image.png", + mediaLocalRoots: ["/tmp/agent-root"], + accountId: "ops", + }); + + expect(sendMessageTelegram).toHaveBeenCalledWith( + "12345", + "hello", + expect.objectContaining({ + mediaUrl: "/tmp/image.png", + mediaLocalRoots: ["/tmp/agent-root"], + }), + ); + expect(result).toMatchObject({ channel: "telegram", messageId: "tg-1" }); + }); }); diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index e82950ed5bf..c562d12470d 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -332,13 +332,24 @@ export const telegramPlugin: ChannelPlugin { + sendMedia: async ({ + to, + text, + mediaUrl, + mediaLocalRoots, + accountId, + deps, + replyToId, + threadId, + silent, + }) => { const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram; const replyToMessageId = parseTelegramReplyToMessageId(replyToId); const messageThreadId = parseTelegramThreadId(threadId); const result = await send(to, text, { verbose: false, mediaUrl, + mediaLocalRoots, messageThreadId, replyToMessageId, accountId: accountId ?? undefined,