mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
Outbound: scope core send media roots by agent (#17268)
Merged with gates skipped by maintainer request.
Prepared head SHA: 663ac49b3a
This commit is contained in:
committed by
GitHub
parent
b4f16001aa
commit
9adcccadb1
@@ -128,6 +128,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Media/Security: allow local media reads from OpenClaw state `workspace/` and `sandboxes/` roots by default so generated workspace media can be delivered without unsafe global path bypasses. (#15541) Thanks @lanceji.
|
||||
- Media/Security: harden local media allowlist bypasses by requiring an explicit `readFile` override when callers mark paths as validated, and reject filesystem-root `localRoots` entries. (#16739)
|
||||
- Media/Security: allow outbound local media reads from the active agent workspace (including `workspace-<agentId>`) via agent-scoped local roots, avoiding broad global allowlisting of all per-agent workspaces. (#17136) Thanks @MisterGuy420.
|
||||
- Outbound/Media: thread explicit `agentId` through core `sendMessage` direct-delivery path so agent-scoped local media roots apply even when mirror metadata is absent. (#17268) Thanks @gumadeiras.
|
||||
- Discord/Security: harden voice message media loading (SSRF + allowed-local-root checks) so tool-supplied paths/URLs cannot be used to probe internal URLs or read arbitrary local files.
|
||||
- Security/BlueBubbles: require explicit `mediaLocalRoots` allowlists for local outbound media path reads to prevent local file disclosure. (#16322) Thanks @mbelinky.
|
||||
- Security/BlueBubbles: reject ambiguous shared-path webhook routing when multiple webhook targets match the same guid/password.
|
||||
|
||||
@@ -102,6 +102,7 @@ describe("runMessageAction threading auto-injection", () => {
|
||||
});
|
||||
|
||||
const call = mocks.executeSendAction.mock.calls[0]?.[0];
|
||||
expect(call?.ctx?.agentId).toBe("main");
|
||||
expect(call?.ctx?.mirror?.sessionKey).toBe("agent:main:slack:channel:c123:thread:111.222");
|
||||
});
|
||||
|
||||
|
||||
@@ -509,6 +509,7 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
cfg,
|
||||
channel,
|
||||
params,
|
||||
agentId,
|
||||
accountId: accountId ?? undefined,
|
||||
gateway,
|
||||
toolContext: input.toolContext,
|
||||
|
||||
54
src/infra/outbound/message.test.ts
Normal file
54
src/infra/outbound/message.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getChannelPlugin: vi.fn(),
|
||||
resolveOutboundTarget: vi.fn(),
|
||||
deliverOutboundPayloads: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
normalizeChannelId: (channel?: string) => channel?.trim().toLowerCase() ?? undefined,
|
||||
getChannelPlugin: mocks.getChannelPlugin,
|
||||
}));
|
||||
|
||||
vi.mock("./targets.js", () => ({
|
||||
resolveOutboundTarget: mocks.resolveOutboundTarget,
|
||||
}));
|
||||
|
||||
vi.mock("./deliver.js", () => ({
|
||||
deliverOutboundPayloads: mocks.deliverOutboundPayloads,
|
||||
}));
|
||||
|
||||
import { sendMessage } from "./message.js";
|
||||
|
||||
describe("sendMessage", () => {
|
||||
beforeEach(() => {
|
||||
mocks.getChannelPlugin.mockReset();
|
||||
mocks.resolveOutboundTarget.mockReset();
|
||||
mocks.deliverOutboundPayloads.mockReset();
|
||||
|
||||
mocks.getChannelPlugin.mockReturnValue({
|
||||
outbound: { deliveryMode: "direct" },
|
||||
});
|
||||
mocks.resolveOutboundTarget.mockImplementation(({ to }: { to: string }) => ({ ok: true, to }));
|
||||
mocks.deliverOutboundPayloads.mockResolvedValue([{ channel: "mattermost", messageId: "m1" }]);
|
||||
});
|
||||
|
||||
it("passes explicit agentId to outbound delivery for scoped media roots", async () => {
|
||||
await sendMessage({
|
||||
cfg: {},
|
||||
channel: "mattermost",
|
||||
to: "channel:town-square",
|
||||
content: "hi",
|
||||
agentId: "work",
|
||||
});
|
||||
|
||||
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "work",
|
||||
channel: "mattermost",
|
||||
to: "channel:town-square",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,8 @@ export type MessageGatewayOptions = {
|
||||
type MessageSendParams = {
|
||||
to: string;
|
||||
content: string;
|
||||
/** Active agent id for per-agent outbound media root scoping. */
|
||||
agentId?: string;
|
||||
channel?: string;
|
||||
mediaUrl?: string;
|
||||
mediaUrls?: string[];
|
||||
@@ -179,6 +181,7 @@ export async function sendMessage(params: MessageSendParams): Promise<MessageSen
|
||||
cfg,
|
||||
channel: outboundChannel,
|
||||
to: resolvedTarget.to,
|
||||
agentId: params.agentId,
|
||||
accountId: params.accountId,
|
||||
payloads: normalizedPayloads,
|
||||
replyToId: params.replyToId,
|
||||
|
||||
55
src/infra/outbound/outbound-send-service.test.ts
Normal file
55
src/infra/outbound/outbound-send-service.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
dispatchChannelMessageAction: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../channels/plugins/message-actions.js", () => ({
|
||||
dispatchChannelMessageAction: (...args: unknown[]) => mocks.dispatchChannelMessageAction(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./message.js", () => ({
|
||||
sendMessage: (...args: unknown[]) => mocks.sendMessage(...args),
|
||||
sendPoll: vi.fn(),
|
||||
}));
|
||||
|
||||
import { executeSendAction } from "./outbound-send-service.js";
|
||||
|
||||
describe("executeSendAction", () => {
|
||||
beforeEach(() => {
|
||||
mocks.dispatchChannelMessageAction.mockReset();
|
||||
mocks.sendMessage.mockReset();
|
||||
});
|
||||
|
||||
it("forwards ctx.agentId to sendMessage on core outbound path", async () => {
|
||||
mocks.dispatchChannelMessageAction.mockResolvedValue(null);
|
||||
mocks.sendMessage.mockResolvedValue({
|
||||
channel: "discord",
|
||||
to: "channel:123",
|
||||
via: "direct",
|
||||
mediaUrl: null,
|
||||
});
|
||||
|
||||
await executeSendAction({
|
||||
ctx: {
|
||||
cfg: {},
|
||||
channel: "discord",
|
||||
params: {},
|
||||
agentId: "work",
|
||||
dryRun: false,
|
||||
},
|
||||
to: "channel:123",
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "work",
|
||||
channel: "discord",
|
||||
to: "channel:123",
|
||||
content: "hello",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,8 @@ export type OutboundSendContext = {
|
||||
cfg: OpenClawConfig;
|
||||
channel: ChannelId;
|
||||
params: Record<string, unknown>;
|
||||
/** Active agent id for per-agent outbound media root scoping. */
|
||||
agentId?: string;
|
||||
accountId?: string | null;
|
||||
gateway?: OutboundGatewayContext;
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
@@ -93,6 +95,7 @@ export async function executeSendAction(params: {
|
||||
cfg: params.ctx.cfg,
|
||||
to: params.to,
|
||||
content: params.message,
|
||||
agentId: params.ctx.agentId,
|
||||
mediaUrl: params.mediaUrl || undefined,
|
||||
mediaUrls: params.mediaUrls,
|
||||
channel: params.ctx.channel || undefined,
|
||||
|
||||
Reference in New Issue
Block a user