mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
- New page user-guide/messaging/relay.md: what Relay is, enrollment (hermes gateway enroll), capability handshake (media, native approval/clarify, threads, typing), config keys, troubleshooting — derived from gateway/relay/ + gateway_enroll.py (PRs #48147 #48242 #71300 #71363 #71404 #71624 #69721). - desktop.md: artifacts viewer, timeline rail, find-in-page, multi-window, git review/worktrees, multi-terminal + persistence, theme import, rebindable shortcuts, quick entry, context-usage popover, keep-awake, command palette, Hermes Cloud mode, memory graph — all verified against apps/desktop/ source.
187 lines
10 KiB
Markdown
187 lines
10 KiB
Markdown
---
|
|
sidebar_position: 30
|
|
title: "Hermes Relay"
|
|
description: "Connect Hermes to messaging platforms through a relay connector that owns the platform credentials — enrollment, capabilities, config, and troubleshooting"
|
|
---
|
|
|
|
# Hermes Relay (Connector)
|
|
|
|
:::warning Experimental
|
|
Relay is **experimental**. The wire contract, auth scheme, and configuration
|
|
may change without a deprecation cycle while the system is being validated.
|
|
:::
|
|
|
|
Hermes Relay is not a chat platform itself — it is a **connector system** that
|
|
lets your gateway front one or more real messaging platforms (Discord,
|
|
Telegram, Slack, WhatsApp, …) **without holding any platform credentials**. A
|
|
separate service, the *connector*, owns the platform bot tokens and sockets.
|
|
Your gateway dials **out** to the connector over a single authenticated
|
|
WebSocket, receives a capability descriptor at handshake, and then exchanges
|
|
normalized message events (inbound) and actions (outbound) over that socket.
|
|
|
|
Key properties:
|
|
|
|
- **Outbound-only networking.** The gateway never opens an inbound port.
|
|
Inbound messages ride back down the same WebSocket the gateway dialed, so
|
|
relay works behind NAT and on hosts with no public IP.
|
|
- **No platform secrets on the gateway.** Bot tokens live on the connector.
|
|
Auth-gated platform media URLs are re-hosted connector-side, so platform
|
|
credentials never cross the wire.
|
|
- **Platform-agnostic.** The gateway learns what the fronted platform can do
|
|
(message length limits, markdown dialect, edit/thread/streaming support, and
|
|
the exact set of supported operations) from the handshake descriptor, not
|
|
from hardcoded platform logic.
|
|
|
|
The formal gateway ⇄ connector interface lives in the repository at
|
|
`docs/relay-connector-contract.md`.
|
|
|
|
## When to use Relay
|
|
|
|
Relay is for deployments where a hosted or shared connector service manages
|
|
the platform side — for example multi-tenant hosting where one shared bot
|
|
fronts many users' agents, or setups where you don't want bot tokens on the
|
|
gateway machine. If you run your own bots directly, use the native platform
|
|
adapters ([Telegram](/user-guide/messaging/telegram),
|
|
[Discord](/user-guide/messaging/discord), etc.) instead.
|
|
|
|
## Enrollment
|
|
|
|
A self-hosted gateway authenticates to the connector with a per-gateway
|
|
secret. `hermes gateway enroll` redeems a **single-use enrollment token**
|
|
(minted by the connector when your tenant's route is provisioned and delivered
|
|
with your gateway config) for that secret:
|
|
|
|
```bash
|
|
hermes gateway enroll \
|
|
--token <enrollment-token> \
|
|
--connector-url wss://connector.example.com/relay
|
|
```
|
|
|
|
What it does:
|
|
|
|
1. Resolves a fresh Nous Portal access token from your existing login
|
|
(`~/.hermes/auth.json`) — this proves which Nous org (tenant) you own. If
|
|
`gateway.idp.token_url` is configured, a generic OAuth2 client-credentials
|
|
token from your own IdP is used instead (the air-gapped / self-hosted-IdP
|
|
path, no Nous Portal involved).
|
|
2. POSTs the enrollment token and a gateway id to the connector's
|
|
`/relay/enroll` endpoint over TLS.
|
|
3. The connector verifies the token (signature, single-use, tenant match),
|
|
mints a per-gateway secret plus a per-tenant delivery key, and returns them
|
|
once.
|
|
4. Persists the credentials into `~/.hermes/.env`:
|
|
`GATEWAY_RELAY_ID`, `GATEWAY_RELAY_SECRET`, `GATEWAY_RELAY_DELIVERY_KEY`
|
|
(plus `GATEWAY_RELAY_URL` / `GATEWAY_RELAY_WAKE_URL` when supplied).
|
|
|
|
Restart the gateway afterwards to pick up the new environment.
|
|
|
|
Flags:
|
|
|
|
| Flag | Description |
|
|
|------|-------------|
|
|
| `--token` | The single-use enrollment token. Also settable via `GATEWAY_RELAY_ENROLL_TOKEN`. |
|
|
| `--connector-url` | Connector base or relay URL (`wss://…/relay` or `https://…`). Also settable via `GATEWAY_RELAY_URL` or `gateway.relay_url` in `config.yaml`. |
|
|
| `--gateway-id` | Stable id for this gateway instance (used for kill-switch granularity). Defaults to `gw-<hostname>`. |
|
|
| `--wake-url` | Optional reachable URL the connector pokes (payload-free GET) to wake this gateway when buffered work arrives while it is idle. Persisted as `GATEWAY_RELAY_WAKE_URL`. Without it the gateway still drains buffered messages whenever it next reconnects. |
|
|
|
|
:::note Managed installs
|
|
`hermes gateway enroll` refuses to run in managed/hosted installs — there the
|
|
hosting platform provisions the relay secret directly into the container
|
|
environment.
|
|
:::
|
|
|
|
## Configuration
|
|
|
|
Relay activates when a connector relay URL is configured — there is no
|
|
separate feature flag. Deployments that don't set it are unaffected.
|
|
|
|
| Setting | Where | Meaning |
|
|
|---------|-------|---------|
|
|
| `GATEWAY_RELAY_URL` | env (`~/.hermes/.env`) | Connector relay WebSocket URL. Presence enables the relay platform. |
|
|
| `gateway.relay_url` | `config.yaml` | Same as above, config-file form (env takes precedence). |
|
|
| `GATEWAY_RELAY_ID` | env | This gateway instance's id (written by `enroll`). |
|
|
| `GATEWAY_RELAY_SECRET` | env | Per-gateway secret authenticating the WebSocket upgrade (written by `enroll`). |
|
|
| `GATEWAY_RELAY_DELIVERY_KEY` | env | Per-tenant delivery key (written by `enroll`; retained for forward-compat). |
|
|
| `GATEWAY_RELAY_WAKE_URL` / `gateway.relay_wake_url` | env / `config.yaml` | Optional wake-poke target for idle/suspended gateways. |
|
|
| `GATEWAY_RELAY_PLATFORMS` | env | Comma-separated list of platforms this gateway fronts over one connection (e.g. `discord,telegram`). Usually stamped by the deployment/orchestrator. |
|
|
| `GATEWAY_RELAY_BOT_IDS` | env | JSON map of per-platform bot identities, e.g. `{"discord": {"botId": "…"}}`. Paired with `GATEWAY_RELAY_PLATFORMS`. |
|
|
| `gateway.idp.token_url` | `config.yaml` | When set, enrollment/provisioning authenticates via generic OAuth2 client-credentials against your own IdP instead of Nous Portal. |
|
|
|
|
## Supported capabilities
|
|
|
|
What actually works over a relay connection is negotiated at handshake: the
|
|
connector advertises a `supported_ops` list, and the gateway only uses an
|
|
operation the connector explicitly advertises (older connectors fall back to a
|
|
legacy `send`/`edit`/`typing`/`follow_up` set). Per-platform capability flags
|
|
(edit-based streaming, threads, draft streaming, markdown dialect, message
|
|
length limit) also come from the handshake descriptor. Subject to that
|
|
negotiation, the relay supports:
|
|
|
|
- **Text messages and streaming** — sends, replies, and progressive
|
|
edit-based streaming when the fronted platform supports message editing;
|
|
otherwise output degrades to one message per segment.
|
|
- **Media, both directions** — outbound images, voice, audio, video, and
|
|
documents are uploaded to the connector (or referenced by public URL) and
|
|
delivered through each platform's native upload lane, with captions.
|
|
Inbound attachments are localized to files for the agent; auth-gated
|
|
platform URLs are re-hosted connector-side so platform credentials never
|
|
reach the gateway. Media re-hosts are capped at 25 MB and expire (~1 hour).
|
|
- **Native interactive prompts** — exec approvals, confirmations, and clarify
|
|
questions render with **native platform controls** (Discord buttons,
|
|
Telegram inline keyboards, Slack Block Kit actions, WhatsApp button/list
|
|
messages) instead of numbered-text fallbacks. Button presses come back as
|
|
authenticated prompt responses from the actual clicking user, so the
|
|
gateway's authorization gates apply exactly as to a typed reply. Prompt
|
|
expiry is enforced gateway-side.
|
|
- **Reaction ack lifecycle** — the bot's processing-status reactions
|
|
(👀 while working, ✅/❌ on completion) work over the relay. Reactions are
|
|
best-effort: a failed reaction never fails a turn.
|
|
- **Thread lifecycle** — creating handoff threads and renaming threads
|
|
(including LLM-titled semantic renames) through platform-abstract
|
|
`thread_create` / `thread_rename` operations, with a no-clobber guard so a
|
|
human's manual rename wins. Availability depends on the platform (e.g.
|
|
Slack threads can't be renamed; WhatsApp has no threads).
|
|
- **Typing indicators** — the gateway egresses typing (and stop-typing) through
|
|
the connector while processing.
|
|
- **Chat metadata** — `get_chat_info` lookups are proxied to the connector
|
|
when advertised.
|
|
- **Buffered delivery and wake** — when the gateway goes idle or disconnects,
|
|
the connector buffers inbound messages durably and replays them in order on
|
|
reconnect (ack-gated, no loss or duplication). If a wake URL is registered,
|
|
the connector pokes it when buffered work arrives for a sleeping gateway.
|
|
|
|
Multi-platform fronting is supported: one gateway can front several platforms
|
|
(e.g. Discord *and* Telegram) over a single relay connection, with each
|
|
outbound message tagged for the platform it targets.
|
|
|
|
## Troubleshooting
|
|
|
|
**Enrollment fails with 401** — the connector could not verify your identity
|
|
token. Re-login with `hermes auth add nous` (or `hermes setup`) and retry.
|
|
|
|
**Enrollment fails with 403** — the enrollment token is invalid, expired,
|
|
already used, or belongs to a different tenant. Enrollment tokens are
|
|
single-use; request a fresh one from whoever provisioned your tenant route.
|
|
|
|
**"Could not reach the connector"** — check the connector URL. You can paste
|
|
either the `wss://…/relay` dial URL or the `https://…` base URL; the CLI maps
|
|
between them automatically.
|
|
|
|
**`enroll` refuses to run** — you are in a managed/hosted install, where the
|
|
relay secret is provisioned by the hosting platform. Self-enrollment is only
|
|
for self-hosted gateways.
|
|
|
|
**Relay platform shows as disabled after it previously worked** — a WebSocket
|
|
close with code 4401 *after* a successful handshake means the gateway's secret
|
|
was revoked (e.g. the instance was deprovisioned). The gateway deliberately
|
|
stops reconnecting and reports relay as disabled rather than retrying. A 4401
|
|
*before* any successful handshake is treated as a transient
|
|
not-yet-provisioned race and retried normally.
|
|
|
|
**Nothing changed after enrolling** — the gateway reads `GATEWAY_RELAY_*` at
|
|
startup. Restart it (`hermes gateway restart`).
|
|
|
|
**A feature (buttons, media, threads…) silently degrades to plain text** — the
|
|
connector for your platform did not advertise that operation in its handshake
|
|
`supported_ops`. The gateway intentionally falls back to the text behavior
|
|
rather than sending an op the connector can't handle.
|