mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:10:43 +00:00
fix(channels): pin dm main route owners
This commit is contained in:
@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
|
||||
- LINE/messages: send quick-reply-only payloads with fallback option text instead of accepting the payload and returning an empty delivery. Thanks @vincentkoc.
|
||||
- Auto-reply/docking: require `/dock-*` route switches to start from direct chats, so group or channel participants cannot reroute a shared session's future replies into a linked DM. Thanks @vincentkoc.
|
||||
- Discord: keep text-DM main-session route updates pinned to the configured DM owner, matching component interactions so another direct-message sender cannot redirect future main-session replies. Thanks @vincentkoc.
|
||||
- Mattermost/Matrix: keep direct-message main-session route updates pinned to the configured DM owner so paired or temporarily allowed senders cannot redirect future shared-session replies. Thanks @vincentkoc.
|
||||
- Gateway/agent: reject strict `openclaw agent --deliver` requests with missing delivery targets before starting the agent run, so users do not wait for a completed turn that cannot send anywhere. Thanks @vincentkoc.
|
||||
- Setup/import: honor non-interactive `--import-from` onboarding flags by running the migration import path instead of silently completing normal setup without importing anything. Thanks @vincentkoc.
|
||||
- Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram.
|
||||
|
||||
@@ -190,6 +190,44 @@ describe("matrix monitor handler pairing account scope", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("pins direct-message main route updates to the configured owner", async () => {
|
||||
const { handler, recordInboundSession } = createMatrixHandlerTestHarness({
|
||||
cfg: {
|
||||
channels: {
|
||||
matrix: {
|
||||
dm: { allowFrom: ["@owner:example.org"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["@owner:example.org"],
|
||||
allowFromResolvedEntries: [{ input: "@owner:example.org", id: "@owner:example.org" }],
|
||||
isDirectMessage: true,
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!dm:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: "$owner-dm",
|
||||
sender: "@owner:example.org",
|
||||
body: "hello",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(recordInboundSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
updateLastRoute: expect.objectContaining({
|
||||
channel: "matrix",
|
||||
to: "room:!dm:example.org",
|
||||
mainDmOwnerPin: expect.objectContaining({
|
||||
ownerRecipient: "@owner:example.org",
|
||||
senderRecipient: "@owner:example.org",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends pairing reminders for pending requests with cooldown", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-03-01T10:00:00.000Z"));
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/context-visibility-runtime";
|
||||
import { hasFinalInboundReplyDispatch } from "openclaw/plugin-sdk/inbound-reply-dispatch";
|
||||
import type { GetReplyOptions } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveSessionStoreEntry,
|
||||
@@ -38,7 +39,7 @@ import { MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY } from "../send/types.js";
|
||||
import { resolveMatrixStoredSessionMeta } from "../session-store-metadata.js";
|
||||
import { resolveMatrixMonitorAccessState } from "./access-state.js";
|
||||
import { resolveMatrixAckReactionConfig } from "./ack-config.js";
|
||||
import { resolveMatrixAllowListMatch } from "./allowlist.js";
|
||||
import { normalizeMatrixUserId, resolveMatrixAllowListMatch } from "./allowlist.js";
|
||||
import {
|
||||
resolveMatrixMonitorLiveUserAllowlist,
|
||||
type MatrixResolvedAllowlistEntry,
|
||||
@@ -1828,6 +1829,13 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
onReplyStart: typingCallbacks.onReplyStart,
|
||||
onIdle: typingCallbacks.onIdle,
|
||||
});
|
||||
const pinnedMainDmOwner = isDirectMessage
|
||||
? resolvePinnedMainDmOwnerFromAllowlist({
|
||||
dmScope: cfg.session?.dmScope,
|
||||
allowFrom: liveDmAllowFrom,
|
||||
normalizeEntry: normalizeMatrixUserId,
|
||||
})
|
||||
: null;
|
||||
|
||||
const turnResult = await core.channel.turn.run({
|
||||
channel: "matrix",
|
||||
@@ -1855,6 +1863,23 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
channel: "matrix",
|
||||
to: `room:${roomId}`,
|
||||
accountId: _route.accountId,
|
||||
mainDmOwnerPin: pinnedMainDmOwner
|
||||
? {
|
||||
ownerRecipient: pinnedMainDmOwner,
|
||||
senderRecipient: normalizeMatrixUserId(senderId),
|
||||
onSkip: ({
|
||||
ownerRecipient,
|
||||
senderRecipient,
|
||||
}: {
|
||||
ownerRecipient: string;
|
||||
senderRecipient: string;
|
||||
}) => {
|
||||
logVerboseMessage(
|
||||
`matrix: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
onRecordError: (err) => {
|
||||
|
||||
@@ -404,4 +404,79 @@ describe("mattermost inbound user posts", () => {
|
||||
Provider: "mattermost",
|
||||
});
|
||||
});
|
||||
|
||||
it("pins direct-message main route updates to the configured owner", async () => {
|
||||
const socket = new FakeWebSocket();
|
||||
const abortController = new AbortController();
|
||||
mockState.abortController = abortController;
|
||||
const directConfig: OpenClawConfig = {
|
||||
channels: {
|
||||
mattermost: {
|
||||
enabled: true,
|
||||
baseUrl: "https://mattermost.example.com",
|
||||
botToken: "bot-token",
|
||||
chatmode: "onmessage",
|
||||
dmPolicy: "allowlist",
|
||||
groupPolicy: "open",
|
||||
allowFrom: ["user-1"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const runtimeCore = createRuntimeCore(directConfig);
|
||||
mockState.runtimeCore = runtimeCore;
|
||||
mockState.resolveChannelInfo.mockResolvedValue({
|
||||
id: "dm-1",
|
||||
name: "",
|
||||
display_name: "",
|
||||
team_id: "team-1",
|
||||
type: "D",
|
||||
});
|
||||
const { monitorMattermostProvider } = await import("./monitor.js");
|
||||
|
||||
const monitor = monitorMattermostProvider({
|
||||
config: directConfig,
|
||||
runtime: testRuntime(),
|
||||
abortSignal: abortController.signal,
|
||||
webSocketFactory: () => socket,
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(socket.openListenerCount).toBeGreaterThan(0);
|
||||
});
|
||||
socket.emitOpen();
|
||||
|
||||
await socket.emitMessage({
|
||||
event: "posted",
|
||||
data: {
|
||||
channel_id: "dm-1",
|
||||
sender_name: "alice",
|
||||
post: JSON.stringify({
|
||||
id: "post-dm-1",
|
||||
channel_id: "dm-1",
|
||||
user_id: "user-1",
|
||||
message: "direct hello",
|
||||
create_at: 1_714_000_000_000,
|
||||
}),
|
||||
},
|
||||
broadcast: {
|
||||
channel_id: "dm-1",
|
||||
user_id: "user-1",
|
||||
},
|
||||
});
|
||||
socket.emitClose(1000);
|
||||
await monitor;
|
||||
|
||||
expect(runtimeCore.channel.session.recordInboundSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
updateLastRoute: expect.objectContaining({
|
||||
channel: "mattermost",
|
||||
to: "user:user-1",
|
||||
mainDmOwnerPin: expect.objectContaining({
|
||||
ownerRecipient: "user-1",
|
||||
senderRecipient: "user-1",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { deliverFinalizableDraftPreview } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import { createClaimableDedupe, type ClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
|
||||
import { isReasoningReplyPayload } from "openclaw/plugin-sdk/reply-payload";
|
||||
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
import {
|
||||
authorizeMattermostCommandInvocation,
|
||||
isMattermostSenderAllowed,
|
||||
normalizeMattermostAllowEntry,
|
||||
normalizeMattermostAllowList,
|
||||
} from "./monitor-auth.js";
|
||||
import {
|
||||
@@ -1568,6 +1570,14 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
OriginatingTo: to,
|
||||
...mediaPayload,
|
||||
});
|
||||
const pinnedMainDmOwner =
|
||||
kind === "direct"
|
||||
? resolvePinnedMainDmOwnerFromAllowlist({
|
||||
dmScope: cfg.session?.dmScope,
|
||||
allowFrom: account.config.allowFrom,
|
||||
normalizeEntry: normalizeMattermostAllowEntry,
|
||||
})
|
||||
: null;
|
||||
|
||||
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
||||
agentId: route.agentId,
|
||||
@@ -1748,6 +1758,23 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
channel: "mattermost",
|
||||
to,
|
||||
accountId: route.accountId,
|
||||
mainDmOwnerPin: pinnedMainDmOwner
|
||||
? {
|
||||
ownerRecipient: pinnedMainDmOwner,
|
||||
senderRecipient: normalizeMattermostAllowEntry(senderId),
|
||||
onSkip: ({
|
||||
ownerRecipient,
|
||||
senderRecipient,
|
||||
}: {
|
||||
ownerRecipient: string;
|
||||
senderRecipient: string;
|
||||
}) => {
|
||||
logVerboseMessage(
|
||||
`mattermost: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
onRecordError: (err) => {
|
||||
|
||||
Reference in New Issue
Block a user