feat(whatsapp): WhatsApp Business Cloud API adapter (salvage #43921) - #44331
Merged
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.
- 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
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
23 |
invalid-argument-type |
23 |
unsupported-operator |
7 |
invalid-method-override |
5 |
unresolved-import |
4 |
invalid-return-type |
2 |
invalid-assignment |
1 |
First entries
gateway/platforms/base.py:2179: [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]`
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/run.py:6439: [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]`
gateway/run.py:6415: [invalid-argument-type] invalid-argument-type: Argument to bound method `PairingStore._is_rate_limited` 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:127: [unresolved-attribute] unresolved-attribute: Attribute `lower` is not defined on `None` in union `str | None`
gateway/platforms/whatsapp_cloud.py:1307: [unresolved-attribute] unresolved-attribute: Attribute `json_response` is not defined on `None` in union `Unknown | None`
gateway/run.py:13924: [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:7411: [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)`
gateway/platforms/whatsapp_common.py:210: [unresolved-attribute] unresolved-attribute: Object of type `Self@_compile_mention_patterns` has no attribute `name`
gateway/platforms/whatsapp_cloud.py:1064: [invalid-method-override] invalid-method-override: Invalid override of method `send_image_file`: Definition is incompatible with `BasePlatformAdapter.send_image_file`
gateway/run.py:11442: [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/platforms/whatsapp_common.py:252: [unresolved-attribute] unresolved-attribute: Object of type `Self@_message_matches_mention_patterns` has no attribute `_mention_patterns`
tests/hermes_cli/test_whatsapp_cloud_setup.py:97: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["GitHub"]` and `str | None`
gateway/platforms/whatsapp_cloud.py:66: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
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", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/platforms/whatsapp_common.py:163: [unresolved-attribute] unresolved-attribute: Object of type `Self@_is_group_allowed` has no attribute `_group_policy`
gateway/run.py:13778: [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]`
hermes_cli/commands.py:1656: [unresolved-attribute] unresolved-attribute: Attribute `startswith` is not defined on `set[Unknown]` in union `Literal["local", "telegram", "discord", "whatsapp", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
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/platforms/whatsapp_cloud.py:1352: [unresolved-attribute] unresolved-attribute: Attribute `Request` is not defined on `None` in union `Unknown | 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", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
gateway/platforms/whatsapp_common.py:155: [unresolved-attribute] unresolved-attribute: Object of type `Self@_is_dm_allowed` has no attribute `_allow_from`
hermes_cli/web_server.py:3710: [invalid-argument-type] invalid-argument-type: Argument to bound method `set.add` is incorrect: Expected `str`, found `Literal["telegram", "discord", "whatsapp", "whatsapp_cloud", "slack", ... omitted 17 literals] | set[Unknown]`
hermes_cli/web_server.py:3711: [invalid-argument-type] invalid-argument-type: Argument to function `_build_catalog_entry` is incorrect: Expected `str`, found `Literal["telegram", "discord", "whatsapp", "whatsapp_cloud", "slack", ... omitted 17 literals] | set[Unknown]`
gateway/run.py:5671: [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", "whatsapp_cloud", ... omitted 18 literals] | set[Unknown]`
... and 40 more
✅ Fixed issues (29):
| Rule | Count |
|---|---|
invalid-argument-type |
22 |
unresolved-attribute |
4 |
invalid-return-type |
2 |
invalid-assignment |
1 |
First entries
hermes_cli/web_server.py:3710: [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/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/run.py:11428: [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/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:7397: [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/run.py:11401: [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]`
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]`
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]`
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/slash_commands.py:681: [unresolved-attribute] unresolved-attribute: Attribute `lower` 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/run.py:1634: [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]`
hermes_cli/web_server.py:3711: [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/run.py:13910: [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]`
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:13764: [invalid-argument-type] invalid-argument-type: Argument to function `resolve_display_setting` is incorrect: Expected `str`, found `Literal["cli", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
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/run.py:6404: [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/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/config.py:581: [invalid-argument-type] invalid-argument-type: Argument to bound method `PlatformRegistry.get` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 16 literals] | set[Unknown]`
gateway/slash_commands.py:1740: [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/slash_commands.py:1739: [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/run.py:11760: [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:5669: [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/run.py:6425: [invalid-argument-type] invalid-argument-type: Argument to bound method `PairingStore._record_rate_limit` is incorrect: Expected `str`, found `Literal["local", "telegram", "discord", "whatsapp", "slack", ... omitted 17 literals] | set[Unknown]`
... and 4 more
Unchanged: 5580 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
AIalliAI
pushed a commit
to AIalliAI/Hermes
that referenced
this pull request
Jun 14, 2026
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
1 task
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
8 tasks
Open
8 tasks
habarmc1223-sudo
pushed a commit
to habarmc1223-sudo/hermes-agent-fluxmem
that referenced
this pull request
Jul 8, 2026
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…-6b48295e feat(whatsapp): WhatsApp Business Cloud API adapter (salvage NousResearch#43921)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Salvage of #43921 (@jquesnelle): official WhatsApp Business Cloud API gateway adapter, plus review follow-up fixes on top.
The contributor's two commits are preserved as-is (merged forward onto current main). One follow-up commit addresses findings from review.
What the original PR adds
gateway/platforms/whatsapp_cloud.py— Meta Cloud API adapter: webhook inbound (verify-token handshake + raw-body HMAC-SHA256 signature verification), Graph API outbound (text, media, opus voice notes, interactive buttons for clarify/approval/confirm), wamid dedup, typing/read receiptsgateway/platforms/whatsapp_common.py—WhatsAppBehaviorMixinshared by Baileys + Cloud adapters (gating, mentions, markdown conversion, chunking); Baileys slimmed ~280 lines, no behavior changehermes whatsapp-cloudsetup wizard with paste-time credential validationFollow-up fixes (review findings)
apply_nous_managed_defaultsnow skips thestt.provider → openaiflip when the account isn't entitled toopenai-audio(pool-only users would be pointed at a gateway that refuses them) or when a local backend (faster-whisper / custom command) works. New_local_stt_backend_available()helper + 2 tests.WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,GROUP_ALLOW_FROM}overrides (sharedWHATSAPP_*names remain fallback); allowlist entries normalized to bare wa_id so Baileys-style JIDs / punctuated numbers match..oggdeleted after voice upload; interactive-button state dicts + per-chat wamid cache FIFO-capped; setup wizard reads token/secret via getpass on TTYs;# **Title**no longer renders literal asterisks.Validation
Closes #43921 — thanks @jquesnelle, commits preserved with original authorship.
Infographic