mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 23:56:07 +00:00
fix(media): honor sender policy for host media reads (#64459)
* fix(media): honor sender policy for host media reads * fix(media): clarify host read group policy gating * fix(media): forward sender identity for outbound reads * fix(media): propagate non-id sender fields through outbound session for e164/username/name policy matching * fix(media): preserve requester provider for host read policy * fix(media): forward full sender identity through followup and core send paths * fix(media): forward requester session/account context through core send fallback * fix(media): preserve account policy fallback for requester-scoped host reads * chore(changelog): add outbound media sender-policy entry * fix(media): align test call shape with production — omit messageProvider when sessionKey is set Addresses P2 review: production call sites pass messageProvider: undefined when sessionKey is present; tests should mirror that so regressions in the precedence order are caught. --------- Co-authored-by: Devin Robison <drobison@nvidia.com>
This commit is contained in:
@@ -132,6 +132,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Browser/security: default browser SSRF policy to strict mode so unconfigured installs block private-network navigation, and align external-content marker span mapping so ZWS-injected boundary spoofs are fully sanitized. (#63885) Thanks @eleqtrizit.
|
||||
- Browser/security: apply SSRF navigation policy to subframe document navigations so iframe-targeted private-network hops are blocked without quarantining the parent page. (#64371) Thanks @eleqtrizit.
|
||||
- Hooks/security: mark agent hook system events as untrusted and sanitize hook display names before cron metadata reuse. (#64372) Thanks @eleqtrizit.
|
||||
- Media/security: honor sender-scoped `toolsBySender` policy for outbound host-media reads so denied senders cannot trigger host file disclosure via attachment hydration. (#64459) Thanks @eleqtrizit.
|
||||
## 2026.4.9
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -127,6 +127,7 @@ export async function emitResetCommandHooks(params: {
|
||||
to,
|
||||
sessionKey: params.sessionKey,
|
||||
accountId: params.ctx.AccountId,
|
||||
requesterSenderId: params.command.senderId,
|
||||
threadId: params.ctx.MessageThreadId,
|
||||
cfg: params.cfg,
|
||||
});
|
||||
|
||||
@@ -252,6 +252,7 @@ export function createAcpDispatchDeliveryCoordinator(params: {
|
||||
message,
|
||||
},
|
||||
sessionKey: params.ctx.SessionKey,
|
||||
requesterAccountId: params.ctx.AccountId,
|
||||
});
|
||||
state.routedCounts.tool += 1;
|
||||
return true;
|
||||
@@ -316,6 +317,10 @@ export function createAcpDispatchDeliveryCoordinator(params: {
|
||||
to: params.originatingTo,
|
||||
sessionKey: params.ctx.SessionKey,
|
||||
accountId: resolvedAccountId,
|
||||
requesterSenderId: params.ctx.SenderId,
|
||||
requesterSenderName: params.ctx.SenderName,
|
||||
requesterSenderUsername: params.ctx.SenderUsername,
|
||||
requesterSenderE164: params.ctx.SenderE164,
|
||||
threadId: params.ctx.MessageThreadId,
|
||||
cfg: params.cfg,
|
||||
});
|
||||
|
||||
@@ -351,6 +351,10 @@ export async function dispatchReplyFromConfig(params: {
|
||||
to: originatingTo,
|
||||
sessionKey: ctx.SessionKey,
|
||||
accountId: ctx.AccountId,
|
||||
requesterSenderId: ctx.SenderId,
|
||||
requesterSenderName: ctx.SenderName,
|
||||
requesterSenderUsername: ctx.SenderUsername,
|
||||
requesterSenderE164: ctx.SenderE164,
|
||||
threadId: routeThreadId,
|
||||
cfg,
|
||||
abortSignal,
|
||||
@@ -374,6 +378,10 @@ export async function dispatchReplyFromConfig(params: {
|
||||
to: originatingTo,
|
||||
sessionKey: ctx.SessionKey,
|
||||
accountId: ctx.AccountId,
|
||||
requesterSenderId: ctx.SenderId,
|
||||
requesterSenderName: ctx.SenderName,
|
||||
requesterSenderUsername: ctx.SenderUsername,
|
||||
requesterSenderE164: ctx.SenderE164,
|
||||
threadId: routeThreadId,
|
||||
cfg,
|
||||
isGroup,
|
||||
@@ -530,6 +538,10 @@ export async function dispatchReplyFromConfig(params: {
|
||||
to: originatingTo,
|
||||
sessionKey: ctx.SessionKey,
|
||||
accountId: ctx.AccountId,
|
||||
requesterSenderId: ctx.SenderId,
|
||||
requesterSenderName: ctx.SenderName,
|
||||
requesterSenderUsername: ctx.SenderUsername,
|
||||
requesterSenderE164: ctx.SenderE164,
|
||||
threadId: routeThreadId,
|
||||
cfg,
|
||||
isGroup,
|
||||
@@ -587,6 +599,10 @@ export async function dispatchReplyFromConfig(params: {
|
||||
to: originatingTo,
|
||||
sessionKey: ctx.SessionKey,
|
||||
accountId: ctx.AccountId,
|
||||
requesterSenderId: ctx.SenderId,
|
||||
requesterSenderName: ctx.SenderName,
|
||||
requesterSenderUsername: ctx.SenderUsername,
|
||||
requesterSenderE164: ctx.SenderE164,
|
||||
threadId: routeThreadId,
|
||||
cfg,
|
||||
isGroup,
|
||||
@@ -1012,6 +1028,10 @@ export async function dispatchReplyFromConfig(params: {
|
||||
to: originatingTo,
|
||||
sessionKey: ctx.SessionKey,
|
||||
accountId: ctx.AccountId,
|
||||
requesterSenderId: ctx.SenderId,
|
||||
requesterSenderName: ctx.SenderName,
|
||||
requesterSenderUsername: ctx.SenderUsername,
|
||||
requesterSenderE164: ctx.SenderE164,
|
||||
threadId: routeThreadId,
|
||||
cfg,
|
||||
isGroup,
|
||||
|
||||
@@ -102,6 +102,10 @@ export function createFollowupRunner(params: {
|
||||
to: originatingTo,
|
||||
sessionKey: queued.run.sessionKey,
|
||||
accountId: queued.originatingAccountId,
|
||||
requesterSenderId: queued.run.senderId,
|
||||
requesterSenderName: queued.run.senderName,
|
||||
requesterSenderUsername: queued.run.senderUsername,
|
||||
requesterSenderE164: queued.run.senderE164,
|
||||
threadId: queued.originatingThreadId,
|
||||
cfg: runtimeConfig,
|
||||
});
|
||||
|
||||
@@ -46,6 +46,14 @@ export type RouteReplyParams = {
|
||||
sessionKey?: string;
|
||||
/** Provider account id (multi-account). */
|
||||
accountId?: string;
|
||||
/** Originating sender id for sender-scoped outbound media policy. */
|
||||
requesterSenderId?: string;
|
||||
/** Originating sender display name for name-keyed sender policy matching. */
|
||||
requesterSenderName?: string;
|
||||
/** Originating sender username for username-keyed sender policy matching. */
|
||||
requesterSenderUsername?: string;
|
||||
/** Originating sender E.164 phone number for e164-keyed sender policy matching. */
|
||||
requesterSenderE164?: string;
|
||||
/** Thread id for replies (Telegram topic id or Matrix thread event id). */
|
||||
threadId?: string | number;
|
||||
/** Config for provider-specific settings. */
|
||||
@@ -187,6 +195,10 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
|
||||
cfg,
|
||||
agentId: resolvedAgentId,
|
||||
sessionKey: params.sessionKey,
|
||||
requesterSenderId: params.requesterSenderId,
|
||||
requesterSenderName: params.requesterSenderName,
|
||||
requesterSenderUsername: params.requesterSenderUsername,
|
||||
requesterSenderE164: params.requesterSenderE164,
|
||||
});
|
||||
const results = await deliverOutboundPayloads({
|
||||
cfg,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
whatsappOutbound,
|
||||
} from "../../../test/helpers/infra/deliver-test-outbounds.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import * as mediaCapabilityModule from "../../media/read-capability.js";
|
||||
import { createHookRunner } from "../../plugins/hooks.js";
|
||||
import { addTestHook } from "../../plugins/hooks.test-helpers.js";
|
||||
import { createEmptyPluginRegistry } from "../../plugins/registry.js";
|
||||
@@ -217,6 +218,100 @@ describe("deliverOutboundPayloads", () => {
|
||||
releasePinnedPluginChannelRegistry();
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
it("keeps requester session channel authoritative for delivery media policy", async () => {
|
||||
const resolveMediaAccessSpy = vi.spyOn(
|
||||
mediaCapabilityModule,
|
||||
"resolveAgentScopedOutboundMediaAccess",
|
||||
);
|
||||
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
|
||||
|
||||
await deliverOutboundPayloads({
|
||||
cfg: {},
|
||||
channel: "whatsapp",
|
||||
to: "+1555",
|
||||
payloads: [{ text: "hello" }],
|
||||
deps: { whatsapp: sendWhatsApp },
|
||||
session: {
|
||||
key: "agent:main:whatsapp:group:ops",
|
||||
requesterSenderId: "attacker",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveMediaAccessSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
messageProvider: undefined,
|
||||
requesterSenderId: "attacker",
|
||||
}),
|
||||
);
|
||||
resolveMediaAccessSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("forwards all sender fields to media access for non-id policy matching", async () => {
|
||||
const resolveMediaAccessSpy = vi.spyOn(
|
||||
mediaCapabilityModule,
|
||||
"resolveAgentScopedOutboundMediaAccess",
|
||||
);
|
||||
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w2", toJid: "jid" });
|
||||
|
||||
await deliverOutboundPayloads({
|
||||
cfg: {},
|
||||
channel: "whatsapp",
|
||||
to: "+1555",
|
||||
payloads: [{ text: "hello" }],
|
||||
deps: { whatsapp: sendWhatsApp },
|
||||
session: {
|
||||
key: "agent:main:whatsapp:group:ops",
|
||||
requesterSenderId: "id:whatsapp:123",
|
||||
requesterSenderName: "Alice",
|
||||
requesterSenderUsername: "alice_u",
|
||||
requesterSenderE164: "+15551234567",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveMediaAccessSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSenderId: "id:whatsapp:123",
|
||||
requesterSenderName: "Alice",
|
||||
requesterSenderUsername: "alice_u",
|
||||
requesterSenderE164: "+15551234567",
|
||||
}),
|
||||
);
|
||||
resolveMediaAccessSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("uses requester account from session for delivery media policy", async () => {
|
||||
const resolveMediaAccessSpy = vi.spyOn(
|
||||
mediaCapabilityModule,
|
||||
"resolveAgentScopedOutboundMediaAccess",
|
||||
);
|
||||
const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w3", toJid: "jid" });
|
||||
|
||||
await deliverOutboundPayloads({
|
||||
cfg: {},
|
||||
channel: "whatsapp",
|
||||
to: "+1555",
|
||||
accountId: "destination-account",
|
||||
payloads: [{ text: "hello" }],
|
||||
deps: { whatsapp: sendWhatsApp },
|
||||
session: {
|
||||
key: "agent:main:whatsapp:group:ops",
|
||||
requesterAccountId: "source-account",
|
||||
requesterSenderId: "attacker",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveMediaAccessSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
accountId: "source-account",
|
||||
requesterSenderId: "attacker",
|
||||
}),
|
||||
);
|
||||
resolveMediaAccessSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("chunks direct adapter text and preserves delivery overrides across sends", async () => {
|
||||
const sendText = vi.fn().mockImplementation(async ({ text }: { text: string }) => ({
|
||||
channel: "matrix" as const,
|
||||
|
||||
@@ -568,6 +568,13 @@ async function deliverOutboundPayloadsCore(
|
||||
cfg,
|
||||
agentId: params.session?.agentId ?? params.mirror?.agentId,
|
||||
mediaSources: collectPayloadMediaSources(payloads),
|
||||
sessionKey: params.session?.key,
|
||||
messageProvider: params.session?.key ? undefined : channel,
|
||||
accountId: params.session?.requesterAccountId ?? accountId,
|
||||
requesterSenderId: params.session?.requesterSenderId,
|
||||
requesterSenderName: params.session?.requesterSenderName,
|
||||
requesterSenderUsername: params.session?.requesterSenderUsername,
|
||||
requesterSenderE164: params.session?.requesterSenderE164,
|
||||
});
|
||||
const results: OutboundDeliveryResult[] = [];
|
||||
const handler = await createChannelHandler({
|
||||
|
||||
@@ -301,6 +301,259 @@ describe("runMessageAction plugin dispatch", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses requester session channel policy for host-media reads", async () => {
|
||||
const handlePolicyCheckedAction = vi.fn(async ({ mediaAccess }) =>
|
||||
jsonResult({
|
||||
ok: true,
|
||||
hasHostReadCapability: typeof mediaAccess?.readFile === "function",
|
||||
}),
|
||||
);
|
||||
const policyPlugin: ChannelPlugin = {
|
||||
id: "feishu",
|
||||
meta: {
|
||||
id: "feishu",
|
||||
label: "Feishu",
|
||||
selectionLabel: "Feishu",
|
||||
docsPath: "/channels/feishu",
|
||||
blurb: "Feishu policy test plugin.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct", "channel"], media: true },
|
||||
config: createAlwaysConfiguredPluginConfig(),
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: () => true,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
supportsAction: ({ action }) => action === "send",
|
||||
handleAction: handlePolicyCheckedAction,
|
||||
},
|
||||
};
|
||||
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "feishu",
|
||||
source: "test",
|
||||
plugin: policyPlugin,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await runMessageAction({
|
||||
cfg: {
|
||||
tools: { allow: ["read"] },
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
},
|
||||
whatsapp: {
|
||||
groups: {
|
||||
ops: {
|
||||
toolsBySender: {
|
||||
"id:trusted-user": {
|
||||
deny: ["read"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "feishu",
|
||||
target: "oc_123",
|
||||
message: "hello",
|
||||
media: "/tmp/host.png",
|
||||
},
|
||||
requesterSenderId: "trusted-user",
|
||||
sessionKey: "agent:alpha:whatsapp:group:ops",
|
||||
dryRun: false,
|
||||
});
|
||||
|
||||
const pluginCall = handlePolicyCheckedAction.mock.calls[0]?.[0];
|
||||
expect(pluginCall?.mediaAccess).toBeDefined();
|
||||
expect(pluginCall?.mediaAccess?.readFile).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses requester account policy for host-media reads when destination account differs", async () => {
|
||||
const handlePolicyCheckedAction = vi.fn(async ({ mediaAccess }) =>
|
||||
jsonResult({
|
||||
ok: true,
|
||||
hasHostReadCapability: typeof mediaAccess?.readFile === "function",
|
||||
}),
|
||||
);
|
||||
const policyPlugin: ChannelPlugin = {
|
||||
id: "feishu",
|
||||
meta: {
|
||||
id: "feishu",
|
||||
label: "Feishu",
|
||||
selectionLabel: "Feishu",
|
||||
docsPath: "/channels/feishu",
|
||||
blurb: "Feishu account policy test plugin.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct", "channel"], media: true },
|
||||
config: createAlwaysConfiguredPluginConfig(),
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: () => true,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
supportsAction: ({ action }) => action === "send",
|
||||
handleAction: handlePolicyCheckedAction,
|
||||
},
|
||||
};
|
||||
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "feishu",
|
||||
source: "test",
|
||||
plugin: policyPlugin,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await runMessageAction({
|
||||
cfg: {
|
||||
tools: { allow: ["read"] },
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
},
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
source: {
|
||||
groups: {
|
||||
ops: {
|
||||
toolsBySender: {
|
||||
"id:trusted-user": {
|
||||
deny: ["read"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destination: {
|
||||
groups: {
|
||||
ops: {
|
||||
toolsBySender: {
|
||||
"id:trusted-user": {
|
||||
allow: ["read"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "feishu",
|
||||
accountId: "destination",
|
||||
target: "oc_123",
|
||||
message: "hello",
|
||||
media: "/tmp/host.png",
|
||||
},
|
||||
requesterAccountId: "source",
|
||||
requesterSenderId: "trusted-user",
|
||||
sessionKey: "agent:alpha:whatsapp:group:ops",
|
||||
dryRun: false,
|
||||
});
|
||||
|
||||
const pluginCall = handlePolicyCheckedAction.mock.calls[0]?.[0];
|
||||
expect(pluginCall?.accountId).toBe("destination");
|
||||
expect(pluginCall?.mediaAccess).toBeDefined();
|
||||
expect(pluginCall?.mediaAccess?.readFile).toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back to the resolved account policy when requester account is unavailable", async () => {
|
||||
const handlePolicyCheckedAction = vi.fn(async ({ mediaAccess }) =>
|
||||
jsonResult({
|
||||
ok: true,
|
||||
hasHostReadCapability: typeof mediaAccess?.readFile === "function",
|
||||
}),
|
||||
);
|
||||
const policyPlugin: ChannelPlugin = {
|
||||
id: "whatsapp",
|
||||
meta: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp",
|
||||
docsPath: "/channels/whatsapp",
|
||||
blurb: "WhatsApp account policy fallback test plugin.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct", "channel"], media: true },
|
||||
config: createAlwaysConfiguredPluginConfig(),
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: () => true,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
describeMessageTool: () => ({ actions: ["send"] }),
|
||||
supportsAction: ({ action }) => action === "send",
|
||||
handleAction: handlePolicyCheckedAction,
|
||||
},
|
||||
};
|
||||
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
source: "test",
|
||||
plugin: policyPlugin,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await runMessageAction({
|
||||
cfg: {
|
||||
tools: { allow: ["read"] },
|
||||
channels: {
|
||||
whatsapp: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
source: {
|
||||
groups: {
|
||||
ops: {
|
||||
toolsBySender: {
|
||||
"id:trusted-user": {
|
||||
deny: ["read"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "whatsapp",
|
||||
accountId: "source",
|
||||
target: "group:ops",
|
||||
message: "hello",
|
||||
media: "/tmp/host.png",
|
||||
},
|
||||
requesterSenderId: "trusted-user",
|
||||
sessionKey: "agent:alpha:whatsapp:group:ops",
|
||||
dryRun: false,
|
||||
});
|
||||
|
||||
const pluginCall = handlePolicyCheckedAction.mock.calls[0]?.[0];
|
||||
expect(pluginCall?.accountId).toBe("source");
|
||||
expect(pluginCall?.mediaAccess).toBeDefined();
|
||||
expect(pluginCall?.mediaAccess?.readFile).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("card-only send behavior", () => {
|
||||
|
||||
@@ -78,6 +78,7 @@ export type RunMessageActionParams = {
|
||||
action: ChannelMessageActionName;
|
||||
params: Record<string, unknown>;
|
||||
defaultAccountId?: string;
|
||||
requesterAccountId?: string | null;
|
||||
requesterSenderId?: string | null;
|
||||
senderIsOwner?: boolean;
|
||||
sessionId?: string;
|
||||
@@ -543,6 +544,9 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
channel,
|
||||
params,
|
||||
agentId,
|
||||
sessionKey: input.sessionKey,
|
||||
requesterAccountId: input.requesterAccountId ?? undefined,
|
||||
requesterSenderId: input.requesterSenderId ?? undefined,
|
||||
mediaAccess: ctx.mediaAccess,
|
||||
accountId: accountId ?? undefined,
|
||||
gateway,
|
||||
@@ -777,6 +781,10 @@ export async function runMessageAction(
|
||||
cfg,
|
||||
agentId: resolvedAgentId,
|
||||
mediaSources: collectActionMediaSourceHints(params),
|
||||
sessionKey: input.sessionKey,
|
||||
messageProvider: input.sessionKey ? undefined : channel,
|
||||
accountId: input.sessionKey ? (input.requesterAccountId ?? accountId) : accountId,
|
||||
requesterSenderId: input.requesterSenderId,
|
||||
});
|
||||
const mediaPolicy = resolveAttachmentMediaPolicy({
|
||||
sandboxRoot: input.sandboxRoot,
|
||||
|
||||
@@ -89,6 +89,56 @@ describe("sendMessage", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards requesterSenderId into the outbound delivery session", async () => {
|
||||
await sendMessage({
|
||||
cfg: {},
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
content: "hi",
|
||||
requesterSenderId: "attacker",
|
||||
mirror: {
|
||||
sessionKey: "agent:main:telegram:group:ops",
|
||||
},
|
||||
});
|
||||
|
||||
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
session: expect.objectContaining({
|
||||
key: "agent:main:telegram:group:ops",
|
||||
requesterSenderId: "attacker",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses requester session/account for outbound delivery policy context", async () => {
|
||||
await sendMessage({
|
||||
cfg: {},
|
||||
channel: "telegram",
|
||||
to: "123456",
|
||||
content: "hi",
|
||||
requesterSessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterAccountId: "work",
|
||||
requesterSenderId: "attacker",
|
||||
mirror: {
|
||||
sessionKey: "agent:main:telegram:dm:123456",
|
||||
},
|
||||
});
|
||||
|
||||
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
session: expect.objectContaining({
|
||||
key: "agent:main:whatsapp:group:ops",
|
||||
requesterAccountId: "work",
|
||||
requesterSenderId: "attacker",
|
||||
}),
|
||||
mirror: expect.objectContaining({
|
||||
sessionKey: "agent:main:telegram:dm:123456",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("propagates the send idempotency key into mirrored transcript delivery", async () => {
|
||||
await sendMessage({
|
||||
cfg: {},
|
||||
|
||||
@@ -49,6 +49,12 @@ type MessageSendParams = {
|
||||
content: string;
|
||||
/** Active agent id for per-agent outbound media root scoping. */
|
||||
agentId?: string;
|
||||
/** Originating session key used for requester-scoped outbound media policy. */
|
||||
requesterSessionKey?: string;
|
||||
/** Originating account id used for requester-scoped outbound media policy. */
|
||||
requesterAccountId?: string;
|
||||
/** Originating sender id used for sender-scoped outbound media policy. */
|
||||
requesterSenderId?: string;
|
||||
channel?: string;
|
||||
mediaUrl?: string;
|
||||
mediaUrls?: string[];
|
||||
@@ -265,7 +271,9 @@ export async function sendMessage(params: MessageSendParams): Promise<MessageSen
|
||||
const outboundSession = buildOutboundSessionContext({
|
||||
cfg,
|
||||
agentId: params.agentId,
|
||||
sessionKey: params.mirror?.sessionKey,
|
||||
sessionKey: params.requesterSessionKey ?? params.mirror?.sessionKey,
|
||||
requesterAccountId: params.requesterAccountId ?? params.accountId,
|
||||
requesterSenderId: params.requesterSenderId,
|
||||
});
|
||||
const results = await deliverOutboundPayloads({
|
||||
cfg,
|
||||
|
||||
@@ -17,7 +17,13 @@ const createAgentScopedHostMediaReadFileMock = vi.hoisted(() =>
|
||||
);
|
||||
const resolveAgentScopedOutboundMediaAccessMock = vi.hoisted(() =>
|
||||
vi.fn<
|
||||
(params: { cfg: unknown; agentId?: string; mediaSources?: readonly string[] }) => {
|
||||
(params: {
|
||||
cfg: unknown;
|
||||
agentId?: string;
|
||||
mediaSources?: readonly string[];
|
||||
accountId?: string;
|
||||
requesterSenderId?: string;
|
||||
}) => {
|
||||
localRoots: string[];
|
||||
readFile: (filePath: string) => Promise<Buffer>;
|
||||
}
|
||||
@@ -180,6 +186,198 @@ describe("executeSendAction", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards requesterSenderId to sendMessage on core outbound path", async () => {
|
||||
mocks.dispatchChannelMessageAction.mockResolvedValue(null);
|
||||
mocks.sendMessage.mockResolvedValue({
|
||||
channel: "demo-outbound",
|
||||
to: "channel:123",
|
||||
via: "direct",
|
||||
mediaUrl: null,
|
||||
});
|
||||
|
||||
await executeSendAction({
|
||||
ctx: {
|
||||
cfg: {},
|
||||
channel: "demo-outbound",
|
||||
params: {},
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterSenderId: "attacker",
|
||||
dryRun: false,
|
||||
},
|
||||
to: "channel:123",
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSenderId: "attacker",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards requester session context to sendMessage on core outbound path", async () => {
|
||||
mocks.dispatchChannelMessageAction.mockResolvedValue(null);
|
||||
mocks.sendMessage.mockResolvedValue({
|
||||
channel: "demo-outbound",
|
||||
to: "channel:123",
|
||||
via: "direct",
|
||||
mediaUrl: null,
|
||||
});
|
||||
|
||||
await executeSendAction({
|
||||
ctx: {
|
||||
cfg: {},
|
||||
channel: "demo-outbound",
|
||||
params: {},
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterAccountId: "source-account",
|
||||
requesterSenderId: "attacker",
|
||||
accountId: "destination-account",
|
||||
dryRun: false,
|
||||
},
|
||||
to: "channel:123",
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterAccountId: "source-account",
|
||||
requesterSenderId: "attacker",
|
||||
accountId: "destination-account",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards requesterSenderId into outbound media access resolution", async () => {
|
||||
mocks.dispatchChannelMessageAction.mockResolvedValue(pluginActionResult("msg-plugin"));
|
||||
|
||||
await executeSendAction({
|
||||
ctx: {
|
||||
cfg: {},
|
||||
channel: "demo-outbound",
|
||||
params: { media: "/tmp/host.png" },
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterSenderId: "attacker",
|
||||
dryRun: false,
|
||||
},
|
||||
to: "channel:123",
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSenderId: "attacker",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps requester session channel authoritative for media policy", async () => {
|
||||
mocks.dispatchChannelMessageAction.mockResolvedValue(pluginActionResult("msg-plugin"));
|
||||
|
||||
await executeSendAction({
|
||||
ctx: {
|
||||
cfg: {},
|
||||
channel: "demo-outbound",
|
||||
params: { media: "/tmp/host.png" },
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterSenderId: "attacker",
|
||||
dryRun: false,
|
||||
},
|
||||
to: "channel:123",
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
messageProvider: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses requester account for media policy when session context is present", async () => {
|
||||
mocks.dispatchChannelMessageAction.mockResolvedValue(pluginActionResult("msg-plugin"));
|
||||
|
||||
await executeSendAction({
|
||||
ctx: {
|
||||
cfg: {},
|
||||
channel: "demo-outbound",
|
||||
params: { media: "/tmp/host.png" },
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterAccountId: "source-account",
|
||||
requesterSenderId: "attacker",
|
||||
accountId: "destination-account",
|
||||
dryRun: false,
|
||||
},
|
||||
to: "channel:123",
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
accountId: "source-account",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to destination account for media policy when requester account is missing", async () => {
|
||||
mocks.dispatchChannelMessageAction.mockResolvedValue(pluginActionResult("msg-plugin"));
|
||||
|
||||
await executeSendAction({
|
||||
ctx: {
|
||||
cfg: {},
|
||||
channel: "demo-outbound",
|
||||
params: { media: "/tmp/host.png" },
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterSenderId: "attacker",
|
||||
accountId: "destination-account",
|
||||
dryRun: false,
|
||||
},
|
||||
to: "channel:123",
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
accountId: "destination-account",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to destination account when forwarding requester context to sendMessage", async () => {
|
||||
mocks.dispatchChannelMessageAction.mockResolvedValue(null);
|
||||
mocks.sendMessage.mockResolvedValue({
|
||||
channel: "demo-outbound",
|
||||
to: "channel:123",
|
||||
via: "direct",
|
||||
mediaUrl: null,
|
||||
});
|
||||
|
||||
await executeSendAction({
|
||||
ctx: {
|
||||
cfg: {},
|
||||
channel: "demo-outbound",
|
||||
params: {},
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterSenderId: "attacker",
|
||||
accountId: "destination-account",
|
||||
dryRun: false,
|
||||
},
|
||||
to: "channel:123",
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterAccountId: "destination-account",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses plugin poll action when available", async () => {
|
||||
mocks.dispatchChannelMessageAction.mockResolvedValue(pluginActionResult("poll-plugin"));
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ export type OutboundSendContext = {
|
||||
params: Record<string, unknown>;
|
||||
/** Active agent id for per-agent outbound media root scoping. */
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
requesterAccountId?: string;
|
||||
requesterSenderId?: string;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaReadFile?: OutboundMediaReadFile;
|
||||
accountId?: string | null;
|
||||
@@ -69,6 +72,13 @@ async function tryHandleWithPluginAction(params: {
|
||||
cfg: params.ctx.cfg,
|
||||
agentId: params.ctx.agentId ?? params.ctx.mirror?.agentId,
|
||||
mediaSources: collectActionMediaSources(params.ctx.params),
|
||||
sessionKey: params.ctx.sessionKey,
|
||||
messageProvider: params.ctx.sessionKey ? undefined : params.ctx.channel,
|
||||
accountId:
|
||||
(params.ctx.sessionKey
|
||||
? (params.ctx.requesterAccountId ?? params.ctx.accountId)
|
||||
: params.ctx.accountId) ?? undefined,
|
||||
requesterSenderId: params.ctx.requesterSenderId,
|
||||
mediaAccess: params.ctx.mediaAccess,
|
||||
mediaReadFile: params.ctx.mediaReadFile,
|
||||
});
|
||||
@@ -145,6 +155,9 @@ export async function executeSendAction(params: {
|
||||
to: params.to,
|
||||
content: params.message,
|
||||
agentId: params.ctx.agentId,
|
||||
requesterSessionKey: params.ctx.sessionKey,
|
||||
requesterAccountId: params.ctx.requesterAccountId ?? params.ctx.accountId ?? undefined,
|
||||
requesterSenderId: params.ctx.requesterSenderId,
|
||||
mediaUrl: params.mediaUrl || undefined,
|
||||
mediaUrls: params.mediaUrls,
|
||||
channel: params.ctx.channel || undefined,
|
||||
|
||||
@@ -75,4 +75,55 @@ describe("buildOutboundSessionContext", () => {
|
||||
agentId: "explicit-agent",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves a trimmed requester sender id when provided", () => {
|
||||
expect(
|
||||
buildOutboundSessionContext({
|
||||
cfg: {} as never,
|
||||
requesterSenderId: " sender-123 ",
|
||||
}),
|
||||
).toEqual({
|
||||
requesterSenderId: "sender-123",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves a trimmed requester account id when provided", () => {
|
||||
expect(
|
||||
buildOutboundSessionContext({
|
||||
cfg: {} as never,
|
||||
requesterAccountId: " work ",
|
||||
}),
|
||||
).toEqual({
|
||||
requesterAccountId: "work",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves trimmed non-id sender fields for e164/username/name policy matching", () => {
|
||||
expect(
|
||||
buildOutboundSessionContext({
|
||||
cfg: {} as never,
|
||||
requesterSenderId: "id:telegram:123",
|
||||
requesterSenderName: " Alice ",
|
||||
requesterSenderUsername: " alice_u ",
|
||||
requesterSenderE164: " +15551234567 ",
|
||||
}),
|
||||
).toEqual({
|
||||
requesterSenderId: "id:telegram:123",
|
||||
requesterSenderName: "Alice",
|
||||
requesterSenderUsername: "alice_u",
|
||||
requesterSenderE164: "+15551234567",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined when all sender and session fields are blank", () => {
|
||||
expect(
|
||||
buildOutboundSessionContext({
|
||||
cfg: {} as never,
|
||||
requesterSenderId: " ",
|
||||
requesterSenderName: " ",
|
||||
requesterSenderUsername: " ",
|
||||
requesterSenderE164: " ",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,24 +7,57 @@ export type OutboundSessionContext = {
|
||||
key?: string;
|
||||
/** Active agent id used for workspace-scoped media roots. */
|
||||
agentId?: string;
|
||||
/** Originating account id used for requester-scoped group policy resolution. */
|
||||
requesterAccountId?: string;
|
||||
/** Originating sender id used for sender-scoped outbound media policy. */
|
||||
requesterSenderId?: string;
|
||||
/** Originating sender display name for name-keyed sender policy matching. */
|
||||
requesterSenderName?: string;
|
||||
/** Originating sender username for username-keyed sender policy matching. */
|
||||
requesterSenderUsername?: string;
|
||||
/** Originating sender E.164 phone number for e164-keyed sender policy matching. */
|
||||
requesterSenderE164?: string;
|
||||
};
|
||||
|
||||
export function buildOutboundSessionContext(params: {
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey?: string | null;
|
||||
agentId?: string | null;
|
||||
requesterAccountId?: string | null;
|
||||
requesterSenderId?: string | null;
|
||||
requesterSenderName?: string | null;
|
||||
requesterSenderUsername?: string | null;
|
||||
requesterSenderE164?: string | null;
|
||||
}): OutboundSessionContext | undefined {
|
||||
const key = normalizeOptionalString(params.sessionKey);
|
||||
const explicitAgentId = normalizeOptionalString(params.agentId);
|
||||
const requesterAccountId = normalizeOptionalString(params.requesterAccountId);
|
||||
const requesterSenderId = normalizeOptionalString(params.requesterSenderId);
|
||||
const requesterSenderName = normalizeOptionalString(params.requesterSenderName);
|
||||
const requesterSenderUsername = normalizeOptionalString(params.requesterSenderUsername);
|
||||
const requesterSenderE164 = normalizeOptionalString(params.requesterSenderE164);
|
||||
const derivedAgentId = key
|
||||
? resolveSessionAgentId({ sessionKey: key, config: params.cfg })
|
||||
: undefined;
|
||||
const agentId = explicitAgentId ?? derivedAgentId;
|
||||
if (!key && !agentId) {
|
||||
if (
|
||||
!key &&
|
||||
!agentId &&
|
||||
!requesterAccountId &&
|
||||
!requesterSenderId &&
|
||||
!requesterSenderName &&
|
||||
!requesterSenderUsername &&
|
||||
!requesterSenderE164
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(key ? { key } : {}),
|
||||
...(agentId ? { agentId } : {}),
|
||||
...(requesterAccountId ? { requesterAccountId } : {}),
|
||||
...(requesterSenderId ? { requesterSenderId } : {}),
|
||||
...(requesterSenderName ? { requesterSenderName } : {}),
|
||||
...(requesterSenderUsername ? { requesterSenderUsername } : {}),
|
||||
...(requesterSenderE164 ? { requesterSenderE164 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,4 +21,105 @@ describe("resolveAgentScopedOutboundMediaAccess", () => {
|
||||
|
||||
expect(result).toMatchObject({ workspaceDir: "/tmp/explicit-workspace" });
|
||||
});
|
||||
|
||||
it("does not enable host reads when sender group policy denies read", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
ops: {
|
||||
toolsBySender: {
|
||||
"id:attacker": {
|
||||
deny: ["read"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolveAgentScopedOutboundMediaAccess({
|
||||
cfg,
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
// Production call sites set messageProvider: undefined when sessionKey is present;
|
||||
// resolveGroupToolPolicy derives channel from the session key instead.
|
||||
requesterSenderId: "attacker",
|
||||
});
|
||||
|
||||
expect(result.readFile).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps host reads enabled when sender group policy allows read", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
ops: {
|
||||
toolsBySender: {
|
||||
"id:trusted-user": {
|
||||
allow: ["read"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolveAgentScopedOutboundMediaAccess({
|
||||
cfg,
|
||||
sessionKey: "agent:main:whatsapp:group:ops",
|
||||
requesterSenderId: "trusted-user",
|
||||
});
|
||||
|
||||
expect(result.readFile).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("keeps host reads enabled when no group policy applies", () => {
|
||||
const result = resolveAgentScopedOutboundMediaAccess({
|
||||
cfg: {
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
messageProvider: "whatsapp",
|
||||
requesterSenderId: "trusted-user",
|
||||
});
|
||||
|
||||
expect(result.readFile).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("keeps host reads enabled for DM sender when no group context exists", () => {
|
||||
const result = resolveAgentScopedOutboundMediaAccess({
|
||||
cfg: {
|
||||
tools: {
|
||||
allow: ["read"],
|
||||
},
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
ops: {
|
||||
toolsBySender: {
|
||||
"id:dm-sender": {
|
||||
deny: ["read"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
messageProvider: "whatsapp",
|
||||
requesterSenderId: "dm-sender",
|
||||
});
|
||||
|
||||
expect(result.readFile).toBeTypeOf("function");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,70 @@
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import { resolvePathFromInput } from "../agents/path-policy.js";
|
||||
import { resolveGroupToolPolicy } from "../agents/pi-tools.policy.js";
|
||||
import { resolveEffectiveToolFsRootExpansionAllowed } from "../agents/tool-fs-policy.js";
|
||||
import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js";
|
||||
import { resolveWorkspaceRoot } from "../agents/workspace-dir.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { readLocalFileSafely } from "../infra/fs-safe.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import type { OutboundMediaAccess, OutboundMediaReadFile } from "./load-options.js";
|
||||
import { getAgentScopedMediaLocalRootsForSources } from "./local-roots.js";
|
||||
|
||||
export function createAgentScopedHostMediaReadFile(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
workspaceDir?: string;
|
||||
}): OutboundMediaReadFile | undefined {
|
||||
type OutboundHostMediaPolicyContext = {
|
||||
sessionKey?: string;
|
||||
messageProvider?: string;
|
||||
groupId?: string | null;
|
||||
groupChannel?: string | null;
|
||||
groupSpace?: string | null;
|
||||
accountId?: string | null;
|
||||
requesterSenderId?: string | null;
|
||||
requesterSenderName?: string | null;
|
||||
requesterSenderUsername?: string | null;
|
||||
requesterSenderE164?: string | null;
|
||||
};
|
||||
|
||||
function isAgentScopedHostMediaReadAllowed(
|
||||
params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
} & OutboundHostMediaPolicyContext,
|
||||
): boolean {
|
||||
if (
|
||||
!resolveEffectiveToolFsRootExpansionAllowed({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const groupPolicy = resolveGroupToolPolicy({
|
||||
config: params.cfg,
|
||||
sessionKey: params.sessionKey,
|
||||
messageProvider: params.messageProvider,
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
accountId: params.accountId,
|
||||
senderId: normalizeOptionalString(params.requesterSenderId),
|
||||
senderName: normalizeOptionalString(params.requesterSenderName),
|
||||
senderUsername: normalizeOptionalString(params.requesterSenderUsername),
|
||||
senderE164: normalizeOptionalString(params.requesterSenderE164),
|
||||
});
|
||||
// Sender/group policy only applies when a concrete group override exists.
|
||||
if (groupPolicy && !isToolAllowedByPolicies("read", [groupPolicy])) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function createAgentScopedHostMediaReadFile(
|
||||
params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
workspaceDir?: string;
|
||||
} & OutboundHostMediaPolicyContext,
|
||||
): OutboundMediaReadFile | undefined {
|
||||
if (!isAgentScopedHostMediaReadAllowed(params)) {
|
||||
return undefined;
|
||||
}
|
||||
const inferredWorkspaceDir =
|
||||
@@ -30,14 +77,16 @@ export function createAgentScopedHostMediaReadFile(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAgentScopedOutboundMediaAccess(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
mediaSources?: readonly string[];
|
||||
workspaceDir?: string;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaReadFile?: OutboundMediaReadFile;
|
||||
}): OutboundMediaAccess {
|
||||
export function resolveAgentScopedOutboundMediaAccess(
|
||||
params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
mediaSources?: readonly string[];
|
||||
workspaceDir?: string;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaReadFile?: OutboundMediaReadFile;
|
||||
} & OutboundHostMediaPolicyContext,
|
||||
): OutboundMediaAccess {
|
||||
const localRoots =
|
||||
params.mediaAccess?.localRoots ??
|
||||
getAgentScopedMediaLocalRootsForSources({
|
||||
@@ -56,6 +105,16 @@ export function resolveAgentScopedOutboundMediaAccess(params: {
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
workspaceDir: resolvedWorkspaceDir,
|
||||
sessionKey: params.sessionKey,
|
||||
messageProvider: params.messageProvider,
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
accountId: params.accountId,
|
||||
requesterSenderId: params.requesterSenderId,
|
||||
requesterSenderName: params.requesterSenderName,
|
||||
requesterSenderUsername: params.requesterSenderUsername,
|
||||
requesterSenderE164: params.requesterSenderE164,
|
||||
});
|
||||
return {
|
||||
...(localRoots?.length ? { localRoots } : {}),
|
||||
|
||||
Reference in New Issue
Block a user