1 Comm Gateway
jfabian edited this page 2026-08-13 11:15:47 -03:00

Comm Gateway (comm.gateway)

comm.gateway is the multi-channel communication capability. It provides inbound webhook ingestion and outbound message delivery through a unified adapter abstraction, so channel-specific behavior (Telegram, WhatsApp, generic HTTP webhooks) lives in adapters — never in the gateway core.

Source: vox/capabilities/comm/gateway/

Design

The gateway is deliberately domain-agnostic:

  • capability.py — CommGatewayCapability; aggregates adapter params, manages the shared server lifecycle, and exposes outbound send.
  • server.py — IngressServer, the shared aiohttp webhook listener.
  • adapters/base.py — BaseAdapter, the channel contract.
  • adapters/*.py — one adapter per channel.
  • adapters/__init__.py — ADAPTER_REGISTRY, maps channel name → adapter class.

Adding a channel is zero-change to capability.py or server.py: write a BaseAdapter subclass and register it in ADAPTER_REGISTRY. The server mounts routes generically from each adapter's WEBHOOK_PATH.

Shared server semantics

The IngressServer is a singleton resource shared across all agents that mount comm.gateway. The first agent to boot creates the server; subsequent agents attach to the existing instance. A reference count (_mounted_agents_count) tracks how many agents have the capability booted — the server is stopped only when the last agent shuts down, so one agent's stop never kills inbound traffic for the rest.

The adapter contract (BaseAdapter)

Member Purpose
CHANNEL Channel identifier (e.g. "telegram", "whatsapp", "webhook")
WEBHOOK_PATH HTTP path served by the shared server (e.g. /webhook/whatsapp)
PARAMS Channel-scoped {param: [description, default]}
SENSITIVE_PARAMS Credential params injected via vault
parse_inbound(raw) Normalize a channel-native payload → VOXInboundMessage
send_outbound(message) Translate a VOXOutboundMessage → channel API call
verify_request(request, body) Authenticate the webhook; body is the raw request bytes read before JSON parsing (required by channels that sign the payload, e.g. Meta)
handle_verification(query) Optional GET handshake (e.g. Meta's hub.challenge); return None to reject
is_configured(config) Whether the channel should be mounted for the given config

PARAMS/SENSITIVE_PARAMS are aggregated into the capability at runtime — the gateway never hardcodes a channel's keys.

Channels

Telegram (telegram)

  • WEBHOOK_PATH: /webhook/telegram
  • Inbound messages normalized from Bot API updates; outbound via sendMessage.
  • Requires TELEGRAM_BOT_TOKEN (+ TELEGRAM_WEBHOOK_SECRET when using signed webhooks, TELEGRAM_LONG_TIMEOUT for long-polling transport).

WhatsApp (whatsapp)

  • WEBHOOK_PATH: /webhook/whatsapp
  • Inbound messages[] (text/image/audio/document/video) and statuses[] normalized into VOXInboundMessage; outbound via POST /{version}/{phone_number_id}/messages (Graph API).
  • Body-signing: verifies Meta's X-Hub-Signature-256 HMAC-SHA256 over the raw payload bytes; hub.challenge GET handshake for subscription verification.
  • Requires WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_APP_SECRET, WHATSAPP_VERIFY_TOKEN, WHATSAPP_API_VERSION. Activation requires the access token and phone number ID to be set ("" = unconfigured, channel inactive).
  • See WhatsApp Setup.

Generic HTTP webhook (webhook)

  • WEBHOOK_PATH: /webhook/generic
  • Adapter for a plain HTTP webhook with a shared GATEWAY_WEBHOOK_SECRET.

Inbound flow

webhook POST
  → IngressServer reads raw body bytes
  → adapter.verify_request(request, body)   (403 on failure)
  → adapter.parse_inbound(raw)              (422 on parse failure)
  → capability.dispatch(inbound)            (SecurityError → guardrail)
  → orchestrator.dispatch_inbound_message(source="comm.gateway", payload=...)

Outbound

# Through the bound capability on the agent:
await self.agent.capabilities["comm.gateway"].send_text(
    "telegram", recipient_id, "Hello", parse_mode="HTML"
)

# Or build a full VOXOutboundMessage and send_message(msg).

There is no send_broadcast() — outbound calls pass the channel and recipient explicitly to send_text(channel, recipient_id, text, ...).