Files
hermes-agent/website/docs/developer-guide/gateway-internals.md
Teknium 289cc47631 docs: resync reference, user-guide, developer-guide, and messaging pages against code (#17738)
Broad drift audit against origin/main (b52b63396).

Reference pages (most user-visible drift):
- slash-commands: add /busy, /curator, /footer, /indicator, /redraw, /steer
  that were missing; drop non-existent /terminal-setup; fix /q footnote
  (resolves to /queue, not /quit); extend CLI-only list with all 24
  CLI-only commands in the registry
- cli-commands: add dedicated sections for hermes curator / fallback /
  hooks (new subcommands not previously documented); remove stale
  hermes honcho standalone section (the plugin registers dynamically
  via hermes memory); list curator/fallback/hooks in top-level table;
  fix completion to include fish
- toolsets-reference: document the real 52-toolset count; split browser
  vs browser-cdp; add discord / discord_admin / spotify / yuanbao;
  correct hermes-cli tool count from 36 to 38; fix misleading claim
  that hermes-homeassistant adds tools (it's identical to hermes-cli)
- tools-reference: bump tool count 55 -> 68; add 7 Spotify, 5 Yuanbao,
  2 Discord toolsets; move browser_cdp/browser_dialog to their own
  browser-cdp toolset section
- environment-variables: add 40+ user-facing HERMES_* vars that were
  undocumented (--yolo, --accept-hooks, --ignore-*, inference model
  override, agent/stream/checkpoint timeouts, OAuth trace, per-platform
  batch tuning for Telegram/Discord/Matrix/Feishu/WeCom, cron knobs,
  gateway restart/connect timeouts); dedupe the Cron Scheduler section;
  replace stale QQ_SANDBOX with QQ_PORTAL_HOST

User-guide (top level):
- cli.md: compression preserves last 20 turns, not 4 (protect_last_n: 20)
- configuration.md: display.platforms is the canonical per-platform
  override key; tool_progress_overrides is deprecated and auto-migrated
- profiles.md: model.default is the config key, not model.model
- sessions.md: CLI/TUI session IDs use 6-char hex, gateway uses 8
- checkpoints-and-rollback.md: destructive-command list now matches
  _DESTRUCTIVE_PATTERNS (adds rmdir, cp, install, dd)
- docker.md: the container runs as non-root hermes (UID 10000) via
  gosu; fix install command (uv pip); add missing --insecure on the
  dashboard compose example (required for non-loopback bind)
- security.md: systemctl danger pattern also matches 'restart'
- index.md: built-in tool count 47 -> 68
- integrations/index.md: 6 STT providers, 8 memory providers
- integrations/providers.md: drop fictional dashscope/qwen aliases

Features:
- overview.md: 9 image models (not 8), 9 TTS providers (not 5),
  8 memory providers (Supermemory was missing)
- tool-gateway.md: 9 image models
- tools.md: extend common-toolsets list with search / messaging /
  spotify / discord / debugging / safe
- fallback-providers.md: add 6 real providers from PROVIDER_REGISTRY
  (lmstudio, kimi-coding-cn, stepfun, alibaba-coding-plan,
  tencent-tokenhub, azure-foundry)
- plugins.md: Available Hooks table now includes on_session_finalize,
  on_session_reset, subagent_stop
- built-in-plugins.md: add the 7 bundled plugins the page didn't
  mention (spotify, google_meet, three image_gen providers, two
  dashboard examples)
- web-dashboard.md: add --insecure and --tui flags
- cron.md: hermes cron create takes positional schedule/prompt, not
  flags

Messaging:
- telegram.md: TELEGRAM_WEBHOOK_SECRET is now REQUIRED when
  TELEGRAM_WEBHOOK_URL is set (gateway refuses to start without it
  per GHSA-3vpc-7q5r-276h). Biggest user-visible drift in the batch.
- discord.md: HERMES_DISCORD_TEXT_BATCH_SPLIT_DELAY_SECONDS default
  is 2.0, not 0.1
- dingtalk.md: document DINGTALK_REQUIRE_MENTION /
  FREE_RESPONSE_CHATS / MENTION_PATTERNS / HOME_CHANNEL /
  ALLOW_ALL_USERS that the adapter supports
- bluebubbles.md: drop fictional BLUEBUBBLES_SEND_READ_RECEIPTS env
  var; the setting lives in platforms.bluebubbles.extra only
- qqbot.md: drop dead QQ_SANDBOX; add real QQ_PORTAL_HOST and
  QQ_GROUP_ALLOWED_USERS
- wecom-callback.md: replace 'hermes gateway start' (service-only)
  with 'hermes gateway' for first-time setup

Developer-guide:
- architecture.md: refresh tool/toolset counts (61/52), terminal
  backend count (7), line counts for run_agent.py (~13.7k), cli.py
  (~11.5k), main.py (~10.4k), setup.py (~3.5k), gateway/run.py
  (~12.2k), mcp_tool.py (~3.1k); add yuanbao adapter, bump platform
  adapter count 18 -> 20
- agent-loop.md: run_agent.py line count 10.7k -> 13.7k
- tools-runtime.md: add vercel_sandbox backend
- adding-tools.md: remove stale 'Discovery import added to
  model_tools.py' checklist item (registry auto-discovery)
- adding-platform-adapters.md: mark send_typing / get_chat_info as
  concrete base methods; only connect/disconnect/send are abstract
- acp-internals.md: ACP sessions now persist to SessionDB
  (~/.hermes/state.db); acp.run_agent call uses
  use_unstable_protocol=True
- cron-internals.md: gateway runs scheduler in a dedicated background
  thread via _start_cron_ticker, not on a maintenance cycle; locking
  is cross-process via fcntl.flock (Unix) / msvcrt.locking (Windows)
- gateway-internals.md: gateway/run.py ~12k lines
- provider-runtime.md: cron DOES support fallback (run_job reads
  fallback_providers from config)
- session-storage.md: SCHEMA_VERSION = 11 (not 9); add migrations
  10 and 11 (trigram FTS, inline-mode FTS5 re-index); add
  api_call_count column to Sessions DDL; document messages_fts_trigram
  and state_meta in the architecture tree
- context-compression-and-caching.md: remove the obsolete 'context
  pressure warnings' section (warnings were removed for causing
  models to give up early)
- context-engine-plugin.md: compress() signature now includes
  focus_topic param
- extending-the-cli.md: _build_tui_layout_children signature now
  includes model_picker_widget; add to default layout

Also fixed three pre-existing broken links/anchors the build warned
about (docker.md -> api-server.md, yuanbao.md -> cron-jobs.md and
tips#background-tasks, nix-setup.md -> #container-aware-cli).

Regenerated per-skill pages via website/scripts/generate-skill-docs.py
so catalog tables and sidebar are consistent with current SKILL.md
frontmatter.

docusaurus build: clean, no broken links or anchors.
2026-04-29 20:55:59 -07:00

12 KiB

sidebar_position, title, description
sidebar_position title description
7 Gateway Internals 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 (~12,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/ Extension point for always-registered hooks (none shipped)
gateway/platforms/ Platform adapters (one per messaging platform)

Architecture Overview

┌─────────────────────────────────────────────────┐
│                  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

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:

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

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

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