fix(channels): route direct DM replies to sender (#124837)

Default direct-message reply context to the authenticated sender address so targetless Reef and Nostr replies do not resolve to the local recipient.
This commit is contained in:
Peter Steinberger
2026-08-16 14:24:30 -07:00
committed by GitHub
parent 4aafed1f4a
commit 313fcf1dba
3 changed files with 35 additions and 23 deletions
+6 -3
View File
@@ -199,8 +199,11 @@ describe("dispatchInboundDirectDm", () => {
onDispatchError: vi.fn(),
});
expect(vi.mocked(buildChannelInboundEventContext).mock.calls.at(-1)?.[0].channelIngress).toBe(
"unsupported",
);
const contextParams = vi.mocked(buildChannelInboundEventContext).mock.calls.at(-1)?.[0];
expect(contextParams?.channelIngress).toBe("unsupported");
expect(contextParams?.reply).toEqual({
to: "reef:bot-1",
originatingTo: "reef:peer-1",
});
});
});
+2 -2
View File
@@ -101,7 +101,7 @@ async function buildDirectDmContext(
},
reply: {
to: params.recipientAddress,
originatingTo: params.originatingTo ?? params.recipientAddress,
originatingTo: params.originatingTo ?? params.senderAddress,
},
message: {
body,
@@ -232,7 +232,7 @@ export async function dispatchInboundDirectDmWithRuntime(
CommandAuthorized: params.commandAuthorized,
...(params.inboundAccessAuthorized === true ? { InboundAccessAuthorized: true } : {}),
OriginatingChannel: params.originatingChannel ?? params.channel,
OriginatingTo: params.originatingTo ?? params.recipientAddress,
OriginatingTo: params.originatingTo ?? params.senderAddress,
NativeDirectUserId: params.peer.id,
...params.extraContext,
});
+27 -18
View File
@@ -2,7 +2,9 @@
* Tests direct-message guard policy helpers exposed through the SDK.
*/
import { describe, expect, it, vi } from "vitest";
import { resolveOriginMessageTo } from "../auto-reply/reply/origin-routing.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveImplicitMessageActionTarget } from "../infra/outbound/message-action-normalization.js";
import {
createDirectDmPreCryptoGuardPolicy,
createPreCryptoDirectDmAuthorizer,
@@ -242,16 +244,16 @@ describe("channel-inbound direct-message helpers", () => {
});
});
it("dispatches direct DMs through the standard route/session/reply pipeline", async () => {
it("routes a targetless contextual reply to the inbound Reef peer", async () => {
const { recordInboundSession, dispatchReplyWithBufferedBlockDispatcher, runtime } =
createDirectDmRuntime();
const deliver = vi.fn(async () => {});
const channelIngress = await resolveStableChannelMessageIngress({
channelId: "nostr",
channelId: "reef",
accountId: "default",
subject: { stableId: "sender-1" },
conversation: { kind: "direct", id: "sender-1" },
dmPolicy: "open",
subject: { stableId: "clawstudio" },
conversation: { kind: "direct", id: "clawstudio" },
dmPolicy: "allowlist",
});
const result = await dispatchInboundDirectDmWithRuntime({
@@ -260,14 +262,14 @@ describe("channel-inbound direct-message helpers", () => {
session: { store: { type: "jsonl" } },
} as never,
runtime,
channel: "nostr",
channelLabel: "Nostr",
channel: "reef",
channelLabel: "Reef",
accountId: "default",
peer: { kind: "direct", id: "sender-1" },
senderId: "sender-1",
senderAddress: "nostr:sender-1",
recipientAddress: "nostr:bot-1",
conversationLabel: "sender-1",
peer: { kind: "direct", id: "clawstudio" },
senderId: "clawstudio",
senderAddress: "reef:clawstudio",
recipientAddress: "reef:roboclaw",
conversationLabel: "@clawstudio's agent",
rawBody: "hello world",
messageId: "event-123",
extraContext: {
@@ -284,19 +286,26 @@ describe("channel-inbound direct-message helpers", () => {
expect(result.route.agentId).toBe("agent-main");
expect(result.route.accountId).toBe("default");
expect(result.route.sessionKey).toBe("dm:sender-1");
expect(result.route.sessionKey).toBe("dm:clawstudio");
expect(result.storePath).toBe("/tmp/direct-dm-session-store");
expect(result.ctxPayload.Body).toBe("env:hello world");
expect(result.ctxPayload.BodyForAgent).toBe("hello world");
expect(result.ctxPayload.From).toBe("nostr:sender-1");
expect(result.ctxPayload.To).toBe("nostr:bot-1");
expect(result.ctxPayload.SenderId).toBe("sender-1");
expect(result.ctxPayload.From).toBe("reef:clawstudio");
expect(result.ctxPayload.To).toBe("reef:roboclaw");
expect(result.ctxPayload.SenderId).toBe("clawstudio");
expect(result.ctxPayload.MessageSid).toBe("event-123");
expect(result.ctxPayload.ReplyToId).toBe("event-parent");
expect(result.ctxPayload.MessageThreadId).toBe("thread-7");
expect(result.ctxPayload.NativeDirectUserId).toBe("sender-1");
expect(result.ctxPayload.OriginatingTo).toBe("nostr:bot-1");
expect(result.ctxPayload.NativeDirectUserId).toBe("clawstudio");
expect(result.ctxPayload.OriginatingTo).toBe("reef:clawstudio");
expect(result.ctxPayload.CommandAuthorized).toBe(true);
const currentChannelId = resolveOriginMessageTo({
originatingTo: result.ctxPayload.OriginatingTo,
to: result.ctxPayload.To,
});
expect(
resolveImplicitMessageActionTarget({ currentChannelId, currentChannelProvider: "reef" }),
).toBe("reef:clawstudio");
expect(recordInboundSession).toHaveBeenCalledTimes(1);
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
expect(deliver).toHaveBeenCalledWith({ text: "reply text" });