Skip to content

Commit 654f63e

Browse files
kevinWangShengKevin Wangsteipete
authored
fix(signal): prevent sentTranscript sync messages from bypassing loop protection (openclaw#31093)
* fix(signal): prevent sentTranscript sync messages from bypassing loop protection Issue: openclaw#31084 On daemon restart, sentTranscript sync messages could bypass loop protection because the syncMessage check happened before the sender validation. This reorganizes the checks to: 1. First resolve the sender (phone or UUID) 2. Check if the message is from our own account (both phone and UUID) 3. Only skip sync messages from other sources after confirming not own account This ensures that sync messages from the own account are properly filtered to prevent self-reply loops, while still allowing messages synced from other devices to be processed. Added optional accountUuid config field for UUID-based account identification. * fix(signal): cover UUID-only own-message loop protection * build: regenerate host env security policy swift --------- Co-authored-by: Kevin Wang <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent b9aa2d4 commit 654f63e

File tree

5 files changed

+50
-7
lines changed

5 files changed

+50
-7
lines changed

src/config/types.signal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export type SignalReactionLevel = "off" | "ack" | "minimal" | "extensive";
66
export type SignalAccountConfig = CommonChannelMessagingConfig & {
77
/** Optional explicit E.164 account for signal-cli. */
88
account?: string;
9+
/** Optional account UUID for signal-cli (used for loop protection). */
10+
accountUuid?: string;
911
/** Optional full base URL for signal-cli HTTP daemon. */
1012
httpUrl?: string;
1113
/** HTTP host for signal-cli daemon (default 127.0.0.1). */

src/signal/monitor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
423423
cfg,
424424
baseUrl,
425425
account,
426+
accountUuid: accountInfo.config.accountUuid,
426427
accountId: accountInfo.accountId,
427428
blockStreaming: accountInfo.config.blockStreaming,
428429
historyLimit,

src/signal/monitor/event-handler.inbound-contract.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,33 @@ describe("signal createSignalEventHandler inbound contract", () => {
172172
expect(capture.ctx).toBeTruthy();
173173
expect(capture.ctx?.CommandAuthorized).toBe(false);
174174
});
175+
176+
it("drops own UUID inbound messages when only accountUuid is configured", async () => {
177+
const ownUuid = "123e4567-e89b-12d3-a456-426614174000";
178+
const handler = createSignalEventHandler(
179+
createBaseSignalEventHandlerDeps({
180+
cfg: {
181+
messages: { inbound: { debounceMs: 0 } },
182+
channels: { signal: { dmPolicy: "open", allowFrom: ["*"], accountUuid: ownUuid } },
183+
},
184+
account: undefined,
185+
accountUuid: ownUuid,
186+
historyLimit: 0,
187+
}),
188+
);
189+
190+
await handler(
191+
createSignalReceiveEvent({
192+
sourceNumber: null,
193+
sourceUuid: ownUuid,
194+
dataMessage: {
195+
message: "self message",
196+
attachments: [],
197+
},
198+
}),
199+
);
200+
201+
expect(capture.ctx).toBeUndefined();
202+
expect(dispatchInboundMessageMock).not.toHaveBeenCalled();
203+
});
175204
});

src/signal/monitor/event-handler.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -420,18 +420,28 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
420420
if (!envelope) {
421421
return;
422422
}
423-
if (envelope.syncMessage) {
424-
return;
425-
}
426423

424+
// Check for syncMessage (e.g., sentTranscript from other devices)
425+
// We need to check if it's from our own account to prevent self-reply loops
427426
const sender = resolveSignalSender(envelope);
428427
if (!sender) {
429428
return;
430429
}
431-
if (deps.account && sender.kind === "phone") {
432-
if (sender.e164 === normalizeE164(deps.account)) {
433-
return;
434-
}
430+
431+
// Check if the message is from our own account to prevent loop/self-reply
432+
// This handles both phone number and UUID based identification
433+
const normalizedAccount = deps.account ? normalizeE164(deps.account) : undefined;
434+
const isOwnMessage =
435+
(sender.kind === "phone" && normalizedAccount != null && sender.e164 === normalizedAccount) ||
436+
(sender.kind === "uuid" && deps.accountUuid != null && sender.raw === deps.accountUuid);
437+
if (isOwnMessage) {
438+
return;
439+
}
440+
441+
// For non-own sync messages (e.g., messages synced from other devices),
442+
// we could process them but for now we skip to be conservative
443+
if (envelope.syncMessage) {
444+
return;
435445
}
436446

437447
const dataMessage = envelope.dataMessage ?? envelope.editMessage?.dataMessage;

src/signal/monitor/event-handler.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type SignalEventHandlerDeps = {
7272
cfg: OpenClawConfig;
7373
baseUrl: string;
7474
account?: string;
75+
accountUuid?: string;
7576
accountId: string;
7677
blockStreaming?: boolean;
7778
historyLimit: number;

0 commit comments

Comments
 (0)