diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index b889a95701e..9d8edeece6f 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -27,7 +27,11 @@ import { import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js"; import type { ReplyPayload } from "../auto-reply/types.js"; import { getChannelPlugin } from "../channels/plugins/index.js"; -import type { ChannelHeartbeatDeps } from "../channels/plugins/types.public.js"; +import type { + ChannelHeartbeatDeps, + ChannelId, + ChannelPlugin, +} from "../channels/plugins/types.public.js"; import { loadConfig } from "../config/config.js"; import { canonicalizeMainSessionAlias, @@ -44,6 +48,7 @@ import type { AgentDefaultsConfig } from "../config/types.agent-defaults.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveCronSession } from "../cron/isolated-agent/session.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { getActivePluginChannelRegistry } from "../plugins/runtime.js"; import { getQueueSize } from "../process/command-queue.js"; import { CommandLane } from "../process/lanes.js"; import { @@ -121,6 +126,13 @@ function loadHeartbeatRunnerRuntime() { return heartbeatRunnerRuntimePromise; } +function resolveHeartbeatChannelPlugin(channel: string): ChannelPlugin | undefined { + const activePlugin = getActivePluginChannelRegistry()?.channels.find( + (entry) => entry.plugin.id === channel, + )?.plugin; + return activePlugin ?? getChannelPlugin(channel as ChannelId); +} + export { areHeartbeatsEnabled, setHeartbeatsEnabled }; export { isHeartbeatEnabledForAgent, @@ -979,7 +991,7 @@ export async function runHeartbeatOnce(opts: { if (!canAttemptHeartbeatOk || delivery.channel === "none" || !delivery.to) { return false; } - const heartbeatPlugin = getChannelPlugin(delivery.channel); + const heartbeatPlugin = resolveHeartbeatChannelPlugin(delivery.channel); if (heartbeatPlugin?.heartbeat?.checkReady) { const readiness = await heartbeatPlugin.heartbeat.checkReady({ cfg, @@ -1167,7 +1179,7 @@ export async function runHeartbeatOnce(opts: { } const deliveryAccountId = delivery.accountId; - const heartbeatPlugin = getChannelPlugin(delivery.channel); + const heartbeatPlugin = resolveHeartbeatChannelPlugin(delivery.channel); if (heartbeatPlugin?.heartbeat?.checkReady) { const readiness = await heartbeatPlugin.heartbeat.checkReady({ cfg, diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index dbf18518591..4348626e356 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -230,7 +230,7 @@ describe("deliverOutboundPayloads", () => { cfg: {}, channel: "whatsapp", to: "+1555", - payloads: [{ text: "hello" }], + payloads: [{ text: "hello", mediaUrl: "file:///tmp/policy.png" }], deps: { whatsapp: sendWhatsApp }, session: { key: "agent:main:whatsapp:group:ops", @@ -259,7 +259,7 @@ describe("deliverOutboundPayloads", () => { cfg: {}, channel: "whatsapp", to: "+1555", - payloads: [{ text: "hello" }], + payloads: [{ text: "hello", mediaUrl: "file:///tmp/policy.png" }], deps: { whatsapp: sendWhatsApp }, session: { key: "agent:main:whatsapp:group:ops", @@ -293,7 +293,7 @@ describe("deliverOutboundPayloads", () => { channel: "whatsapp", to: "+1555", accountId: "destination-account", - payloads: [{ text: "hello" }], + payloads: [{ text: "hello", mediaUrl: "file:///tmp/policy.png" }], deps: { whatsapp: sendWhatsApp }, session: { key: "agent:main:whatsapp:group:ops", @@ -312,6 +312,29 @@ describe("deliverOutboundPayloads", () => { resolveMediaAccessSpy.mockRestore(); }); + it("skips media access policy for text-only delivery", async () => { + const resolveMediaAccessSpy = vi.spyOn( + mediaCapabilityModule, + "resolveAgentScopedOutboundMediaAccess", + ); + const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w4", 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).not.toHaveBeenCalled(); + 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, diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index 1855e738d20..c5066e0ea76 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -557,18 +557,22 @@ async function deliverOutboundPayloadsCore( const accountId = params.accountId; const deps = params.deps; const abortSignal = params.abortSignal; - const mediaAccess = resolveAgentScopedOutboundMediaAccess({ - cfg, - agentId: params.session?.agentId ?? params.mirror?.agentId, - mediaSources: collectPayloadMediaSources(outboundPayloadPlan), - 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 mediaSources = collectPayloadMediaSources(outboundPayloadPlan); + const mediaAccess = + mediaSources.length > 0 + ? resolveAgentScopedOutboundMediaAccess({ + cfg, + agentId: params.session?.agentId ?? params.mirror?.agentId, + mediaSources, + 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({ cfg,