Skip to content

feat(whatsapp): WhatsApp Business Cloud API adapter (salvage #43921) - #44331

Merged
teknium1 merged 4 commits into
mainfrom
hermes/hermes-6b48295e
Jun 12, 2026
Merged

feat(whatsapp): WhatsApp Business Cloud API adapter (salvage #43921)#44331
teknium1 merged 4 commits into
mainfrom
hermes/hermes-6b48295e

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

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 receipts
  • gateway/platforms/whatsapp_common.pyWhatsAppBehaviorMixin shared by Baileys + Cloud adapters (gating, mentions, markdown conversion, chunking); Baileys slimmed ~280 lines, no behavior change
  • hermes whatsapp-cloud setup wizard with paste-time credential validation
  • Gateway/authz/cron/status/prompt integration, STT in Nous subscription detection, full docs

Follow-up fixes (review findings)

  • STT managed-default flip gated correctlyapply_nous_managed_defaults now skips the stt.provider → openai flip when the account isn't entitled to openai-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.
  • Parallel-adapter env decouplingWHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,GROUP_ALLOW_FROM} overrides (shared WHATSAPP_* names remain fallback); allowlist entries normalized to bare wa_id so Baileys-style JIDs / punctuated numbers match.
  • Webhook message-loss fix — per-message event build wrapped in try/except: a raise after dedup-marking would 500 the batch and Meta's retry would silently drop every message in it as a duplicate.
  • Hardening — media_id validated before URL/filename interpolation; transient .ogg deleted 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

Result
Targeted suites (whatsapp_cloud, whatsapp_*, nous_subscription, scheduler, display_config, prompt_builder, setup wizard) 571/571 pass
E2E (real imports, temp HERMES_HOME) cloud env overrides win, JID allowlist matches bare wa_id, stranger blocked, Baileys unaffected, header fix verified

Closes #43921 — thanks @jquesnelle, commits preserved with original authorship.

Infographic

WhatsApp Business Cloud API adapter infographic

jquesnelle and others added 4 commits May 23, 2026 01:07
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
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-6b48295e vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10790 on HEAD, 10717 on base (🆕 +73)

🆕 New issues (65):

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.

@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/whatsapp WhatsApp Business adapter labels Jun 11, 2026
@teknium1
teknium1 merged commit db7714d into main Jun 12, 2026
29 checks passed
@teknium1
teknium1 deleted the hermes/hermes-6b48295e branch June 12, 2026 05:48
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)
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)
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/whatsapp WhatsApp Business adapter type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants