From d68e939412cbbb4e5f4e4245eb702ee454959c6c Mon Sep 17 00:00:00 2001 From: Jesse Merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:46:00 +1000 Subject: [PATCH] fix(signal): reject sibling account collisions --- extensions/signal/src/setup-surface.test.ts | 33 +++++++++++++++++++++ extensions/signal/src/setup-surface.ts | 10 +++++++ 2 files changed, 43 insertions(+) diff --git a/extensions/signal/src/setup-surface.test.ts b/extensions/signal/src/setup-surface.test.ts index f83d67bab9f6..233992bd7636 100644 --- a/extensions/signal/src/setup-surface.test.ts +++ b/extensions/signal/src/setup-surface.test.ts @@ -447,6 +447,39 @@ describe("signalSetupWizard QR linking", () => { expect(result.cfg.channels?.signal?.accounts?.work?.account).toBe("+15555550444"); }); + it("does not assign a QR-linked identity already owned by a sibling account", async () => { + const transport = { + kind: "managed-native" as const, + cliPath: "/opt/signal-cli", + configPath: "~/.local/share/signal-cli", + }; + const cfg = { + channels: { + signal: { + defaultAccount: "default", + accounts: { + default: { account: "+15555550123", transport }, + work: { transport }, + }, + }, + }, + }; + const note = vi.fn(async () => undefined); + + const result = await configure({ + cfg, + accountId: "work", + prompter: { ...createQrPrompter(), note }, + }); + + expect(result.cfg.channels?.signal?.accounts?.default?.account).toBe("+15555550123"); + expect(result.cfg.channels?.signal?.accounts?.work?.account).toBeUndefined(); + expect(note).toHaveBeenCalledWith( + "+15555550123 is already assigned to another OpenClaw Signal account. The selected account was not changed.", + "Signal account linking", + ); + }); + it("finishes setup when signal-cli completes before the QR is acknowledged", async () => { const qrCode = vi.fn(async (params: Parameters>[0]) => { await params.dismissed; diff --git a/extensions/signal/src/setup-surface.ts b/extensions/signal/src/setup-surface.ts index ba724ea06913..ceddb60f80ad 100644 --- a/extensions/signal/src/setup-surface.ts +++ b/extensions/signal/src/setup-surface.ts @@ -211,6 +211,16 @@ export const signalSetupWizard: ChannelSetupWizard = { // Preserve that authoritative result even if cancellation raced its final output. preparedCredentialValues[SIGNAL_LINK_COMPLETED_INPUT_KEY] = "true"; if (linkResult.associatedAccount) { + if (siblingAccounts.has(linkResult.associatedAccount)) { + // The device link succeeded, but this setup target must not claim a sibling's identity. + // Mark it handled so the later number prompt cannot recreate the same collision. + preparedCredentialValues[SIGNAL_LINKED_ACCOUNT_INPUT_KEY] = "true"; + await prompter.note( + `${linkResult.associatedAccount} is already assigned to another OpenClaw Signal account. The selected account was not changed.`, + "Signal account linking", + ); + return { credentialValues: preparedCredentialValues }; + } if (configuredAccount && linkResult.associatedAccount !== configuredAccount) { const replaceConfiguredAccount = await prompter.confirm({ message: `Use ${linkResult.associatedAccount} instead of configured Signal account ${configuredAccount}?`,