From 54960d1380e35eb0c36a7ebacc5e754c8a7de52e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 7 Jan 2026 20:49:44 +0100 Subject: [PATCH] fix: refine whatsapp personal phone onboarding --- CHANGELOG.md | 2 +- docs/providers/whatsapp.md | 20 ++++++++- src/commands/onboard-providers.ts | 74 ++++++++++++++++++++++++++++--- 3 files changed, 87 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e3483525..82a4557e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ - macOS: prevent gateway launchd startup race where the app could kill a just-started gateway; avoid unnecessary `bootout` and ensure the job is enabled at login. Fixes #306. Thanks @gupsammy for PR #387. - Pairing: generate DM pairing codes with CSPRNG, expire pending codes after 1 hour, and avoid re-sending codes for already pending requests. - Pairing: lock + atomically write pairing stores with 0600 perms and stop logging pairing codes in provider logs. -- WhatsApp: add self-phone mode to suppress pairing replies for outbound DMs and prompt during onboarding. +- WhatsApp: add self-phone mode (no pairing replies for outbound DMs) and onboarding prompt for personal vs separate numbers (auto allowlist + response prefix for personal). - Discord: include all inbound attachments in `MediaPaths`/`MediaUrls` (back-compat `MediaPath`/`MediaUrl` still first). - Sandbox: add `agent.sandbox.workspaceAccess` (`none`/`ro`/`rw`) to control agent workspace visibility inside the container; `ro` hard-disables `write`/`edit`. - Routing: allow per-agent sandbox overrides (including `workspaceAccess` and `sandbox.tools`) plus per-agent tool policies in multi-agent configs. Thanks @pasogott for PR #380. diff --git a/docs/providers/whatsapp.md b/docs/providers/whatsapp.md index dfbe1d0dd..a777af70f 100644 --- a/docs/providers/whatsapp.md +++ b/docs/providers/whatsapp.md @@ -61,7 +61,25 @@ WhatsApp requires a real mobile number for verification. VoIP and virtual number - Pairing: unknown senders get a pairing code (approve via `clawdbot pairing approve --provider whatsapp `; codes expire after 1 hour). - Open: requires `whatsapp.allowFrom` to include `"*"`. - Self messages are always allowed; “self-chat mode” still requires `whatsapp.allowFrom` to include your own number. -- **Same-phone mode**: set `whatsapp.selfChatMode=true` when Clawdbot runs on your personal WhatsApp number. This suppresses pairing replies for outbound DMs. + +### Same-phone mode (personal number) +If you run Clawdbot on your **personal WhatsApp number**, set: + +```json +{ + "whatsapp": { + "selfChatMode": true + } +} +``` + +Behavior: +- Suppresses pairing replies for **outbound DMs** (prevents spamming contacts). +- Inbound unknown senders still follow `whatsapp.dmPolicy`. + +Recommended for personal numbers: +- Set `whatsapp.dmPolicy="allowlist"` and add your number to `whatsapp.allowFrom`. +- Set `messages.responsePrefix` (for example, `[clawdbot]`) so replies are clearly labeled. - **Group policy**: `whatsapp.groupPolicy` controls group handling (`open|disabled|allowlist`). - `allowlist` uses `whatsapp.groupAllowFrom` (fallback: explicit `whatsapp.allowFrom`). - **Self-chat mode**: avoids auto read receipts and ignores mention JIDs. diff --git a/src/commands/onboard-providers.ts b/src/commands/onboard-providers.ts index ae81e683a..d489ff872 100644 --- a/src/commands/onboard-providers.ts +++ b/src/commands/onboard-providers.ts @@ -202,6 +202,19 @@ function setWhatsAppAllowFrom(cfg: ClawdbotConfig, allowFrom?: string[]) { }; } +function setMessagesResponsePrefix( + cfg: ClawdbotConfig, + responsePrefix?: string, +) { + return { + ...cfg, + messages: { + ...cfg.messages, + responsePrefix, + }, + }; +} + function setWhatsAppSelfChatMode( cfg: ClawdbotConfig, selfChatMode?: boolean, @@ -403,6 +416,7 @@ async function promptWhatsAppAllowFrom( const existingAllowFrom = cfg.whatsapp?.allowFrom ?? []; const existingLabel = existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset"; + const existingResponsePrefix = cfg.messages?.responsePrefix; await prompter.note( [ @@ -418,6 +432,56 @@ async function promptWhatsAppAllowFrom( "WhatsApp DM access", ); + const phoneMode = (await prompter.select({ + message: "WhatsApp phone setup", + options: [ + { value: "personal", label: "This is my personal phone number" }, + { value: "separate", label: "Separate phone just for Clawdbot" }, + ], + })) as "personal" | "separate"; + + if (phoneMode === "personal") { + const entry = await prompter.text({ + message: "Your WhatsApp number (E.164)", + placeholder: "+15555550123", + initialValue: existingAllowFrom[0], + validate: (value) => { + const raw = String(value ?? "").trim(); + if (!raw) return "Required"; + const normalized = normalizeE164(raw); + if (!normalized) return `Invalid number: ${raw}`; + return undefined; + }, + }); + const normalized = normalizeE164(String(entry).trim()); + const merged = [ + ...existingAllowFrom + .filter((item) => item !== "*") + .map((item) => normalizeE164(item)) + .filter(Boolean), + normalized, + ]; + const unique = [...new Set(merged.filter(Boolean))]; + let next = setWhatsAppSelfChatMode(cfg, true); + next = setWhatsAppDmPolicy(next, "allowlist"); + next = setWhatsAppAllowFrom(next, unique); + if (existingResponsePrefix === undefined) { + next = setMessagesResponsePrefix(next, "[clawdbot]"); + } + await prompter.note( + [ + "Personal phone mode enabled.", + "- dmPolicy set to allowlist (pairing skipped)", + `- allowFrom includes ${normalized}`, + existingResponsePrefix === undefined + ? "- responsePrefix set to [clawdbot]" + : "- responsePrefix left unchanged", + ].join("\n"), + "WhatsApp personal phone", + ); + return next; + } + const policy = (await prompter.select({ message: "WhatsApp DM policy", options: [ @@ -428,7 +492,8 @@ async function promptWhatsAppAllowFrom( ], })) as DmPolicy; - let next = setWhatsAppDmPolicy(cfg, policy); + let next = setWhatsAppSelfChatMode(cfg, false); + next = setWhatsAppDmPolicy(next, policy); if (policy === "open") { next = setWhatsAppAllowFrom(next, ["*"]); } @@ -490,12 +555,7 @@ async function promptWhatsAppAllowFrom( next = setWhatsAppAllowFrom(next, unique); } - const selfChatMode = await prompter.confirm({ - message: - "Same-phone setup? (using your personal WhatsApp number for Clawdbot)", - initialValue: next.whatsapp?.selfChatMode ?? false, - }); - return setWhatsAppSelfChatMode(next, selfChatMode); + return next; } type SetupProvidersOptions = {