| --- |
| sidebar_position: 7 |
| title: "Gateway Internals" |
| description: "How the messaging gateway boots, authorizes users, routes sessions, and delivers messages" |
| --- |
| |
| # Gateway Internals |
|
|
| The messaging gateway is the long-running process that connects Hermes to 14+ external messaging platforms through a unified architecture. |
|
|
| ## Key Files |
|
|
| | File | Purpose | |
| |------|---------| |
| | `gateway/run.py` | `GatewayRunner` β main loop, slash commands, message dispatch (~9,000 lines) | |
| | `gateway/session.py` | `SessionStore` β conversation persistence and session key construction | |
| | `gateway/delivery.py` | Outbound message delivery to target platforms/channels | |
| | `gateway/pairing.py` | DM pairing flow for user authorization | |
| | `gateway/channel_directory.py` | Maps chat IDs to human-readable names for cron delivery | |
| | `gateway/hooks.py` | Hook discovery, loading, and lifecycle event dispatch | |
| | `gateway/mirror.py` | Cross-session message mirroring for `send_message` | |
| | `gateway/status.py` | Token lock management for profile-scoped gateway instances | |
| | `gateway/builtin_hooks/` | Always-registered hooks (e.g., BOOT.md system prompt hook) | |
| | `gateway/platforms/` | Platform adapters (one per messaging platform) | |
|
|
| ## Architecture Overview |
|
|
| ```text |
| βββββββββββββββββββββββββββββββββββββββββββββββββββ |
| β GatewayRunner β |
| β β |
| β ββββββββββββ ββββββββββββ ββββββββββββ β |
| β β Telegram β β Discord β β Slack β β |
| β β Adapter β β Adapter β β Adapter β β |
| β ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ β |
| β β β β β |
| β βββββββββββββββΌββββββββββββββ β |
| β βΌ β |
| β _handle_message() β |
| β β β |
| β βββββββββββββΌββββββββββββ β |
| β βΌ βΌ βΌ β |
| β Slash command AIAgent Queue/BG β |
| β dispatch creation sessions β |
| β β β |
| β βΌ β |
| β SessionStore β |
| β (SQLite persistence) β |
| βββββββββββββββββββββββββββββββββββββββββββββββββββ |
| ``` |
|
|
| ## Message Flow |
|
|
| When a message arrives from any platform: |
|
|
| 1. **Platform adapter** receives raw event, normalizes it into a `MessageEvent` |
| 2. **Base adapter** checks active session guard: |
| - If agent is running for this session β queue message, set interrupt event |
| - If `/approve`, `/deny`, `/stop` β bypass guard (dispatched inline) |
| 3. **GatewayRunner._handle_message()** receives the event: |
| - Resolve session key via `_session_key_for_source()` (format: `agent:main:{platform}:{chat_type}:{chat_id}`) |
| - Check authorization (see Authorization below) |
| - Check if it's a slash command β dispatch to command handler |
| - Check if agent is already running β intercept commands like `/stop`, `/status` |
| - Otherwise β create `AIAgent` instance and run conversation |
| 4. **Response** is sent back through the platform adapter |
|
|
| ### Session Key Format |
|
|
| Session keys encode the full routing context: |
|
|
| ``` |
| agent:main:{platform}:{chat_type}:{chat_id} |
| ``` |
|
|
| For example: `agent:main:telegram:private:123456789` |
|
|
| Thread-aware platforms (Telegram forum topics, Discord threads, Slack threads) may include thread IDs in the chat_id portion. **Never construct session keys manually** β always use `build_session_key()` from `gateway/session.py`. |
| |
| ### Two-Level Message Guard |
| |
| When an agent is actively running, incoming messages pass through two sequential guards: |
| |
| 1. **Level 1 β Base adapter** (`gateway/platforms/base.py`): Checks `_active_sessions`. If the session is active, queues the message in `_pending_messages` and sets an interrupt event. This catches messages *before* they reach the gateway runner. |
| |
| 2. **Level 2 β Gateway runner** (`gateway/run.py`): Checks `_running_agents`. Intercepts specific commands (`/stop`, `/new`, `/queue`, `/status`, `/approve`, `/deny`) and routes them appropriately. Everything else triggers `running_agent.interrupt()`. |
|
|
| Commands that must reach the runner while the agent is blocked (like `/approve`) are dispatched **inline** via `await self._message_handler(event)` β they bypass the background task system to avoid race conditions. |
|
|
| ## Authorization |
|
|
| The gateway uses a multi-layer authorization check, evaluated in order: |
|
|
| 1. **Per-platform allow-all flag** (e.g., `TELEGRAM_ALLOW_ALL_USERS`) β if set, all users on that platform are authorized |
| 2. **Platform allowlist** (e.g., `TELEGRAM_ALLOWED_USERS`) β comma-separated user IDs |
| 3. **DM pairing** β authenticated users can pair new users via a pairing code |
| 4. **Global allow-all** (`GATEWAY_ALLOW_ALL_USERS`) β if set, all users across all platforms are authorized |
| 5. **Default: deny** β unauthorized users are rejected |
|
|
| ### DM Pairing Flow |
|
|
| ```text |
| Admin: /pair |
| Gateway: "Pairing code: ABC123. Share with the user." |
| New user: ABC123 |
| Gateway: "Paired! You're now authorized." |
| ``` |
|
|
| Pairing state is persisted in `gateway/pairing.py` and survives restarts. |
|
|
| ## Slash Command Dispatch |
|
|
| All slash commands in the gateway flow through the same resolution pipeline: |
|
|
| 1. `resolve_command()` from `hermes_cli/commands.py` maps input to canonical name (handles aliases, prefix matching) |
| 2. The canonical name is checked against `GATEWAY_KNOWN_COMMANDS` |
| 3. Handler in `_handle_message()` dispatches based on canonical name |
| 4. Some commands are gated on config (`gateway_config_gate` on `CommandDef`) |
|
|
| ### Running-Agent Guard |
|
|
| Commands that must NOT execute while the agent is processing are rejected early: |
|
|
| ```python |
| if _quick_key in self._running_agents: |
| if canonical == "model": |
| return "β³ Agent is running β wait for it to finish or /stop first." |
| ``` |
|
|
| Bypass commands (`/stop`, `/new`, `/approve`, `/deny`, `/queue`, `/status`) have special handling. |
|
|
| ## Config Sources |
|
|
| The gateway reads configuration from multiple sources: |
|
|
| | Source | What it provides | |
| |--------|-----------------| |
| | `~/.hermes/.env` | API keys, bot tokens, platform credentials | |
| | `~/.hermes/config.yaml` | Model settings, tool configuration, display options | |
| | Environment variables | Override any of the above | |
|
|
| Unlike the CLI (which uses `load_cli_config()` with hardcoded defaults), the gateway reads `config.yaml` directly via YAML loader. This means config keys that exist in the CLI's defaults dict but not in the user's config file may behave differently between CLI and gateway. |
|
|
| ## Platform Adapters |
|
|
| Each messaging platform has an adapter in `gateway/platforms/`: |
|
|
| ```text |
| gateway/platforms/ |
| βββ base.py # BaseAdapter β shared logic for all platforms |
| βββ telegram.py # Telegram Bot API (long polling or webhook) |
| βββ discord.py # Discord bot via discord.py |
| βββ slack.py # Slack Socket Mode |
| βββ whatsapp.py # WhatsApp Business Cloud API |
| βββ signal.py # Signal via signal-cli REST API |
| βββ matrix.py # Matrix via mautrix (optional E2EE) |
| βββ mattermost.py # Mattermost WebSocket API |
| βββ email.py # Email via IMAP/SMTP |
| βββ sms.py # SMS via Twilio |
| βββ dingtalk.py # DingTalk WebSocket |
| βββ feishu.py # Feishu/Lark WebSocket or webhook |
| βββ wecom.py # WeCom (WeChat Work) callback |
| βββ weixin.py # Weixin (personal WeChat) via iLink Bot API |
| βββ bluebubbles.py # Apple iMessage via BlueBubbles macOS server |
| βββ qqbot.py # QQ Bot (Tencent QQ) via Official API v2 |
| βββ webhook.py # Inbound/outbound webhook adapter |
| βββ api_server.py # REST API server adapter |
| βββ homeassistant.py # Home Assistant conversation integration |
| ``` |
|
|
| Adapters implement a common interface: |
| - `connect()` / `disconnect()` β lifecycle management |
| - `send_message()` β outbound message delivery |
| - `on_message()` β inbound message normalization β `MessageEvent` |
|
|
| ### Token Locks |
|
|
| Adapters that connect with unique credentials call `acquire_scoped_lock()` in `connect()` and `release_scoped_lock()` in `disconnect()`. This prevents two profiles from using the same bot token simultaneously. |
|
|
| ## Delivery Path |
|
|
| Outgoing deliveries (`gateway/delivery.py`) handle: |
|
|
| - **Direct reply** β send response back to the originating chat |
| - **Home channel delivery** β route cron job outputs and background results to a configured home channel |
| - **Explicit target delivery** β `send_message` tool specifying `telegram:-1001234567890` |
| - **Cross-platform delivery** β deliver to a different platform than the originating message |
|
|
| Cron job deliveries are NOT mirrored into gateway session history β they live in their own cron session only. This is a deliberate design choice to avoid message alternation violations. |
|
|
| ## Hooks |
|
|
| Gateway hooks are Python modules that respond to lifecycle events: |
|
|
| ### Gateway Hook Events |
|
|
| | Event | When fired | |
| |-------|-----------| |
| | `gateway:startup` | Gateway process starts | |
| | `session:start` | New conversation session begins | |
| | `session:end` | Session completes or times out | |
| | `session:reset` | User resets session with `/new` | |
| | `agent:start` | Agent begins processing a message | |
| | `agent:step` | Agent completes one tool-calling iteration | |
| | `agent:end` | Agent finishes and returns response | |
| | `command:*` | Any slash command is executed | |
|
|
| Hooks are discovered from `gateway/builtin_hooks/` (always active) and `~/.hermes/hooks/` (user-installed). Each hook is a directory with a `HOOK.yaml` manifest and `handler.py`. |
|
|
| ## Memory Provider Integration |
|
|
| When a memory provider plugin (e.g., Honcho) is enabled: |
|
|
| 1. Gateway creates an `AIAgent` per message with the session ID |
| 2. The `MemoryManager` initializes the provider with the session context |
| 3. Provider tools (e.g., `honcho_profile`, `viking_search`) are routed through: |
|
|
| ```text |
| AIAgent._invoke_tool() |
| β self._memory_manager.handle_tool_call(name, args) |
| β provider.handle_tool_call(name, args) |
| ``` |
|
|
| 4. On session end/reset, `on_session_end()` fires for cleanup and final data flush |
|
|
| ### Memory Flush Lifecycle |
|
|
| When a session is reset, resumed, or expires: |
| 1. Built-in memories are flushed to disk |
| 2. Memory provider's `on_session_end()` hook fires |
| 3. A temporary `AIAgent` runs a memory-only conversation turn |
| 4. Context is then discarded or archived |
|
|
| ## Background Maintenance |
|
|
| The gateway runs periodic maintenance alongside message handling: |
|
|
| - **Cron ticking** β checks job schedules and fires due jobs |
| - **Session expiry** β cleans up abandoned sessions after timeout |
| - **Memory flush** β proactively flushes memory before session expiry |
| - **Cache refresh** β refreshes model lists and provider status |
|
|
| ## Process Management |
|
|
| The gateway runs as a long-lived process, managed via: |
|
|
| - `hermes gateway start` / `hermes gateway stop` β manual control |
| - `systemctl` (Linux) or `launchctl` (macOS) β service management |
| - PID file at `~/.hermes/gateway.pid` β profile-scoped process tracking |
|
|
| **Profile-scoped vs global**: `start_gateway()` uses profile-scoped PID files. `hermes gateway stop` stops only the current profile's gateway. `hermes gateway stop --all` uses global `ps aux` scanning to kill all gateway processes (used during updates). |
|
|
| ## Related Docs |
|
|
| - [Session Storage](./session-storage.md) |
| - [Cron Internals](./cron-internals.md) |
| - [ACP Internals](./acp-internals.md) |
| - [Agent Loop Internals](./agent-loop.md) |
| - [Messaging Gateway (User Guide)](/docs/user-guide/messaging) |
|
|