feat: WhatsApp role-router overlay (WS2/WS3) + waba-routing dashboard plugin#14
feat: WhatsApp role-router overlay (WS2/WS3) + waba-routing dashboard plugin#14Wizarck wants to merge 5 commits into
Conversation
…ternal MCP)
Adds a new platform adapter ``whatsapp_via_mcp_meta_business_api`` that lets
Hermes serve a phone number on the official Meta WhatsApp Business Cloud
API. The Meta-side concerns (webhook ingress, outbound delivery, message
persistence, sender tagging) are delegated to an external WhatsApp MCP
server that forwards inbound messages to this gateway over a thin HTTP
forward; the gateway replies via Meta Graph API directly.
Why this exists: the upstream ``whatsapp.py`` platform uses the Baileys
Node bridge (reverse-engineered WhatsApp Web), which violates Meta's
terms of service and is fragile on Windows. Users who run a phone number
on the official Meta WhatsApp Business Cloud API have no first-class
platform in Hermes today. This module fills that gap.
Architecture:
Meta Cloud Business API
v webhook
[external WA-MCP server] (owns Meta credentials)
v POST {message_id, phone, type, content, tags, raw}
v X-Webhook-Secret: <shared secret>
[whatsapp_via_mcp_meta_business_api adapter]
^ v
| reply via Meta Graph <- send()
/messages
Files:
* ``gateway/platforms/whatsapp_via_mcp_meta_business_api.py`` — new adapter
(~376 LOC, mirrors the WABA design in shape and dependencies).
* ``gateway/config.py`` — Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API enum
+ env loader for token / phone_number_id / webhook_secret / host / port /
path / home_channel.
* ``gateway/run.py`` — adapter factory + auth allowlist + allow-all map.
* ``agent/prompt_builder.py`` — PLATFORM_HINTS entry so the agent knows
it's on WhatsApp (plain text, 4096-char body limit, MEDIA: works).
* ``toolsets.py`` — hermes-whatsapp-via-mcp-meta-business-api toolset +
inclusion in the hermes-gateway composite.
* ``website/docs/user-guide/messaging/whatsapp-via-mcp-meta-business-api.md``
— user-guide doc covering env config and the WA-MCP wiring.
Configuration via env (all pluralised under
``WHATSAPP_VIA_MCP_META_BUSINESS_API_*``):
* ``_TOKEN`` — Meta System User access token (whatsapp_business_messaging).
* ``_PHONE_NUMBER_ID`` — phone number id for the Graph API path.
* ``_WEBHOOK_SECRET`` — shared secret with the WA-MCP, validated as the
``X-Webhook-Secret`` header on every forward (constant-time compare).
* ``_HOST`` / ``_PORT`` / ``_PATH`` — aiohttp bind for the forward endpoint.
* ``_HOME_CHANNEL`` — default destination for cron-driven nudges.
* ``_ALLOWED_USERS`` / ``_ALLOW_ALL_USERS`` — auth allowlists.
Closes the gap for the most common WhatsApp deployment pattern (Meta
Cloud API + a small forwarder MCP) without requiring Hermes itself to
own the Meta credentials directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…deo/document)
The webhook previously hard-skipped any non-text message
(return {skipped: non_text}), so images and voice notes from clients were
silently dropped — a limitation of THIS adapter, not of Meta's Cloud API,
which delivers media normally. The router already forwards the full Meta
message as `raw`, which carries the media id.
Parse `raw` for the media object, download the bytes via the Graph two-step
flow (GET /{media-id} -> short-lived lookaside URL -> GET with the bearer
token we already hold), cache them, and build a typed MessageEvent
(PHOTO/VOICE/VIDEO/DOCUMENT) with media_urls — the same shape the bridge
platform and Telegram/Discord already use, so the agent can see images and
transcribe voice notes. Download failures and unsupported types ACK-and-skip
instead of 500 (Meta retries 5xx, which would loop).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Now that inbound WhatsApp images reach the agent, langfuse v2 auto-captured the image data URI from the traced messages and base64-decoded our truncated (thus corrupted) copy, logging "Error parsing base64 data URI" once per image (non-fatal, but ERROR spam). _truncate_text now replaces any "data:" string with a compact "[media <mime> N b64 chars omitted]" marker before slicing, so the SDK never sees a data: prefix to decode/upload. Trace copy only — the real LLM request is untouched. Covers image/audio/video alike. Adds 4 regression tests (data URI -> marker, missing-mime fallback, plain text pass-through, long-text truncation still works). Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ht bank (WS3)
The role stamped server-side by waba-mcp on the inbound forward drives a
per-role thread_id / channel_prompt / channel_skill_bindings (WS2) and a
per-role Hindsight memory bank via SessionSource.role + the _SESSION_ROLE
contextvar + _effective_bank_id (WS3; activate with bank_id_template:
hermes-{role}). Includes the webhook MessageDeduplicator guard and the
flush-on-switch fix (capture _effective_bank_id synchronously). 23 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Profile Builder dashboard plugin (tab "Routing", :9119) that manages the waba-mcp routing registry without a redeploy. plugin_api.py proxies the waba-mcp admin API server-side (bearer kept off the browser; env WABA_ADMIN_URL + WABA_ADMIN_API_TOKEN); dist/index.js renders Lines / Grants / audit tables + forms via the plugin SDK. Backend validates + audits. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
📝 WalkthroughWalkthroughAdds a new ChangesWhatsApp MCP Adapter, Role Multiplexing, and Supporting Changes
WABA Routing Dashboard Plugin
Sequence Diagram(s)sequenceDiagram
participant User as WhatsApp User
participant MCP as External WhatsApp MCP Server
participant Adapter as WhatsAppViaMcpMetaBusinessApiAdapter
participant Deduplicator as MessageDeduplicator
participant Hermes as Hermes Agent (handle_message)
participant Meta as Meta Graph API
User->>MCP: sends WhatsApp message
MCP->>Adapter: POST /webhook (X-Webhook-Secret, JSON body)
Adapter->>Adapter: verify secret, parse phone/content/message_id
Adapter->>Deduplicator: is message_id seen?
alt duplicate
Adapter-->>MCP: 200 {"ok":true,"queued":false}
else first-seen
Adapter->>Adapter: resolve role/tier from payload fields/tags
Adapter->>Adapter: build MessageEvent (text or media download)
Adapter-->>MCP: 200 {"ok":true,"queued":true}
Adapter->>Hermes: asyncio.create_task(handle_message)
Hermes->>Adapter: send(chat_id, reply_text)
Adapter->>Meta: POST /messages (bearer token)
Meta-->>Adapter: {"messages":[{"id":"..."}]}
Adapter-->>Hermes: SendResult
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (5)
gateway/platforms/whatsapp_via_mcp_meta_business_api.py (4)
639-641: ⚖️ Poor tradeoffSynchronous file write may block the event loop for large media.
path.write_bytes(data)is a blocking I/O operation. For large media files (videos up to 16MB per Meta docs), this could momentarily block the event loop. Consider usingasyncio.to_thread()oraiofilesfor non-blocking writes.♻️ Proposed async file write
- path = _media_cache_dir() / f"wa_{media_id}{ext}" - path.write_bytes(data) - return str(path) + path = _media_cache_dir() / f"wa_{media_id}{ext}" + await asyncio.to_thread(path.write_bytes, data) + return str(path)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gateway/platforms/whatsapp_via_mcp_meta_business_api.py` around lines 639 - 641, The path.write_bytes(data) call is a synchronous blocking I/O operation that can block the event loop when handling large media files up to 16MB. Convert this to a non-blocking operation by wrapping the write operation with asyncio.to_thread() to offload the blocking I/O to a thread pool, or alternatively use the aiofiles library for asynchronous file operations. Ensure the function containing this write operation is marked as async and properly awaits the non-blocking write call.
168-170: 💤 Low valuePort parsing may raise
ValueErroron invalid input.If
extra.get("port")or the environment variable contains a non-numeric string,int()will raise an unhandledValueError, causing adapter initialization to fail with an unclear error.🛡️ Proposed defensive parsing
- self._port: int = int( - extra.get("port") or os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT", "8643") - ) + try: + self._port: int = int( + extra.get("port") or os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT", "8643") + ) + except ValueError: + self._port = 8643 + logger.warning("Invalid port configured; falling back to default 8643")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gateway/platforms/whatsapp_via_mcp_meta_business_api.py` around lines 168 - 170, The port parsing in the initialization of self._port lacks error handling and will raise an unhandled ValueError if extra.get("port") or the environment variable contains non-numeric values. Wrap the int() conversion in a try-except block to catch ValueError exceptions, and either use a sensible default port value, raise a more descriptive error message, or log a warning when invalid port input is detected. This will ensure the adapter initialization fails gracefully with clear error messaging rather than an obscure ValueError.
421-421: 💤 Low valueConsider storing task reference for explicit lifecycle management.
The task returned by
asyncio.create_task()is not stored. While running tasks are kept alive by the event loop, storing references enables graceful shutdown (awaiting pending tasks indisconnect()) and exception logging if a task fails silently.♻️ Proposed pattern for task tracking
Add a task set to the class:
# In __init__: self._pending_tasks: set[asyncio.Task] = set()- asyncio.create_task(self.handle_message(event)) + task = asyncio.create_task(self.handle_message(event)) + self._pending_tasks.add(task) + task.add_done_callback(self._pending_tasks.discard)Optionally await pending tasks in
disconnect():if self._pending_tasks: await asyncio.gather(*self._pending_tasks, return_exceptions=True)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gateway/platforms/whatsapp_via_mcp_meta_business_api.py` at line 421, The task created by asyncio.create_task(self.handle_message(event)) is not stored, preventing graceful shutdown and exception handling. Add a _pending_tasks set attribute in the class __init__ method, store the task reference returned by asyncio.create_task() in this set, and ensure tasks are removed from the set when they complete (using add_done_callback). Optionally, in the disconnect() method, await any pending tasks in _pending_tasks with return_exceptions=True to ensure clean shutdown before the connection closes.
510-512: 💤 Low valueInconsistent error handling between
_post_metaand_download_meta_media.
_post_metausesassert self._http_client is not None(line 511), which raisesAssertionErrorif the adapter isn't connected. In contrast,_download_meta_mediaraisesRuntimeError(line 622). The assert can also be stripped in optimized bytecode (python -O). Consider using explicit checks consistently.♻️ Proposed consistency fix
async def _post_meta(self, payload: Dict[str, Any]) -> SendResult: - assert self._http_client is not None + if self._http_client is None: + return SendResult(success=False, error="adapter not connected") url = f"{self._meta_base_url}/{self._phone_number_id}/messages"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gateway/platforms/whatsapp_via_mcp_meta_business_api.py` around lines 510 - 512, Replace the assert statement in the `_post_meta` method that checks `self._http_client is not None` with an explicit RuntimeError check to match the error handling pattern used in `_download_meta_media`. This ensures consistent error handling across both methods and avoids relying on assertions which can be stripped when Python runs in optimized bytecode mode.tests/gateway/test_whatsapp_via_mcp_role_router.py (1)
133-152: ⚡ Quick winAsync test relies on fixed sleep duration which may be flaky.
The test uses
asyncio.sleep(0.02)to wait for the background task. On slow CI runners or under load, this might not be sufficient. Consider using a more robust synchronization mechanism.♻️ Proposed fix using an Event for synchronization
`@pytest.mark.asyncio` async def test_duplicate_webhook_skips_second_agent_run(monkeypatch): a = _adapter(ROLE_EXTRA) a._secret = "" # disable secret check for the test calls = [] + done_event = asyncio.Event() async def fake_handle(event): calls.append(event.message_id) + done_event.set() monkeypatch.setattr(a, "handle_message", fake_handle) body = json.dumps( {"message_id": "wamid.dup", "phone": "34600", "type": "text", "content": "hola"} ).encode() await a._handle_webhook(_FakeReq(body)) await a._handle_webhook(_FakeReq(body)) # redelivery - await asyncio.sleep(0.02) # let the create_task run + await asyncio.wait_for(done_event.wait(), timeout=1.0) # wait for task with timeout assert calls == ["wamid.dup"] # second delivery deduped🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/gateway/test_whatsapp_via_mcp_role_router.py` around lines 133 - 152, The test `test_duplicate_webhook_skips_second_agent_run` uses a fixed `asyncio.sleep(0.02)` duration to wait for background tasks, which is unreliable and can be flaky on slow CI runners. Replace the sleep-based synchronization with an asyncio.Event that signals when the background task completes. Create an Event object before patching the `fake_handle` function, have `fake_handle` set the Event after appending each call to the calls list, and then await the Event with a reasonable timeout instead of using asyncio.sleep. This ensures the test waits for actual task completion rather than an arbitrary duration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@gateway/config.py`:
- Around line 1597-1608: The webhook_secret configuration for the
WHATSAPP_VIA_MCP_META_BUSINESS_API platform uses an empty string as a default
when the WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET environment variable
is missing, which weakens authentication on an internet-facing endpoint. Modify
the os.getenv call for webhook_secret to remove the empty string default and
instead require the environment variable to be present; if missing, raise an
appropriate error to hard-fail the configuration and prevent the adapter from
being enabled without proper webhook authentication.
- Line 1606: The port parsing at the WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT
configuration will crash if the environment variable is set to a non-numeric
value because int() raises a ValueError. Wrap the int() conversion with error
handling to safely parse the port value and fall back to the default port "8643"
if the environment variable is malformed or not numeric. This ensures the
application continues to load configuration gracefully even when invalid port
values are provided via environment variables.
In `@gateway/platforms/whatsapp_via_mcp_meta_business_api.py`:
- Around line 186-220: The connect() method is missing a call to acquire a
platform lock after validating the token. Add a call to
self._acquire_platform_lock('whatsapp-mcp-token', self._token, 'WhatsApp MCP
token') immediately after the token validation check (after the if not
self._token block). Additionally, add a corresponding call to
self._release_platform_lock() in the disconnect() method to release the lock
when the adapter disconnects. This prevents multiple profiles from sharing the
same WhatsApp MCP token credential, consistent with other gateway platform
adapters.
In `@gateway/session.py`:
- Line 94: The `role` field added to the class is not being persisted through
serialization. Update the `to_dict()` method to include the `role` field when
converting the session to a dictionary, and update the `from_dict()` method to
extract and restore the `role` field when reconstructing the session from a
dictionary. This ensures that the role information is preserved when sessions
are saved and loaded, maintaining role-aware session continuity across restarts.
In `@plugins/memory/hindsight/__init__.py`:
- Around line 1306-1311: The try-except block in the role-resolution code around
the get_session_env call is catching all exceptions indiscriminately, which
silently masks unexpected errors and can lead to role-scoped writes being routed
incorrectly when they shouldn't be. Replace the bare except Exception clause
with specific exception handling that catches only ImportError and
AttributeError (which are expected when the gateway.session_context module or
function is unavailable), allowing these to fall back to role="". For any other
unexpected exceptions, log them explicitly using an appropriate logger before
falling back, so that unexpected failures in session context resolution don't go
unnoticed and can be debugged properly.
In `@plugins/observability/langfuse/__init__.py`:
- Around line 155-169: The _truncate_text function returns early when detecting
a data URI (value starting with _DATA_URI_PREFIX), bypassing the max_chars
truncation check. Instead of returning the media marker immediately, construct
the marker string and then apply the same truncation logic used for non-data-URI
values, ensuring the marker also respects the max_chars limit. Route the
generated media marker through the existing length check and truncation logic at
the end of the function rather than returning it directly.
In `@plugins/waba-routing/dashboard/plugin_api.py`:
- Around line 22-25: Replace all four broad `except Exception` clauses with
specific exception types: change the exception handler in the httpx import guard
to catch `ImportError` instead, change the exception handler for `await
request.json()` calls to catch `ValueError` instead, change the exception
handler for httpx client requests to catch `httpx.RequestError` instead, and
change the exception handler for `resp.json()` calls to catch `ValueError`
instead. This will ensure that only expected exceptions are caught at each
location, allowing unrelated failures and real bugs to surface rather than being
silently masked by overly broad exception handling.
In `@website/docs/user-guide/messaging/whatsapp-via-mcp-meta-business-api.md`:
- Around line 29-41: The markdown file contains fenced code blocks that are
missing language identifiers, which violates the markdownlint MD040 rule. Add
the language identifier "text" to both fenced code block declarations: the first
one containing the architecture diagram (WhatsApp users through Meta Cloud API)
and the second one at lines 105-107 containing the WhatsApp via MCP listening
message. Change the opening fence from triple backticks (```) to triple
backticks followed by "text" (```text) for both blocks to satisfy the linting
requirement.
---
Nitpick comments:
In `@gateway/platforms/whatsapp_via_mcp_meta_business_api.py`:
- Around line 639-641: The path.write_bytes(data) call is a synchronous blocking
I/O operation that can block the event loop when handling large media files up
to 16MB. Convert this to a non-blocking operation by wrapping the write
operation with asyncio.to_thread() to offload the blocking I/O to a thread pool,
or alternatively use the aiofiles library for asynchronous file operations.
Ensure the function containing this write operation is marked as async and
properly awaits the non-blocking write call.
- Around line 168-170: The port parsing in the initialization of self._port
lacks error handling and will raise an unhandled ValueError if extra.get("port")
or the environment variable contains non-numeric values. Wrap the int()
conversion in a try-except block to catch ValueError exceptions, and either use
a sensible default port value, raise a more descriptive error message, or log a
warning when invalid port input is detected. This will ensure the adapter
initialization fails gracefully with clear error messaging rather than an
obscure ValueError.
- Line 421: The task created by asyncio.create_task(self.handle_message(event))
is not stored, preventing graceful shutdown and exception handling. Add a
_pending_tasks set attribute in the class __init__ method, store the task
reference returned by asyncio.create_task() in this set, and ensure tasks are
removed from the set when they complete (using add_done_callback). Optionally,
in the disconnect() method, await any pending tasks in _pending_tasks with
return_exceptions=True to ensure clean shutdown before the connection closes.
- Around line 510-512: Replace the assert statement in the `_post_meta` method
that checks `self._http_client is not None` with an explicit RuntimeError check
to match the error handling pattern used in `_download_meta_media`. This ensures
consistent error handling across both methods and avoids relying on assertions
which can be stripped when Python runs in optimized bytecode mode.
In `@tests/gateway/test_whatsapp_via_mcp_role_router.py`:
- Around line 133-152: The test `test_duplicate_webhook_skips_second_agent_run`
uses a fixed `asyncio.sleep(0.02)` duration to wait for background tasks, which
is unreliable and can be flaky on slow CI runners. Replace the sleep-based
synchronization with an asyncio.Event that signals when the background task
completes. Create an Event object before patching the `fake_handle` function,
have `fake_handle` set the Event after appending each call to the calls list,
and then await the Event with a reasonable timeout instead of using
asyncio.sleep. This ensures the test waits for actual task completion rather
than an arbitrary duration.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e9ef4db3-0578-4eac-8d9d-3e939546459f
⛔ Files ignored due to path filters (1)
plugins/waba-routing/dashboard/dist/index.jsis excluded by!**/dist/**
📒 Files selected for processing (16)
agent/prompt_builder.pygateway/config.pygateway/platforms/base.pygateway/platforms/whatsapp_via_mcp_meta_business_api.pygateway/run.pygateway/session.pygateway/session_context.pyplugins/memory/hindsight/__init__.pyplugins/observability/langfuse/__init__.pyplugins/waba-routing/dashboard/manifest.jsonplugins/waba-routing/dashboard/plugin_api.pytests/gateway/test_whatsapp_via_mcp_role_router.pytests/plugins/memory/test_hindsight_role_bank.pytests/plugins/test_langfuse_plugin.pytoolsets.pywebsite/docs/user-guide/messaging/whatsapp-via-mcp-meta-business-api.md
| if wamba_token and wamba_phone_id: | ||
| if Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API not in config.platforms: | ||
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API] = PlatformConfig() | ||
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].enabled = True | ||
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].token = wamba_token | ||
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].extra.update({ | ||
| "phone_number_id": wamba_phone_id, | ||
| "webhook_secret": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET", ""), | ||
| "host": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_HOST", "0.0.0.0"), | ||
| "port": int(os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT", "8643")), | ||
| "path": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PATH", "/wa"), | ||
| }) |
There was a problem hiding this comment.
Require (or hard-fail on missing) webhook secret for this public webhook adapter.
This enables the adapter even when WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET is empty, which weakens request authentication for an internet-facing endpoint.
Suggested hardening
if wamba_token and wamba_phone_id:
+ webhook_secret = os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET", "").strip()
+ if not webhook_secret:
+ logger.error(
+ "WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET is required "
+ "for whatsapp_via_mcp_meta_business_api; adapter will not be enabled."
+ )
+ return
if Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API not in config.platforms:
config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API] = PlatformConfig()
config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].enabled = True
config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].token = wamba_token
config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].extra.update({
"phone_number_id": wamba_phone_id,
- "webhook_secret": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET", ""),
+ "webhook_secret": webhook_secret,
"host": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_HOST", "0.0.0.0"),
"port": int(os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT", "8643")),
"path": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PATH", "/wa"),
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if wamba_token and wamba_phone_id: | |
| if Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API not in config.platforms: | |
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API] = PlatformConfig() | |
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].enabled = True | |
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].token = wamba_token | |
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].extra.update({ | |
| "phone_number_id": wamba_phone_id, | |
| "webhook_secret": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET", ""), | |
| "host": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_HOST", "0.0.0.0"), | |
| "port": int(os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT", "8643")), | |
| "path": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PATH", "/wa"), | |
| }) | |
| if wamba_token and wamba_phone_id: | |
| webhook_secret = os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET", "").strip() | |
| if not webhook_secret: | |
| logger.error( | |
| "WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET is required " | |
| "for whatsapp_via_mcp_meta_business_api; adapter will not be enabled." | |
| ) | |
| return | |
| if Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API not in config.platforms: | |
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API] = PlatformConfig() | |
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].enabled = True | |
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].token = wamba_token | |
| config.platforms[Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API].extra.update({ | |
| "phone_number_id": wamba_phone_id, | |
| "webhook_secret": webhook_secret, | |
| "host": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_HOST", "0.0.0.0"), | |
| "port": int(os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT", "8643")), | |
| "path": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PATH", "/wa"), | |
| }) |
🧰 Tools
🪛 Ruff (0.15.17)
[error] 1605-1605: Possible binding to all interfaces
(S104)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@gateway/config.py` around lines 1597 - 1608, The webhook_secret configuration
for the WHATSAPP_VIA_MCP_META_BUSINESS_API platform uses an empty string as a
default when the WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET environment
variable is missing, which weakens authentication on an internet-facing
endpoint. Modify the os.getenv call for webhook_secret to remove the empty
string default and instead require the environment variable to be present; if
missing, raise an appropriate error to hard-fail the configuration and prevent
the adapter from being enabled without proper webhook authentication.
| "phone_number_id": wamba_phone_id, | ||
| "webhook_secret": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET", ""), | ||
| "host": os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_HOST", "0.0.0.0"), | ||
| "port": int(os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT", "8643")), |
There was a problem hiding this comment.
Guard port parsing to avoid startup crash on malformed env values.
int(os.getenv(...)) will raise ValueError and abort config loading if the env var is non-numeric.
Safer parsing
- "port": int(os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT", "8643")),
+ "port": _coerce_int(
+ os.getenv("WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT"), 8643
+ ),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@gateway/config.py` at line 1606, The port parsing at the
WHATSAPP_VIA_MCP_META_BUSINESS_API_PORT configuration will crash if the
environment variable is set to a non-numeric value because int() raises a
ValueError. Wrap the int() conversion with error handling to safely parse the
port value and fall back to the default port "8643" if the environment variable
is malformed or not numeric. This ensures the application continues to load
configuration gracefully even when invalid port values are provided via
environment variables.
| async def connect(self) -> bool: | ||
| if not self._token: | ||
| logger.error( | ||
| "WhatsApp via MCP: no token configured (WHATSAPP_VIA_MCP_META_BUSINESS_API_TOKEN)" | ||
| ) | ||
| return False | ||
| if not self._phone_number_id: | ||
| logger.error( | ||
| "WhatsApp via MCP: no phone_number_id configured " | ||
| "(WHATSAPP_VIA_MCP_META_BUSINESS_API_PHONE_NUMBER_ID)" | ||
| ) | ||
| return False | ||
| if not self._secret: | ||
| logger.warning( | ||
| "WhatsApp via MCP: no webhook secret configured — forwards will be accepted " | ||
| "without authentication. Set WHATSAPP_VIA_MCP_META_BUSINESS_API_WEBHOOK_SECRET." | ||
| ) | ||
|
|
||
| self._http_client = httpx.AsyncClient(timeout=30) | ||
|
|
||
| self._app = web.Application() | ||
| self._app.router.add_get("/health", self._handle_health) | ||
| self._app.router.add_post(self._path, self._handle_webhook) | ||
| self._runner = web.AppRunner(self._app) | ||
| await self._runner.setup() | ||
| self._site = web.TCPSite(self._runner, self._host, self._port) | ||
| await self._site.start() | ||
| logger.info( | ||
| "WhatsApp via MCP listening on %s:%s%s (phone_id=%s)", | ||
| self._host, | ||
| self._port, | ||
| self._path, | ||
| self._phone_number_id, | ||
| ) | ||
| return True |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the token lock pattern in the reference Telegram adapter
ast-grep --pattern $'async def connect(self) -> bool:
$$$
acquire_scoped_lock($$$)
$$$
}'Repository: Wizarck/hermes-agent
Length of output: 46
🏁 Script executed:
# Find and inspect the Telegram adapter reference implementation
fd -t f -i telegram.pyRepository: Wizarck/hermes-agent
Length of output: 94
🏁 Script executed:
# Search for acquire_scoped_lock usage in gateway platform adapters
rg "acquire_scoped_lock|release_scoped_lock" gateway/platforms/Repository: Wizarck/hermes-agent
Length of output: 680
🏁 Script executed:
# Inspect the whatsapp adapter connect and disconnect methods
fd -t f whatsapp_via_mcp_meta_business_api.py | head -1 | xargs cat | grep -A 50 "async def connect\|async def disconnect"Repository: Wizarck/hermes-agent
Length of output: 3572
🏁 Script executed:
# Check if these functions exist in gateway.status
fd -t f status.py | xargs grep -l "acquire_scoped_lock\|release_scoped_lock"Repository: Wizarck/hermes-agent
Length of output: 111
🏁 Script executed:
# Examine the Telegram adapter to see the exact token lock pattern
fd -t f telegram.py gateway/platforms/ | xargs cat | grep -A 30 "async def connect"Repository: Wizarck/hermes-agent
Length of output: 1350
🏁 Script executed:
# Check the base class to understand _acquire_platform_lock
fd -t f base.py gateway/platforms/ | xargs cat | grep -B 5 -A 20 "_acquire_platform_lock\|_release_platform_lock"Repository: Wizarck/hermes-agent
Length of output: 1886
🏁 Script executed:
# Check if whatsapp adapter extends base class
fd -t f whatsapp_via_mcp_meta_business_api.py | xargs head -50 | grep -A 3 "class\|import.*base"Repository: Wizarck/hermes-agent
Length of output: 353
🏁 Script executed:
# Check the full class definition and inheritance of whatsapp adapter
fd -t f whatsapp_via_mcp_meta_business_api.py | xargs head -100Repository: Wizarck/hermes-agent
Length of output: 3450
🏁 Script executed:
# Check if whatsapp adapter has any lock-related code
fd -t f whatsapp_via_mcp_meta_business_api.py | xargs rg "_acquire_platform_lock|_release_platform_lock|acquire_scoped_lock|release_scoped_lock"Repository: Wizarck/hermes-agent
Length of output: 48
🏁 Script executed:
# Check the disconnect method of whatsapp adapter for lock release
fd -t f whatsapp_via_mcp_meta_business_api.py | xargs cat | grep -A 15 "async def disconnect"Repository: Wizarck/hermes-agent
Length of output: 704
🏁 Script executed:
# Verify whatsapp adapter class definition and inheritance
fd -t f whatsapp_via_mcp_meta_business_api.py | xargs grep "^class.*BasePlatformAdapter"Repository: Wizarck/hermes-agent
Length of output: 130
🏁 Script executed:
# Check if other platform adapters besides Telegram use the lock pattern
rg "_acquire_platform_lock" gateway/platforms/ --type pyRepository: Wizarck/hermes-agent
Length of output: 1282
Missing token lock to prevent credential conflicts across profiles.
The adapter does not call _acquire_platform_lock() in connect() or _release_platform_lock() in disconnect(). Every other gateway platform adapter (Telegram, WhatsApp, Discord, Slack, Signal, Weixin, QQBot) uses this pattern to prevent two profiles from sharing the same credential. Add a call like self._acquire_platform_lock('whatsapp-mcp-token', self._token, 'WhatsApp MCP token') after validating the token, and call self._release_platform_lock() in disconnect().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@gateway/platforms/whatsapp_via_mcp_meta_business_api.py` around lines 186 -
220, The connect() method is missing a call to acquire a platform lock after
validating the token. Add a call to
self._acquire_platform_lock('whatsapp-mcp-token', self._token, 'WhatsApp MCP
token') immediately after the token validation check (after the if not
self._token block). Additionally, add a corresponding call to
self._release_platform_lock() in the disconnect() method to release the lock
when the adapter disconnects. This prevents multiple profiles from sharing the
same WhatsApp MCP token credential, consistent with other gateway platform
adapters.
Source: Coding guidelines
| guild_id: Optional[str] = None # Discord guild / Slack workspace / Matrix server scope | ||
| parent_chat_id: Optional[str] = None # Parent channel when chat_id refers to a thread | ||
| message_id: Optional[str] = None # ID of the triggering message (for pin/reply/react) | ||
| role: Optional[str] = None # WS2/WS3 persona/role multiplexed within one channel (WhatsApp role router) |
There was a problem hiding this comment.
Persist SessionSource.role through serialization.
Line 94 adds role, but to_dict()/from_dict() never carry it, so SessionEntry.origin drops role after save/load. That breaks role-aware session continuity after restart.
Suggested fix
diff --git a/gateway/session.py b/gateway/session.py
@@
def to_dict(self) -> Dict[str, Any]:
d = {
"platform": self.platform.value,
"chat_id": self.chat_id,
"chat_name": self.chat_name,
"chat_type": self.chat_type,
"user_id": self.user_id,
"user_name": self.user_name,
"thread_id": self.thread_id,
"chat_topic": self.chat_topic,
+ "role": self.role,
}
@@
def from_dict(cls, data: Dict[str, Any]) -> "SessionSource":
return cls(
platform=Platform(data["platform"]),
chat_id=str(data["chat_id"]),
chat_name=data.get("chat_name"),
chat_type=data.get("chat_type", "dm"),
user_id=data.get("user_id"),
user_name=data.get("user_name"),
thread_id=data.get("thread_id"),
chat_topic=data.get("chat_topic"),
+ role=data.get("role"),
user_id_alt=data.get("user_id_alt"),
chat_id_alt=data.get("chat_id_alt"),
guild_id=data.get("guild_id"),
parent_chat_id=data.get("parent_chat_id"),
message_id=data.get("message_id"),
)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@gateway/session.py` at line 94, The `role` field added to the class is not
being persisted through serialization. Update the `to_dict()` method to include
the `role` field when converting the session to a dictionary, and update the
`from_dict()` method to extract and restore the `role` field when reconstructing
the session from a dictionary. This ensures that the role information is
preserved when sessions are saved and loaded, maintaining role-aware session
continuity across restarts.
| try: | ||
| from gateway.session_context import get_session_env | ||
|
|
||
| role = get_session_env("HERMES_SESSION_ROLE", "") | ||
| except Exception: | ||
| role = "" |
There was a problem hiding this comment.
Narrow the role-resolution fallback to avoid silent cross-role bank mixing.
Catching all exceptions here silently collapses to role="", which can route role-scoped writes into the fallback bank when session-context lookup regresses. Keep fail-open for missing gateway context, but log unexpected failures explicitly.
Suggested patch
try:
from gateway.session_context import get_session_env
role = get_session_env("HERMES_SESSION_ROLE", "")
- except Exception:
+ except ImportError:
role = ""
+ except Exception as exc:
+ logger.warning(
+ "Failed to resolve HERMES_SESSION_ROLE; using fallback bank: %s",
+ exc,
+ )
+ role = ""🧰 Tools
🪛 Ruff (0.15.17)
[warning] 1310-1310: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/memory/hindsight/__init__.py` around lines 1306 - 1311, The
try-except block in the role-resolution code around the get_session_env call is
catching all exceptions indiscriminately, which silently masks unexpected errors
and can lead to role-scoped writes being routed incorrectly when they shouldn't
be. Replace the bare except Exception clause with specific exception handling
that catches only ImportError and AttributeError (which are expected when the
gateway.session_context module or function is unavailable), allowing these to
fall back to role="". For any other unexpected exceptions, log them explicitly
using an appropriate logger before falling back, so that unexpected failures in
session context resolution don't go unnoticed and can be debugged properly.
Source: Linters/SAST tools
| def _truncate_text(value: str, max_chars: int) -> str: | ||
| # Langfuse v2 auto-captures any traced string starting with "data:" as | ||
| # multimodal media (media_manager._find_and_process_media), then runs | ||
| # base64.b64decode on the tail. Our own truncation corrupts that base64 | ||
| # while leaving the "data:" prefix intact, so the SDK logs "Error parsing | ||
| # base64 data URI" once per inbound image. Replace data URIs with a | ||
| # compact, non-"data:" marker BEFORE slicing so the SDK skips them | ||
| # entirely. This only mutates the trace copy, never the real LLM request. | ||
| if value.startswith(_DATA_URI_PREFIX): | ||
| header, _, rest = value[len(_DATA_URI_PREFIX):].partition(",") | ||
| mime = header.split(";", 1)[0] or "application/octet-stream" | ||
| return f"[media {mime} {len(rest)} b64 chars omitted]" | ||
| if len(value) <= max_chars: | ||
| return value | ||
| return value[:max_chars] + f"... [truncated {len(value) - max_chars} chars]" |
There was a problem hiding this comment.
Preserve max_chars enforcement for data: inputs too.
The data: branch returns early, so a long/invalid URI header can bypass truncation limits and inflate trace payload size. Route the media marker through the same truncation tail logic.
Suggested patch
def _truncate_text(value: str, max_chars: int) -> str:
@@
if value.startswith(_DATA_URI_PREFIX):
header, _, rest = value[len(_DATA_URI_PREFIX):].partition(",")
mime = header.split(";", 1)[0] or "application/octet-stream"
- return f"[media {mime} {len(rest)} b64 chars omitted]"
+ value = f"[media {mime} {len(rest)} b64 chars omitted]"
if len(value) <= max_chars:
return value
return value[:max_chars] + f"... [truncated {len(value) - max_chars} chars]"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/observability/langfuse/__init__.py` around lines 155 - 169, The
_truncate_text function returns early when detecting a data URI (value starting
with _DATA_URI_PREFIX), bypassing the max_chars truncation check. Instead of
returning the media marker immediately, construct the marker string and then
apply the same truncation logic used for non-data-URI values, ensuring the
marker also respects the max_chars limit. Route the generated media marker
through the existing length check and truncation logic at the end of the
function rather than returning it directly.
| try: # the dashboard process ships httpx; degrade clearly if not | ||
| import httpx | ||
| except Exception: # pragma: no cover | ||
| httpx = None # type: ignore[assignment] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify broad exception handling and replacement with specific exceptions.
rg -n -C2 'except Exception|except ImportError|RequestError|except ValueError' plugins/waba-routing/dashboard/plugin_api.pyRepository: Wizarck/hermes-agent
Length of output: 894
🏁 Script executed:
cat -n plugins/waba-routing/dashboard/plugin_api.pyRepository: Wizarck/hermes-agent
Length of output: 5244
🏁 Script executed:
# Check if httpx module is used elsewhere in the codebase and how exceptions are handled
rg -n 'httpx\.' plugins/waba-routing/ --type py | head -20Repository: Wizarck/hermes-agent
Length of output: 174
🌐 Web query:
httpx.RequestError exception class documentation
💡 Result:
In the HTTPX library, httpx.RequestError is the base class for all exceptions that occur while issuing a request (e.g., during the process of sending the request or awaiting the response) [1][2]. It is a subclass of httpx.HTTPError [1][2]. Key characteristics and context: Hierarchy: httpx.RequestError serves as the parent class for specific network-related and transport-layer errors, including TransportError (which covers timeouts, connection issues, and protocol errors), DecodingError, and TooManyRedirects [3][4][5]. Purpose: Because it is a base class, it is rarely raised directly. Instead, it is used to catch a broad category of request-processing failures in try-except blocks [6][5]. Request Context: The httpx.RequestError class has a.request property [2]. HTTPX uses a context manager called request_context to ensure that this property is attached to any RequestError that occurs within a defined block, allowing developers to identify which request triggered the exception [2][6]. Example of the hierarchy: * HTTPError * RequestError * TransportError * TimeoutException (ConnectTimeout, ReadTimeout, etc.) * NetworkError (ConnectError, ReadError, etc.) * ProtocolError * ProxyError * UnsupportedProtocol * DecodingError * TooManyRedirects * HTTPStatusError (for 4xx/5xx responses) [4][5]
Citations:
- 1: https://www.python-httpx.org/exceptions/
- 2: https://github.com/encode/httpx/blob/master/httpx/_exceptions.py
- 3: https://github.com/encode/httpx/blob/master/docs/exceptions.md
- 4: https://fossies.org/dox/httpx-0.28.1/namespacehttpx_1_1__exceptions.html
- 5: https://deepwiki.com/encode/httpx/7.1-exception-hierarchy
- 6: https://deepwiki.com/encode/httpx/7-error-handling
🌐 Web query:
FastAPI Request.json() exception type JSONDecodeError ValueError
💡 Result:
In FastAPI, calling the async method await request.json parses the request body as JSON using the standard Python json.loads function [1]. If the request body is not valid JSON, it raises a json.decoder.JSONDecodeError [2][3][4]. However, when this error occurs during FastAPI's internal request processing (such as when FastAPI automatically tries to parse the JSON body for a route handler), FastAPI catches the JSONDecodeError and raises a fastapi.exceptions.RequestValidationError instead [5][4]. This allows FastAPI to provide a standardized 422 Unprocessable Entity response rather than a generic 400 Bad Request error [5][4]. Key points for handling these exceptions: 1. Manual Handling: If you call await request.json manually within your route or a custom exception handler, you must wrap it in a try/except block to catch the json.decoder.JSONDecodeError [2][3]. 2. Error Information: FastAPI preserves the original JSONDecodeError information (including the error position and message) within the RequestValidationError if automatic parsing fails [4]. You can access these details in custom exception handlers by catching RequestValidationError [4]. 3. Avoiding "Hanging" Behavior: Be cautious when calling await request.json inside exception handlers. Since request bodies are consumed once, attempting to read the body multiple times or in incorrect contexts can lead to issues or hanging [6][7]. It is generally recommended to handle JSON parsing errors explicitly if you expect varied or malformed input [2][8]. Example of manual handling: from json import JSONDecodeError from fastapi import FastAPI, Request app = FastAPI @app.post("/") async def root(request: Request): try: payload = await request.json except JSONDecodeError: return {"error": "Invalid JSON provided"} return {"data": payload}
Citations:
- 1: https://fastapi.tiangolo.com/de/reference/request/
- 2: https://stackoverflow.com/questions/66131402/what-is-the-equivalent-fastapi-way-to-do-request-json-as-in-flask
- 3: How to log request body on ValidationError? fastapi/fastapi#1486
- 4: Preserve json.JSONDecodeError when handling invalid JSON in request body fastapi/fastapi#3993
- 5: Better JSON Decode error handling fastapi/fastapi#1354
- 6: Custom exception handler + await request.json() == hanging service fastapi/fastapi#1909
- 7: Custom exception handler + await request.json() == hanging service fastapi/fastapi#1909
- 8: Question: is it better to return an empty {} or raise JSONDecodeError for "await req.json()" Kludex/starlette#788
Narrow catch-all exceptions to preserve error visibility and prevent masking unrelated failures.
At lines 24, 48, 72, and 76, except Exception is too broad. It masks unrelated failures and hides real bugs. Catch only the specific exceptions expected from each operation:
- Line 24:
except ImportErrorfor the import guard - Line 48:
except ValueErrorforawait request.json() - Line 72:
except httpx.RequestErrorfor the httpx client request - Line 76:
except ValueErrorforresp.json()
Proposed fix
try: # the dashboard process ships httpx; degrade clearly if not
import httpx
-except Exception: # pragma: no cover
+except ImportError: # pragma: no cover
httpx = None # type: ignore[assignment]
@@
async def _read_json(request: Request) -> dict[str, Any]:
try:
data = await request.json()
return data if isinstance(data, dict) else {}
- except Exception:
+ except ValueError:
return {}
@@
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.request(method, f"{base}{path}", params=params, json=json_body, headers=headers)
- except Exception as e: # waba-mcp down / network
+ except httpx.RequestError as e: # waba-mcp down / network
return JSONResponse({"error": f"waba-mcp unreachable: {e}"}, status_code=502)
try:
data = resp.json()
- except Exception:
+ except ValueError:
data = {"error": resp.text}🧰 Tools
🪛 Ruff (0.15.17)
[warning] 24-24: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/waba-routing/dashboard/plugin_api.py` around lines 22 - 25, Replace
all four broad `except Exception` clauses with specific exception types: change
the exception handler in the httpx import guard to catch `ImportError` instead,
change the exception handler for `await request.json()` calls to catch
`ValueError` instead, change the exception handler for httpx client requests to
catch `httpx.RequestError` instead, and change the exception handler for
`resp.json()` calls to catch `ValueError` instead. This will ensure that only
expected exceptions are caught at each location, allowing unrelated failures and
real bugs to surface rather than being silently masked by overly broad exception
handling.
Source: Linters/SAST tools
| ``` | ||
| WhatsApp users | ||
| | | ||
| v Cloud API webhook | ||
| [ External WA-MCP server ] (your own; e.g. an FastAPI app you run) | ||
| | POST /wa {message_id, phone, content, ...} | ||
| | X-Webhook-Secret: <shared secret> | ||
| v | ||
| [ Hermes gateway, this platform ] | ||
| | agent reasoning (SOUL.md, memory, skills, MCP tools, ...) | ||
| v POST graph.facebook.com/{phone-number-id}/messages | ||
| [ Meta Cloud API ] -> WhatsApp users | ||
| ``` |
There was a problem hiding this comment.
Add language identifiers to fenced code blocks (MD040).
Both fences should declare a language to satisfy markdownlint (text is sufficient here).
Suggested patch
-```
+```text
WhatsApp users
@@
[ Meta Cloud API ] -> WhatsApp users@@
- +text
WhatsApp via MCP listening on 0.0.0.0:8643/wa (phone_id=)
Also applies to: 105-107
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 29-29: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@website/docs/user-guide/messaging/whatsapp-via-mcp-meta-business-api.md`
around lines 29 - 41, The markdown file contains fenced code blocks that are
missing language identifiers, which violates the markdownlint MD040 rule. Add
the language identifier "text" to both fenced code block declarations: the first
one containing the architecture diagram (WhatsApp users through Meta Cloud API)
and the second one at lines 105-107 containing the WhatsApp via MCP listening
message. Change the opening fence from triple backticks (```) to triple
backticks followed by "text" (```text) for both blocks to satisfy the linting
requirement.
Source: Linters/SAST tools
Overlay del role-router para el agente Hermes (fork wamba).
WS2 — sesión/SOUL/skills por rol
El
roleque estampa waba-mcp en el forward entrante dirigethread_id/channel_prompt/channel_skill_bindingspor rol.WS3 — banco Hindsight por rol
SessionSource.role+ contextvar_SESSION_ROLE+_effective_bank_id()→ bancohermes-{role}(activar conbank_id_template: hermes-{role}). Incluye elMessageDeduplicatordel webhook y el fix de flush-on-switch (captura síncrona del banco). 23 tests verde.Plugin waba-routing (dashboard)
Pestaña "Routing" del Profile Builder (:9119) que gestiona el registry multi-número de waba-mcp sin redeploy.
plugin_api.pyproxya la API admin de waba-mcp server-side (bearer fuera del browser; envWABA_ADMIN_URL+WABA_ADMIN_API_TOKEN);dist/index.js= tablas Líneas/Permisos/audit. Pareja del PR Wizarck/waba-mcp#4.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Improvements