Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: WhatsApp role-router overlay (WS2/WS3) + waba-routing dashboard plugin#14

Open
Wizarck wants to merge 5 commits into
mainfrom
feat/whatsapp-role-router-ws2
Open

feat: WhatsApp role-router overlay (WS2/WS3) + waba-routing dashboard plugin#14
Wizarck wants to merge 5 commits into
mainfrom
feat/whatsapp-role-router-ws2

Conversation

@Wizarck

@Wizarck Wizarck commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Overlay del role-router para el agente Hermes (fork wamba).

WS2 — sesión/SOUL/skills por rol

El role que estampa waba-mcp en el forward entrante dirige thread_id / channel_prompt / channel_skill_bindings por rol.

WS3 — banco Hindsight por rol

SessionSource.role + contextvar _SESSION_ROLE + _effective_bank_id() → banco hermes-{role} (activar con bank_id_template: hermes-{role}). Incluye el MessageDeduplicator del 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.py proxya la API admin de waba-mcp server-side (bearer fuera del browser; env WABA_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

    • Added WhatsApp integration via Meta Business API with webhook-based message forwarding.
    • Added multi-role/persona support to enable distinct conversations within a single channel.
    • Added role-aware memory bank selection for context management.
    • Added dashboard for WhatsApp routing and line management.
  • Improvements

    • Enhanced observability text truncation to better handle media data formats.

Wizarck and others added 5 commits May 6, 2026 23:20
…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]>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new WhatsAppViaMcpMetaBusinessApiAdapter that receives forwarded webhooks from an external MCP server and replies via the Meta Graph API, introduces a HERMES_SESSION_ROLE context variable for per-role/persona multiplexing, updates Hindsight memory to select role-aware banks per operation, adds a WABA routing dashboard plugin that proxies admin API calls, and fixes Langfuse truncation of base64 data URIs.

Changes

WhatsApp MCP Adapter, Role Multiplexing, and Supporting Changes

Layer / File(s) Summary
Role/session context contracts and propagation
gateway/session.py, gateway/session_context.py, gateway/platforms/base.py, gateway/run.py
Adds role field to SessionSource, introduces HERMES_SESSION_ROLE ContextVar with set/clear support, extends build_source to accept and pass through role, and wires context.source.role into _set_session_env.
Platform enum, env config, toolset, and gateway wiring
gateway/config.py, toolsets.py, gateway/run.py
Adds Platform.WHATSAPP_VIA_MCP_META_BUSINESS_API enum member, extends _apply_env_overrides to parse token/phone-number-id/webhook settings, registers the adapter in _create_adapter and authorization maps, and adds the toolset entry to hermes-gateway.
WhatsApp adapter: constructor, lifecycle, and aiohttp server
gateway/platforms/whatsapp_via_mcp_meta_business_api.py
Implements class constructor (token, phone-number-id, webhook secret, host/port/path, deduplicator), connect/disconnect lifecycle, and aiohttp app setup with /health and webhook POST routes.
WhatsApp adapter: role/tier resolution and webhook handler
gateway/platforms/whatsapp_via_mcp_meta_business_api.py, tests/gateway/test_whatsapp_via_mcp_role_router.py
Implements role allowlist computation, role extraction with precedence rules (explicit field → role: tag → bare tag → default_role), main webhook POST handler with secret auth, deduplication, MessageEvent construction for text and media, async Hermes dispatch, and tests for all role-resolution paths and deduplication.
WhatsApp adapter: outbound send, media download, and Meta API helpers
gateway/platforms/whatsapp_via_mcp_meta_business_api.py
Implements send (text chunking), send_image (public URL + caption), no-op typing indicators, _post_meta (Meta Graph API bearer POST), _build_media_event (media-id → type/extension → cache), and _download_meta_media (two-step Meta URL resolution and file write).
Hindsight memory: role-aware bank selection
plugins/memory/hindsight/__init__.py, tests/plugins/memory/test_hindsight_role_bank.py
Adds _effective_bank_id() to re-resolve bank_id_template with HERMES_SESSION_ROLE at call time; updates system_prompt_block, queue_prefetch, sync_turn, _build_retain_kwargs, tool handlers (retain/recall/reflect), and on_session_switch flush to use the effective bank; tested for template resolution, fast paths, session-switch bank capture, and session env propagation.
Langfuse data URI truncation fix
plugins/observability/langfuse/__init__.py, tests/plugins/test_langfuse_plugin.py
Adds _DATA_URI_PREFIX and modifies _truncate_text to emit a compact MIME-type placeholder for data: URIs instead of slicing raw base64 bytes; tests verify MIME extraction, fallback to application/octet-stream, and plain-text pass-through.
Agent prompt hints and user documentation
agent/prompt_builder.py, website/docs/user-guide/messaging/whatsapp-via-mcp-meta-business-api.md
Adds PLATFORM_HINTS entry for whatsapp_via_mcp_meta_business_api with plain-text and MEDIA: tag instructions, and adds the full user-facing documentation page covering architecture, env vars, forward contract, limitations, and see-also links.

WABA Routing Dashboard Plugin

Layer / File(s) Summary
WABA routing dashboard manifest and proxy API
plugins/waba-routing/dashboard/manifest.json, plugins/waba-routing/dashboard/plugin_api.py
Adds manifest.json defining the plugin tab/entry/api metadata and plugin_api.py implementing a FastAPI router with a _call proxy helper and CRUD endpoints (roles, lines, grants, audit) forwarded to the waba-mcp admin REST API with bearer auth and X-Admin-Actor header injection.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A webhook hops through Meta's gate,
Plain text only — no markdown fate!
Each role gets its own memory bank,
Data URIs get a compact frank,
The routing dashboard lights up the hall —
One fluffy PR to forward them all! 🌐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the two main features: WhatsApp role-router overlay (WS2/WS3) and the waba-routing dashboard plugin. It directly reflects the primary objectives of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/whatsapp-role-router-ws2
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/whatsapp-role-router-ws2

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (5)
gateway/platforms/whatsapp_via_mcp_meta_business_api.py (4)

639-641: ⚖️ Poor tradeoff

Synchronous 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 using asyncio.to_thread() or aiofiles for 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 value

Port parsing may raise ValueError on invalid input.

If extra.get("port") or the environment variable contains a non-numeric string, int() will raise an unhandled ValueError, 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 value

Consider 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 in disconnect()) 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 value

Inconsistent error handling between _post_meta and _download_meta_media.

_post_meta uses assert self._http_client is not None (line 511), which raises AssertionError if the adapter isn't connected. In contrast, _download_meta_media raises RuntimeError (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 win

Async 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4ba838a and b797c27.

⛔ Files ignored due to path filters (1)
  • plugins/waba-routing/dashboard/dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (16)
  • agent/prompt_builder.py
  • gateway/config.py
  • gateway/platforms/base.py
  • gateway/platforms/whatsapp_via_mcp_meta_business_api.py
  • gateway/run.py
  • gateway/session.py
  • gateway/session_context.py
  • plugins/memory/hindsight/__init__.py
  • plugins/observability/langfuse/__init__.py
  • plugins/waba-routing/dashboard/manifest.json
  • plugins/waba-routing/dashboard/plugin_api.py
  • tests/gateway/test_whatsapp_via_mcp_role_router.py
  • tests/plugins/memory/test_hindsight_role_bank.py
  • tests/plugins/test_langfuse_plugin.py
  • toolsets.py
  • website/docs/user-guide/messaging/whatsapp-via-mcp-meta-business-api.md

Comment thread gateway/config.py
Comment on lines +1597 to +1608
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"),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment thread gateway/config.py
"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")),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +186 to +220
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.py

Repository: 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 -100

Repository: 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 py

Repository: 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

Comment thread gateway/session.py
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +1306 to +1311
try:
from gateway.session_context import get_session_env

role = get_session_env("HERMES_SESSION_ROLE", "")
except Exception:
role = ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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

Comment on lines 155 to 169
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]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +22 to +25
try: # the dashboard process ships httpx; degrade clearly if not
import httpx
except Exception: # pragma: no cover
httpx = None # type: ignore[assignment]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.py

Repository: Wizarck/hermes-agent

Length of output: 894


🏁 Script executed:

cat -n plugins/waba-routing/dashboard/plugin_api.py

Repository: 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 -20

Repository: 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:


🌐 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:


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 ImportError for the import guard
  • Line 48: except ValueError for await request.json()
  • Line 72: except httpx.RequestError for the httpx client request
  • Line 76: except ValueError for resp.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

Comment on lines +29 to +41
```
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
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant