feat(whatsapp): WhatsApp Business Cloud API adapter - #43921
Conversation
Add an official, production-grade WhatsApp integration via Meta's Business Cloud API as a complement to the existing Baileys bridge. No bridge subprocess, no QR codes, no account-ban risk — at the cost of a Meta Business account and a public HTTPS webhook URL. Setup is fully wizard-driven: 'hermes whatsapp-cloud' walks through every credential with paste-time validation (catches the #1 trap of pasting a phone number into the Phone Number ID field), generates a verify token, and ends with copy-paste instructions for the cloudflared / Meta-dashboard / Business Manager pieces that can't be automated. The wizard also points users at Meta's Business Manager for setting the bot's display name and profile picture. Feature set: - Inbound: text, images (with native-vision routing), voice notes (STT), documents (small text inlined, larger cached), reply context. - Outbound: text with WhatsApp-flavored markdown conversion, images, videos, documents, opus voice notes via ffmpeg with MP3 fallback. - Native interactive buttons for clarify, dangerous-command approval, and slash-command confirmation flows — matches the Telegram / Discord UX, graceful degrades to plain text. - Read receipts (blue double-checkmarks) and typing indicator, using Meta's combined endpoint so they fire in a single API call. - Webhook security: X-Hub-Signature-256 HMAC verification (raw body, constant-time), wamid deduplication, group-shaped-message refusal (groups deferred to v2 — Baileys still covers them). - Full integration with the gateway's session, cron, display-tier, prompt-hint, and auth-allowlist systems. Cloud and Baileys can run side-by-side against different phone numbers. Also wires STT (speech-to-text) through Nous's managed audio gateway for Nous subscribers — previously the default stt.provider=local required a separate faster-whisper install. New subscribers now get voice-note transcription out of the box. Docs: 418-line user guide at website/docs/user-guide/messaging/ whatsapp-cloud.md, sidebar entry, environment-variables reference, ADDING_A_PLATFORM.md updated with the optional interactive-UX contract for future adapter authors. Tests: 100 dedicated tests for the adapter, 32 for the setup wizard, 20 for the Nous subscription STT wiring, plus regression coverage across display_config, prompt_builder, and the cron scheduler. Known limitations (deferred until clear demand signal): - Group chats — use the Baileys bridge if you need them. - Message templates for 24-hour-window outside-conversation sends — reactive chat is unaffected; cron / delegate_task with gaps > 24h will fail with a clear error. The agent's system prompt warns the model about this so it knows to mention it when scheduling delayed messages.
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-argument-type |
23 |
unresolved-attribute |
23 |
unsupported-operator |
7 |
invalid-method-override |
5 |
unresolved-import |
4 |
invalid-return-type |
2 |
invalid-assignment |
1 |
First entries
gateway/run.py:13684: [invalid-argument-type] invalid-argument-type: Argument to function `resolve_display_setting` is incorrect: Expected `str`, found `Literal["cli", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/slash_commands.py:633: [unresolved-attribute] unresolved-attribute: Attribute `lower` is not defined on `set[Unknown]` in union `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/platforms/whatsapp_common.py:103: [unresolved-attribute] unresolved-attribute: Object of type `Self@_whatsapp_free_response_chats` has no attribute `config`
tests/hermes_cli/test_whatsapp_cloud_setup.py:87: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["OpenAI"]` and `str | None`
gateway/slash_commands.py:1692: [invalid-argument-type] invalid-argument-type: Argument to function `_home_thread_env_var` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/slash_commands.py:3425: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.get` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/platforms/whatsapp_common.py:90: [unresolved-attribute] unresolved-attribute: Object of type `Self@_whatsapp_require_mention` has no attribute `config`
gateway/platforms/whatsapp_cloud.py:1076: [invalid-method-override] invalid-method-override: Invalid override of method `send_document`: Definition is incompatible with `BasePlatformAdapter.send_document`
tests/hermes_cli/test_whatsapp_cloud_setup.py:49: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["Phone number ID"]` and `str | None`
gateway/authz_mixin.py:198: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.get` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/run.py:13830: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `str`, found `Literal["cli", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/run.py:6360: [invalid-argument-type] invalid-argument-type: Argument to bound method `PairingStore._record_rate_limit` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
tests/hermes_cli/test_whatsapp_cloud_setup.py:102: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["EAA"]` and `str | None`
gateway/platforms/whatsapp_common.py:252: [unresolved-attribute] unresolved-attribute: Object of type `Self@_message_matches_mention_patterns` has no attribute `_mention_patterns`
gateway/platforms/whatsapp_common.py:164: [unresolved-attribute] unresolved-attribute: Object of type `Self@_is_group_allowed` has no attribute `_group_allow_from`
tests/hermes_cli/test_whatsapp_cloud_setup.py:22: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/run.py:11363: [invalid-argument-type] invalid-argument-type: Argument to function `set_session_vars` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/channel_directory.py:87: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]` and value of type `list[dict[str, str]]` on object of type `dict[str, list[dict[str, str]]]`
gateway/session.py:269: [unresolved-attribute] unresolved-attribute: Attribute `title` is not defined on `set[Unknown]` in union `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/platforms/whatsapp_cloud.py:317: [unresolved-attribute] unresolved-attribute: Attribute `AsyncClient` is not defined on `None` in union `Unknown | None`
tests/hermes_cli/test_whatsapp_cloud_setup.py:122: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["32"]` and `str | None`
gateway/channel_directory.py:87: [invalid-argument-type] invalid-argument-type: Argument to function `_build_from_sessions` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/run.py:6339: [invalid-argument-type] invalid-argument-type: Argument to bound method `PairingStore.generate_code` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/run.py:6030: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.create_adapter` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/run.py:7332: [invalid-argument-type] invalid-argument-type: Argument to function `get_disabled_skill_names` is incorrect: Expected `str | None`, found `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | (set[Unknown] & ~AlwaysFalsy)`
... and 40 more
✅ Fixed issues (29):
| Rule | Count |
|---|---|
invalid-argument-type |
22 |
unresolved-attribute |
4 |
invalid-return-type |
2 |
invalid-assignment |
1 |
First entries
tests/gateway/test_whatsapp_group_gating.py:372: [invalid-argument-type] invalid-argument-type: Argument to function `WhatsAppAdapter._is_broadcast_chat` is incorrect: Expected `str`, found `None`
gateway/session.py:257: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.get` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/session.py:269: [unresolved-attribute] unresolved-attribute: Attribute `title` is not defined on `set[Unknown]` in union `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:11349: [invalid-argument-type] invalid-argument-type: Argument to function `set_session_vars` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/platforms/base.py:2179: [unresolved-attribute] unresolved-attribute: Attribute `title` is not defined on `set[Unknown]` in union `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:6325: [invalid-argument-type] invalid-argument-type: Argument to bound method `PairingStore.generate_code` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/slash_commands.py:1692: [invalid-argument-type] invalid-argument-type: Argument to function `_home_thread_env_var` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:11681: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.is_registered` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:11322: [invalid-argument-type] invalid-argument-type: Argument to bound method `set.add` is incorrect: Expected `tuple[str, str, str | None]`, found `tuple[Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown], str, str | None]`
hermes_cli/web_server.py:3564: [invalid-argument-type] invalid-argument-type: Argument to bound method `set.add` is incorrect: Expected `str`, found `Literal["telegram", "discord", "whatsapp", "slack", "signal", ... omitted 16 literals] | set[Unknown]`
gateway/run.py:7318: [invalid-argument-type] invalid-argument-type: Argument to function `get_disabled_skill_names` is incorrect: Expected `str | None`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | (set[Unknown] & ~AlwaysFalsy)`
gateway/delivery.py:172: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `str`, found `Literal["telegram", "discord", "whatsapp", "slack", "signal", ... omitted 16 literals] | set[Unknown]`
gateway/run.py:5590: [invalid-argument-type] invalid-argument-type: Argument to bound method `GatewayRunner._update_platform_runtime_status` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/channel_directory.py:87: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]` and value of type `list[dict[str, str]]` on object of type `dict[str, list[dict[str, str]]]`
gateway/run.py:13816: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `str`, found `Literal["cli", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
hermes_cli/web_server.py:3565: [invalid-argument-type] invalid-argument-type: Argument to function `_build_catalog_entry` is incorrect: Expected `str`, found `Literal["telegram", "discord", "whatsapp", "slack", "signal", ... omitted 16 literals] | set[Unknown]`
gateway/slash_commands.py:3425: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.get` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
hermes_cli/commands.py:1656: [unresolved-attribute] unresolved-attribute: Attribute `startswith` is not defined on `set[Unknown]` in union `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
tests/gateway/test_title_command.py:174: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.create_session` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/authz_mixin.py:196: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.get` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/slash_commands.py:1691: [invalid-argument-type] invalid-argument-type: Argument to function `_home_target_env_var` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/channel_directory.py:87: [invalid-argument-type] invalid-argument-type: Argument to function `_build_from_sessions` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:1579: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `str`, found `Literal["cli", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:6028: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.create_adapter` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:6322: [invalid-argument-type] invalid-argument-type: Argument to bound method `PairingStore._is_rate_limited` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
... and 4 more
Unchanged: 5533 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Comment (high-surface-area, full review deferred)
Feature: WhatsApp Business Cloud API adapter
This is a very large PR (6,383 additions / 292 deletions, 7,258 diff lines) — a major new platform adapter. In cron batch mode I cannot do a thorough deep review within available resources.
Surface area concerns (require human review):
- WhatsApp Business Cloud API authentication and token handling
- Message format translation between Baileys (legacy) and Business Cloud API
- Group message handling and participant ID mapping
- Webhook security and signature verification
- Error handling and reconnection logic
This is a significant architectural addition. Please treat this as an informational comment — the PR warrants a focused human review and potentially a dedicated testing session before merge.
Reviewed by Hermes Agent (batch mode — high-surface-area, full review deferred)
|
Merged via PR #44331 — your commits were merged forward onto current main with your authorship preserved in git history. On top of your work we added a small follow-up commit addressing review findings: STT managed-default flip now gated on openai-audio entitlement (and skipped when a local faster-whisper/custom backend works), WHATSAPP_CLOUD_* policy env overrides + wa_id allowlist normalization so both adapters can run in parallel with independent policies, per-message webhook try/except (avoids silent message loss on Meta batch retries), media id validation, opus temp-file cleanup, bounded interactive-state caches, and getpass for wizard secrets. Excellent adapter — the webhook security (raw-body HMAC, constant-time compares, refuse-when-unconfigured) and the mixin extraction were both exactly right. Thanks! |
- nous_subscription: gate the STT managed-default flip on openai-audio
entitlement and skip when a local backend (faster-whisper or custom
command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
delete transient .ogg after voice upload, FIFO-cap interactive-button
state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
- nous_subscription: gate the STT managed-default flip on openai-audio
entitlement and skip when a local backend (faster-whisper or custom
command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
delete transient .ogg after voice upload, FIFO-cap interactive-button
state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
- nous_subscription: gate the STT managed-default flip on openai-audio
entitlement and skip when a local backend (faster-whisper or custom
command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
delete transient .ogg after voice upload, FIFO-cap interactive-button
state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
- nous_subscription: gate the STT managed-default flip on openai-audio
entitlement and skip when a local backend (faster-whisper or custom
command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
delete transient .ogg after voice upload, FIFO-cap interactive-button
state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
- nous_subscription: gate the STT managed-default flip on openai-audio
entitlement and skip when a local backend (faster-whisper or custom
command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
delete transient .ogg after voice upload, FIFO-cap interactive-button
state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
- nous_subscription: gate the STT managed-default flip on openai-audio
entitlement and skip when a local backend (faster-whisper or custom
command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
delete transient .ogg after voice upload, FIFO-cap interactive-button
state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
- nous_subscription: gate the STT managed-default flip on openai-audio
entitlement and skip when a local backend (faster-whisper or custom
command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
delete transient .ogg after voice upload, FIFO-cap interactive-button
state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
- nous_subscription: gate the STT managed-default flip on openai-audio
entitlement and skip when a local backend (faster-whisper or custom
command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
delete transient .ogg after voice upload, FIFO-cap interactive-button
state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
- nous_subscription: gate the STT managed-default flip on openai-audio
entitlement and skip when a local backend (faster-whisper or custom
command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
delete transient .ogg after voice upload, FIFO-cap interactive-button
state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
Adds an official, production-grade WhatsApp integration via Meta's Business Cloud API, complementing the existing Baileys bridge. No bridge subprocess, no QR codes, no account-ban risk — at the cost of a Meta Business account and a public HTTPS webhook URL.
Why
The Baileys bridge is great for personal accounts but emulates WhatsApp Web, which carries ban risk and isn't viable for business bots. This adapter is the Meta-supported path. The two adapters can run in parallel against different phone numbers.
What's included
Adapter (
gateway/platforms/whatsapp_cloud.py)Shared behavior extraction (
gateway/platforms/whatsapp_common.py)WhatsAppBehaviorMixincarrying gating (dm/group policy), mention parsing, markdown conversion, and chunking shared by both adapters — Baileys adapter slimmed by ~280 lines, no behavior changeSetup wizard (
hermes whatsapp-cloud)Integration
Platform.WHATSAPP_CLOUDwired into gateway adapter creation, authz allowlists (WHATSAPP_CLOUD_ALLOWED_USERS/_ALLOW_ALL_USERS), and cron delivery (WHATSAPP_CLOUD_HOME_CHANNEL)openai-audiogateway, sharing the TTS entitlement categorymessaging/whatsapp-cloud.md), cross-links from the Baileys page, sidebar entryMerge with main
Branch was rebased-by-merge against current main (2,100+ commits). Notable integration points:
supports_code_blocks,enforces_own_access_policy, text-debounce batching) hoisted into / kept alongsideWhatsAppBehaviorMixinso both adapters inherit them_entitled_for,MANAGED_FEATURE_COVERAGE_CATEGORY)get_nous_auth_statusmock API toget_nous_portal_account_infoTesting
tests/gateway/test_whatsapp_cloud.py(2,250 lines) +tests/hermes_cli/test_whatsapp_cloud_setup.py(406 lines): 132/132 passingtest_nous_subscription.py+test_scheduler.py: 169/169 passingtests/gateway/suite: failure set identical to main baseline (no regressions; +94 new passing tests)hermes whatsapp-cloud --help, parser registration, adapter imports verified