Academy → Developer GuideOfficial documentation · Arabic guidance

Gateway Internals

بنية البوابة من الداخل

Advanced9 min readLesson 113 questions✓ 2026-08-18
Before you read

What this page is, and what it holds.

This page covers Gateway Internals. You will use hermes gateway stop and hermes chat here; about 9 minutes to read. Open the channel to yourself first with an allowlist. An open channel means anyone can message your agent.

13sections
6code examples
3tables
4commands
1,569source words
The official one-line description

How the messaging gateway boots, authorizes users, routes sessions, and delivers messages

What you will be able to do

Outcomes taken from this page, not a template.

  • Understand what بوابة المراسلة is and when you need it.
  • Run hermes gateway stop and hermes chat and understand what happens next.
  • Read the table and take only the row that applies to you.
  • Set TELEGRAM_ALLOW_ALL_USERS in the right place.
Identifiers you will meet

Exactly as they appear in Hermes.

Commands
  • hermes gateway stop
  • hermes chat
  • hermes send
  • hermes gateway start
Environment variables
  • TELEGRAM_ALLOW_ALL_USERS
  • TELEGRAM_ALLOWED_USERS
  • GATEWAY_ALLOW_ALL_USERS
  • GATEWAY_KNOWN_COMMANDS
  • GATEWAY_RELAY_URL
Page map

Jump to the part you need.

  1. 01Key Files
  2. 02Architecture Overview
  3. 03Message Flow
  4. 04Authorization
  5. 05Slash Command Dispatch
  6. 06Config Sources
  7. 07Platform Adapters
  8. 08Delivery Path
  9. 09Hooks
  10. 10Memory Provider Integration
  11. 11Background Maintenance
  12. 12Process Management
  13. 13Related Docs
The full official page

Nothing summarised away.

The documentation body below is reproduced from the official source so commands and identifiers stay exact. Each section carries a short note describing what it contains.

The messaging gateway is the long-running process that connects Hermes to 20+ external messaging platforms through a unified architecture.

Key Files

A lookup table. Do not read it all; find the row that applies to you.

FilePurpose
gateway/run.pyGatewayRunner — main loop, slash commands, message dispatch (large file; check git for current LOC)
gateway/session.pySessionStore — conversation persistence and session key construction
gateway/delivery.pyOutbound message delivery to target platforms/channels
gateway/pairing.pyDM pairing flow for user authorization
gateway/channel_directory.pyMaps chat IDs to human-readable names for cron delivery
gateway/hooks.pyHook discovery, loading, and lifecycle event dispatch
gateway/mirror.pyCross-session message mirroring for send_message
gateway/status.pyToken lock management for profile-scoped gateway instances
gateway/builtin_hooks/Extension point for always-registered hooks (none shipped)
gateway/platform_registry.pyAdapter registry, factories, and deferred (lazy) loaders for bundled platform plugins
plugins/platforms/<name>/Bundled messaging adapters (most platforms: adapter.py + plugin.yaml)
gateway/platforms/Shared base.py plus legacy/direct adapters (Signal, API server, webhooks, …)

Architecture Overview

Explains the idea itself. Read it slowly; the later sections build on it.

Text21 lines
┌─────────────────────────────────────────────────┐
│                  GatewayRunner                  │
│                                                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ Telegram │  │ Discord  │  │  Slack   │       │
│  │ Adapter  │  │ Adapter  │  │ Adapter  │       │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘       │
│       │             │             │             │
│       └─────────────┼─────────────┘             │
│                     ▼                           │
│              _handle_message()                  │
│                     │                           │
│         ┌───────────┼───────────┐               │
│         ▼           ▼           ▼               │
│  Slash command   AIAgent    Queue/BG            │
│    dispatch      creation   sessions            │
│                     │                           │
│                     ▼                           │
│                 SessionStore                    │
│              (SQLite persistence)               │
└───────┴─────────────┴─────────────┴─────────────┘

Message Flow

Explains the idea itself. Read it slowly; the later sections build on it.

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:
  3. If agent is running for this session → queue message, set interrupt event
  4. If /approve, /deny, /stop → bypass guard (dispatched inline)
  5. GatewayRunner._handle_message() receives the event:
  6. Resolve session key via _session_key_for_source() (format: agent:main:{platform}:{chat_type}:{chat_id})
  7. Check authorization (see Authorization below)
  8. Check if it's a slash command → dispatch to command handler
  9. Check if agent is already running → intercept commands like /stop, /status
  10. Otherwise → create AIAgent instance and run conversation
  11. Response is sent back through the platform adapter

Session Key Format

Session keys encode the full routing context:

Text1 line
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.
  1. 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

Settings you configure once. Change one at a time so you can see what each does. Set TELEGRAM_ALLOW_ALL_USERS, TELEGRAM_ALLOWED_USERS in your environment, not in the chat.

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

Text4 lines
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

Settings you configure once. Change one at a time so you can see what each does. Set GATEWAY_KNOWN_COMMANDS in your environment, not in the chat.

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:

Python3 lines
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

Explains the idea itself. Read it slowly; the later sections build on it.

The gateway reads configuration from multiple sources:

SourceWhat it provides
~/.hermes/.envAPI keys, bot tokens, platform credentials
~/.hermes/config.yamlModel settings, tool configuration, display options
Environment variablesOverride 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

Settings you configure once. Change one at a time so you can see what each does. Commands here: hermes chat. Set GATEWAY_RELAY_URL in your environment, not in the chat.

Most messaging platforms ship as plugin adapters under plugins/platforms/<name>/adapter.py; a few legacy adapters still live directly in gateway/platforms/. All extend BasePlatformAdapter from gateway/platforms/base.py:

Text28 lines
plugins/platforms/                  # plugin-packaged adapters (one dir each)
├── telegram/adapter.py     # Telegram Bot API (long polling or webhook)
├── discord/adapter.py      # Discord bot via discord.py
├── slack/adapter.py        # Slack Socket Mode
├── whatsapp/adapter.py     # WhatsApp Business Cloud API
├── matrix/adapter.py       # Matrix via mautrix (optional E2EE)
├── mattermost/adapter.py   # Mattermost WebSocket API
├── email/adapter.py        # Email via IMAP/SMTP
├── sms/adapter.py          # SMS via Twilio
├── dingtalk/adapter.py     # DingTalk WebSocket
├── feishu/adapter.py       # Feishu/Lark WebSocket or webhook
├── wecom/adapter.py        # WeCom (WeChat Work) callback
├── line/adapter.py         # LINE Messaging API
├── teams/adapter.py        # Microsoft Teams
├── irc/adapter.py          # IRC (canonical scoped-lock example)
├── homeassistant/adapter.py # Home Assistant conversation integration
└── …                       # google_chat, ntfy, photon, raft, simplex, …

gateway/platforms/                  # core base + legacy direct adapters
├── base.py              # BasePlatformAdapter — shared logic for all platforms
├── signal.py            # Signal via signal-cli REST API
├── weixin.py            # Weixin (personal WeChat) via iLink Bot API
├── bluebubbles.py       # Apple iMessage via BlueBubbles macOS server
├── qqbot/               # QQ Bot (Tencent QQ) via Official API v2 (sub-package)
├── yuanbao.py           # Yuanbao (Tencent) DM/group adapter
├── msgraph_webhook.py   # Microsoft Graph change-notification webhook (Teams, Outlook, etc.)
├── webhook.py           # Inbound/outbound webhook adapter
└── api_server.py        # REST API server adapter

Deferred loading: Bundled kind: platform plugins register cheap register_deferred loaders in gateway/platform_registry.py (via hermes_cli/plugins.py) so platform SDKs import only when the gateway starts, delivers, or runs setup/status — not on plain hermes chat. Resolution loads one adapter on lookup; full enumeration runs pending loaders only on paths that need every platform.

Experimental connector-backed platforms use the generic relay adapter in gateway/relay/ instead of a direct platform module. When GATEWAY_RELAY_URL or gateway.relay_url is configured, the gateway registers the relay platform, dials the connector over an outbound WebSocket, and receives descriptor, inbound, and interrupt_inbound frames on that same socket. The connector advertises a CapabilityDescriptor; Hermes can send normal outbound replies, token-less follow_up operations, and interrupt frames back through the relay. The source-grounded wire contract lives in docs/relay-connector-contract.md ↗.

Adapters implement a common interface:

  • connect() / disconnect() — lifecycle management
  • send() — outbound message delivery
  • inbound events are normalized into a MessageEvent and forwarded via handle_message()

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

Commands you type in a terminal. Understand what one does before copying it. Commands here: hermes send.

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 — the send engine specifying telegram:-1001234567890, exposed via the hermes send CLI for shell scripts and via cron deliver: targets
  • 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

A lookup table. Do not read it all; find the row that applies to you.

Gateway hooks are Python modules that respond to lifecycle events:

Gateway Hook Events

EventWhen fired
gateway:startupGateway process starts
session:startNew conversation session begins
session:endSession completes or times out
session:resetUser resets session with /new
agent:startAgent begins processing a message
agent:stepAgent completes one tool-calling iteration
agent:endAgent finishes and returns response
command:*Any slash command is executed

Hooks are discovered from gateway/builtin_hooks/ (an extension point — currently empty in the shipped distribution; _register_builtin_hooks() is a no-op stub) and ~/.hermes/hooks/ (user-installed). Each hook is a directory with a HOOK.yaml manifest and handler.py.

Memory Provider Integration

Explains the idea itself. Read it slowly; the later sections build on it.

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:
Text3 lines
AIAgent._invoke_tool()
  → self._memory_manager.handle_tool_call(name, args)
    → provider.handle_tool_call(name, args)
  1. 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

Explains the idea itself. Read it slowly; the later sections build on it.

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

Commands you type in a terminal. Understand what one does before copying it. Commands here: hermes gateway stop, hermes gateway start.

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).

Knowledge check

3 questions answered by this page alone.

Every option is a real identifier from the Hermes documentation. The wrong ones are real too, just from other pages.

1. In this lesson's table, what is the “Purpose” for “gateway/builtinhooks/”?
2. Which of these environment variables actually appears in this lesson?
3. Which of these headings does not appear in this lesson?