Hermes-agent
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Messaging Gateway",
|
||||
"position": 3,
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "user-guide/messaging/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
# BlueBubbles (iMessage)
|
||||
|
||||
Connect Hermes to Apple iMessage via [BlueBubbles](https://bluebubbles.app/) — a free, open-source macOS server that bridges iMessage to any device.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A **Mac** (always on) running [BlueBubbles Server](https://bluebubbles.app/)
|
||||
- Apple ID signed into Messages.app on that Mac
|
||||
- BlueBubbles Server v1.0.0+ (webhooks require this version)
|
||||
- Network connectivity between Hermes and the BlueBubbles server
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install BlueBubbles Server
|
||||
|
||||
Download and install from [bluebubbles.app](https://bluebubbles.app/). Complete the setup wizard — sign in with your Apple ID and configure a connection method (local network, Ngrok, Cloudflare, or Dynamic DNS).
|
||||
|
||||
### 2. Get your Server URL and Password
|
||||
|
||||
In BlueBubbles Server → **Settings → API**, note:
|
||||
- **Server URL** (e.g., `http://192.168.1.10:1234`)
|
||||
- **Server Password**
|
||||
|
||||
### 3. Configure Hermes
|
||||
|
||||
Run the setup wizard:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **BlueBubbles (iMessage)** and enter your server URL and password.
|
||||
|
||||
Or set environment variables directly in `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
BLUEBUBBLES_SERVER_URL=http://192.168.1.10:1234
|
||||
BLUEBUBBLES_PASSWORD=your-server-password
|
||||
```
|
||||
|
||||
#### Optional: Require mentions in group chats
|
||||
|
||||
By default, Hermes responds to every authorized BlueBubbles/iMessage DM or group message. To make group chats opt-in, enable mention gating:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
bluebubbles:
|
||||
enabled: true
|
||||
extra:
|
||||
require_mention: true
|
||||
```
|
||||
|
||||
With `require_mention: true`, DMs still work normally, but group-chat messages are ignored unless they match a mention pattern. If you do not configure custom patterns, Hermes uses conservative defaults for `Hermes` and `@Hermes agent` variants.
|
||||
|
||||
For a custom agent name, set regex patterns:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
bluebubbles:
|
||||
extra:
|
||||
require_mention: true
|
||||
mention_patterns:
|
||||
- '(?<![\w@])@?amos\b[,:\-]?'
|
||||
```
|
||||
|
||||
### 4. Authorize Users
|
||||
|
||||
Choose one approach:
|
||||
|
||||
**DM Pairing (recommended):**
|
||||
When someone messages your iMessage, Hermes automatically sends them a pairing code. Approve it with:
|
||||
```bash
|
||||
hermes pairing approve bluebubbles <CODE>
|
||||
```
|
||||
Use `hermes pairing list` to see pending codes and approved users.
|
||||
|
||||
**Pre-authorize specific users** (in `~/.hermes/.env`):
|
||||
```bash
|
||||
BLUEBUBBLES_ALLOWED_USERS=user@icloud.com,+15551234567
|
||||
```
|
||||
|
||||
**Open access** (in `~/.hermes/.env`):
|
||||
```bash
|
||||
BLUEBUBBLES_ALLOW_ALL_USERS=true
|
||||
```
|
||||
|
||||
### 5. Start the Gateway
|
||||
|
||||
```bash
|
||||
hermes gateway run
|
||||
```
|
||||
|
||||
Hermes will connect to your BlueBubbles server, register a webhook, and start listening for iMessage messages.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
iMessage → Messages.app → BlueBubbles Server → Webhook → Hermes
|
||||
Hermes → BlueBubbles REST API → Messages.app → iMessage
|
||||
```
|
||||
|
||||
- **Inbound:** BlueBubbles sends webhook events to a local listener when new messages arrive. No polling — instant delivery.
|
||||
- **Outbound:** Hermes sends messages via the BlueBubbles REST API.
|
||||
- **Media:** Images, voice messages, videos, and documents are supported in both directions. Inbound attachments are downloaded and cached locally for the agent to process.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `BLUEBUBBLES_SERVER_URL` | Yes | — | BlueBubbles server URL |
|
||||
| `BLUEBUBBLES_PASSWORD` | Yes | — | Server password |
|
||||
| `BLUEBUBBLES_WEBHOOK_HOST` | No | `127.0.0.1` | Webhook listener bind address |
|
||||
| `BLUEBUBBLES_WEBHOOK_PORT` | No | `8645` | Webhook listener port |
|
||||
| `BLUEBUBBLES_WEBHOOK_PATH` | No | `/bluebubbles-webhook` | Webhook URL path |
|
||||
| `BLUEBUBBLES_HOME_CHANNEL` | No | — | Phone/email for cron delivery |
|
||||
| `BLUEBUBBLES_ALLOWED_USERS` | No | — | Comma-separated authorized users |
|
||||
| `BLUEBUBBLES_ALLOW_ALL_USERS` | No | `false` | Allow all users |
|
||||
| `BLUEBUBBLES_REQUIRE_MENTION` | No | `false` | Require a mention pattern before responding in group chats |
|
||||
| `BLUEBUBBLES_MENTION_PATTERNS` | No | Hermes wake words | JSON array, newline-separated, or comma-separated regex patterns for group mention matching |
|
||||
|
||||
Auto-marking messages as read is controlled by the `send_read_receipts` key under `platforms.bluebubbles.extra` in `~/.hermes/config.yaml` (default: `true`). There is no corresponding environment variable.
|
||||
|
||||
## Features
|
||||
|
||||
### Text Messaging
|
||||
Send and receive iMessages. Markdown is automatically stripped for clean plain-text delivery.
|
||||
|
||||
### Rich Media
|
||||
- **Images:** Photos appear natively in the iMessage conversation
|
||||
- **Voice messages:** Audio files sent as iMessage voice messages
|
||||
- **Videos:** Video attachments
|
||||
- **Documents:** Files sent as iMessage attachments
|
||||
|
||||
### Tapback Reactions
|
||||
Love, like, dislike, laugh, emphasize, and question reactions. Requires the BlueBubbles [Private API helper](https://docs.bluebubbles.app/helper-bundle/installation).
|
||||
|
||||
### Typing Indicators
|
||||
Shows "typing..." in the iMessage conversation while the agent is processing. Requires Private API.
|
||||
|
||||
### Read Receipts
|
||||
Automatically marks messages as read after processing. Requires Private API.
|
||||
|
||||
### Chat Addressing
|
||||
You can address chats by email or phone number — Hermes resolves them to BlueBubbles chat GUIDs automatically. No need to use raw GUID format.
|
||||
|
||||
## Private API
|
||||
|
||||
Some features require the BlueBubbles [Private API helper](https://docs.bluebubbles.app/helper-bundle/installation):
|
||||
- Tapback reactions
|
||||
- Typing indicators
|
||||
- Read receipts
|
||||
- Creating new chats by address
|
||||
|
||||
Without the Private API, basic text messaging and media still work.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Cannot reach server"
|
||||
- Verify the server URL is correct and the Mac is on
|
||||
- Check that BlueBubbles Server is running
|
||||
- Ensure network connectivity (firewall, port forwarding)
|
||||
|
||||
### Messages not arriving
|
||||
- Check that the webhook is registered in BlueBubbles Server → Settings → API → Webhooks
|
||||
- Verify the webhook URL is reachable from the Mac
|
||||
- Check `hermes logs gateway` for webhook errors (or `hermes logs -f` to follow in real-time)
|
||||
|
||||
### "Private API helper not connected"
|
||||
- Install the Private API helper: [docs.bluebubbles.app](https://docs.bluebubbles.app/helper-bundle/installation)
|
||||
- Basic messaging works without it — only reactions, typing, and read receipts require it
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
---
|
||||
sidebar_position: 10
|
||||
title: "DingTalk"
|
||||
description: "Set up Hermes Agent as a DingTalk chatbot"
|
||||
---
|
||||
|
||||
# DingTalk Setup
|
||||
|
||||
Hermes Agent integrates with DingTalk (钉钉) as a chatbot, letting you chat with your AI assistant through direct messages or group chats. The bot connects via DingTalk's Stream Mode — a long-lived WebSocket connection that requires no public URL or webhook server — and replies using markdown-formatted messages through DingTalk's session webhook API.
|
||||
|
||||
Before setup, here's the part most people want to know: how Hermes behaves once it's in your DingTalk workspace.
|
||||
|
||||
## How Hermes Behaves
|
||||
|
||||
| Context | Behavior |
|
||||
|---------|----------|
|
||||
| **DMs (1:1 chat)** | Hermes responds to every message. No `@mention` needed. Each DM has its own session. |
|
||||
| **Group chats** | Hermes responds when you `@mention` it. Without a mention, Hermes ignores the message. |
|
||||
| **Shared groups with multiple users** | By default, Hermes isolates session history per user inside the group. Two people talking in the same group do not share one transcript unless you explicitly disable that. |
|
||||
|
||||
### Session Model in DingTalk
|
||||
|
||||
By default:
|
||||
|
||||
- each DM gets its own session
|
||||
- each user in a shared group chat gets their own session inside that group
|
||||
|
||||
This is controlled by `config.yaml`:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
Set it to `false` only if you explicitly want one shared conversation for the entire group:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: false
|
||||
```
|
||||
|
||||
This guide walks you through the full setup process — from creating your DingTalk bot to sending your first message.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install the required Python packages:
|
||||
|
||||
```bash
|
||||
pip install "hermes-agent[dingtalk]"
|
||||
```
|
||||
|
||||
Or individually:
|
||||
|
||||
```bash
|
||||
pip install dingtalk-stream httpx alibabacloud-dingtalk
|
||||
```
|
||||
|
||||
- `dingtalk-stream` — DingTalk's official SDK for Stream Mode (WebSocket-based real-time messaging)
|
||||
- `httpx` — async HTTP client used for sending replies via session webhooks
|
||||
- `alibabacloud-dingtalk` — DingTalk OpenAPI SDK for AI Cards, emoji reactions, and media downloads
|
||||
|
||||
## Step 1: Create a DingTalk App
|
||||
|
||||
1. Go to the [DingTalk Developer Console](https://open-dev.dingtalk.com/).
|
||||
2. Log in with your DingTalk admin account.
|
||||
3. Click **Application Development** → **Custom Apps** → **Create App via H5 Micro-App** (or **Robot** depending on your console version).
|
||||
4. Fill in:
|
||||
- **App Name**: e.g., `Hermes Agent`
|
||||
- **Description**: optional
|
||||
5. After creating, navigate to **Credentials & Basic Info** to find your **Client ID** (AppKey) and **Client Secret** (AppSecret). Copy both.
|
||||
|
||||
:::warning[Credentials shown only once]
|
||||
The Client Secret is only displayed once when you create the app. If you lose it, you'll need to regenerate it. Never share these credentials publicly or commit them to Git.
|
||||
:::
|
||||
|
||||
## Step 2: Enable the Robot Capability
|
||||
|
||||
1. In your app's settings page, go to **Add Capability** → **Robot**.
|
||||
2. Enable the robot capability.
|
||||
3. Under **Message Reception Mode**, select **Stream Mode** (recommended — no public URL needed).
|
||||
|
||||
:::tip
|
||||
Stream Mode is the recommended setup. It uses a long-lived WebSocket connection initiated from your machine, so you don't need a public IP, domain name, or webhook endpoint. This works behind NAT, firewalls, and on local machines.
|
||||
:::
|
||||
|
||||
## Step 3: Find Your DingTalk User ID
|
||||
|
||||
Hermes Agent uses your DingTalk User ID to control who can interact with the bot. DingTalk User IDs are alphanumeric strings set by your organization's admin.
|
||||
|
||||
To find yours:
|
||||
|
||||
1. Ask your DingTalk organization admin — User IDs are configured in the DingTalk admin console under **Contacts** → **Members**.
|
||||
2. Alternatively, the bot logs the `sender_id` for each incoming message. Start the gateway, send the bot a message, then check the logs for your ID.
|
||||
|
||||
## Step 4: Configure Hermes Agent
|
||||
|
||||
### Option A: Interactive Setup (Recommended)
|
||||
|
||||
Run the guided setup command:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **DingTalk** when prompted. The setup wizard can authorize via one of two paths:
|
||||
|
||||
- **QR-code device flow (recommended).** Scan the QR that prints in your terminal with the DingTalk mobile app — your Client ID and Client Secret are returned automatically and written to `~/.hermes/.env`. No developer-console trip needed.
|
||||
- **Manual paste.** If you already have credentials (or QR scanning isn't convenient), paste your Client ID, Client Secret, and allowed user IDs when prompted.
|
||||
|
||||
:::note openClaw branding disclosure
|
||||
Because DingTalk's `verification_uri_complete` is hardcoded to the openClaw identity at the API layer, the QR currently authorizes under an `openClaw` source string until Alibaba / DingTalk-Real-AI registers a Hermes-specific template server-side. This is purely how DingTalk presents the consent screen — the bot you create is fully yours and private to your tenant.
|
||||
:::
|
||||
|
||||
### Option B: Manual Configuration
|
||||
|
||||
Add the following to your `~/.hermes/.env` file:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
DINGTALK_CLIENT_ID=your-app-key
|
||||
DINGTALK_CLIENT_SECRET=your-app-secret
|
||||
|
||||
# Security: restrict who can interact with the bot
|
||||
DINGTALK_ALLOWED_USERS=user-id-1
|
||||
|
||||
# Multiple allowed users (comma-separated)
|
||||
# DINGTALK_ALLOWED_USERS=user-id-1,user-id-2
|
||||
|
||||
# Optional: group-chat gating (mirrors Slack/Telegram/Discord/WhatsApp)
|
||||
# DINGTALK_REQUIRE_MENTION=true
|
||||
# DINGTALK_FREE_RESPONSE_CHATS=cidABC==,cidDEF==
|
||||
# DINGTALK_MENTION_PATTERNS=^小马
|
||||
# DINGTALK_HOME_CHANNEL=cidXXXX==
|
||||
# DINGTALK_ALLOW_ALL_USERS=true
|
||||
```
|
||||
|
||||
Optional behavior settings in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
|
||||
gateway:
|
||||
platforms:
|
||||
dingtalk:
|
||||
extra:
|
||||
# Require @mention in groups before the bot replies (parity with Slack/Telegram/Discord).
|
||||
# DMs ignore this — the bot always replies in 1:1 chats.
|
||||
require_mention: true
|
||||
|
||||
# Per-platform allowlist. When set, only these DingTalk user IDs can interact with the bot
|
||||
# (same semantics as DINGTALK_ALLOWED_USERS, but scoped here instead of in .env).
|
||||
allowed_users:
|
||||
- user-id-1
|
||||
- user-id-2
|
||||
```
|
||||
|
||||
- `group_sessions_per_user: true` keeps each participant's context isolated inside shared group chats
|
||||
- `require_mention: true` prevents the bot from responding to every group message — it only answers when someone @-mentions it
|
||||
- `allowed_users` under `dingtalk.extra` is an alternative to `DINGTALK_ALLOWED_USERS`; if both are set, they're merged
|
||||
|
||||
### Start the Gateway
|
||||
|
||||
Once configured, start the DingTalk gateway:
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
The bot should connect to DingTalk's Stream Mode within a few seconds. Send it a message — either a DM or in a group where it's been added — to test.
|
||||
|
||||
:::tip
|
||||
You can run `hermes gateway` in the background or as a systemd service for persistent operation. See the deployment docs for details.
|
||||
:::
|
||||
|
||||
## Features
|
||||
|
||||
### AI Cards
|
||||
|
||||
Hermes can reply using DingTalk AI Cards instead of plain markdown messages. Cards provide a richer, more structured display and support streaming updates as the agent generates its response.
|
||||
|
||||
To enable AI Cards, configure a card template ID in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
dingtalk:
|
||||
enabled: true
|
||||
extra:
|
||||
card_template_id: "your-card-template-id"
|
||||
```
|
||||
|
||||
You can find your card template ID in the DingTalk Developer Console under your app's AI Card settings. When AI Cards are enabled, all replies are sent as cards with streaming text updates.
|
||||
|
||||
### Emoji Reactions
|
||||
|
||||
Hermes automatically adds emoji reactions to your messages to show processing status:
|
||||
|
||||
- 🤔Thinking — added when the bot starts processing your message
|
||||
- 🥳Done — added when the response is complete (replaces the Thinking reaction)
|
||||
|
||||
These reactions work in both DMs and group chats.
|
||||
|
||||
### Display Settings
|
||||
|
||||
You can customize DingTalk's display behavior independently from other platforms:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
platforms:
|
||||
dingtalk:
|
||||
show_reasoning: false # Show model reasoning/thinking in replies
|
||||
streaming: true # Enable streaming responses (works with AI Cards)
|
||||
tool_progress: all # Show tool execution progress (all/new/off)
|
||||
interim_assistant_messages: true # Show intermediate commentary messages
|
||||
```
|
||||
|
||||
To disable tool progress and intermediate messages for a cleaner experience:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
platforms:
|
||||
dingtalk:
|
||||
tool_progress: off
|
||||
interim_assistant_messages: false
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot is not responding to messages
|
||||
|
||||
**Cause**: The robot capability isn't enabled, or `DINGTALK_ALLOWED_USERS` doesn't include your User ID.
|
||||
|
||||
**Fix**: Verify the robot capability is enabled in your app settings and that Stream Mode is selected. Check that your User ID is in `DINGTALK_ALLOWED_USERS`. Restart the gateway.
|
||||
|
||||
### "dingtalk-stream not installed" error
|
||||
|
||||
**Cause**: The `dingtalk-stream` Python package is not installed.
|
||||
|
||||
**Fix**: Install it:
|
||||
|
||||
```bash
|
||||
pip install dingtalk-stream httpx
|
||||
```
|
||||
|
||||
### "DINGTALK_CLIENT_ID and DINGTALK_CLIENT_SECRET required"
|
||||
|
||||
**Cause**: The credentials aren't set in your environment or `.env` file.
|
||||
|
||||
**Fix**: Verify `DINGTALK_CLIENT_ID` and `DINGTALK_CLIENT_SECRET` are set correctly in `~/.hermes/.env`. The Client ID is your AppKey, and the Client Secret is your AppSecret from the DingTalk Developer Console.
|
||||
|
||||
### Stream disconnects / reconnection loops
|
||||
|
||||
**Cause**: Network instability, DingTalk platform maintenance, or credential issues.
|
||||
|
||||
**Fix**: The adapter automatically reconnects with exponential backoff (2s → 5s → 10s → 30s → 60s). Check that your credentials are valid and your app hasn't been deactivated. Verify your network allows outbound WebSocket connections.
|
||||
|
||||
### Bot is offline
|
||||
|
||||
**Cause**: The Hermes gateway isn't running, or it failed to connect.
|
||||
|
||||
**Fix**: Check that `hermes gateway` is running. Look at the terminal output for error messages. Common issues: wrong credentials, app deactivated, `dingtalk-stream` or `httpx` not installed.
|
||||
|
||||
### "No session_webhook available"
|
||||
|
||||
**Cause**: The bot tried to reply but doesn't have a session webhook URL. This typically happens if the webhook expired or the bot was restarted between receiving the message and sending the reply.
|
||||
|
||||
**Fix**: Send a new message to the bot — each incoming message provides a fresh session webhook for replies. This is a normal DingTalk limitation; the bot can only reply to messages it has received recently.
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
Always set `DINGTALK_ALLOWED_USERS` to restrict who can interact with the bot. Without it, the gateway denies all users by default as a safety measure. Only add User IDs of people you trust — authorized users have full access to the agent's capabilities, including tool use and system access.
|
||||
:::
|
||||
|
||||
For more information on securing your Hermes Agent deployment, see the [Security Guide](../security.md).
|
||||
|
||||
## Notes
|
||||
|
||||
- **Stream Mode**: No public URL, domain name, or webhook server needed. The connection is initiated from your machine via WebSocket, so it works behind NAT and firewalls.
|
||||
- **AI Cards**: Optionally reply with rich AI Cards instead of plain markdown. Configure via `card_template_id`.
|
||||
- **Emoji Reactions**: Automatic 🤔Thinking/🥳Done reactions for processing status.
|
||||
- **Markdown responses**: Replies are formatted in DingTalk's markdown format for rich text display.
|
||||
- **Media support**: Images and files in incoming messages are automatically resolved and can be processed by vision tools.
|
||||
- **Message deduplication**: The adapter deduplicates messages with a 5-minute window to prevent processing the same message twice.
|
||||
- **Auto-reconnection**: If the stream connection drops, the adapter automatically reconnects with exponential backoff.
|
||||
- **Message length limit**: Responses are capped at 20,000 characters per message. Longer responses are truncated.
|
||||
@@ -0,0 +1,832 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: "Discord"
|
||||
description: "Set up Hermes Agent as a Discord bot"
|
||||
---
|
||||
|
||||
# Discord Setup
|
||||
|
||||
Hermes Agent integrates with Discord as a bot, letting you chat with your AI assistant through direct messages or server channels. The bot receives your messages, processes them through the Hermes Agent pipeline (including tool use, memory, and reasoning), and responds in real time. It supports text, voice messages, file attachments, and slash commands.
|
||||
|
||||
Before setup, here's the part most people want to know: how Hermes behaves once it's in your server.
|
||||
|
||||
## How Hermes Behaves
|
||||
|
||||
| Context | Behavior |
|
||||
|---------|----------|
|
||||
| **DMs** | Hermes responds to every message. No `@mention` needed. Each DM has its own session. |
|
||||
| **Server channels** | By default, Hermes only responds when you `@mention` it. If you post in a channel without mentioning it, Hermes ignores the message. |
|
||||
| **Free-response channels** | You can make specific channels mention-free with `DISCORD_FREE_RESPONSE_CHANNELS`, or disable mentions globally with `DISCORD_REQUIRE_MENTION=false`. Messages in these channels are answered inline — auto-threading is skipped so the channel stays a lightweight chat. |
|
||||
| **Threads** | Hermes replies in the same thread. Mention rules still apply unless that thread or its parent channel is configured as free-response. Threads stay isolated from the parent channel for session history. |
|
||||
| **Shared channels with multiple users** | By default, Hermes isolates session history per user inside the channel for safety and clarity. Two people talking in the same channel do not share one transcript unless you explicitly disable that. |
|
||||
| **Messages mentioning other users** | When `DISCORD_IGNORE_NO_MENTION` is `true` (the default), Hermes stays silent if a message @mentions other users but does **not** mention the bot. This prevents the bot from jumping into conversations directed at other people. Set to `false` if you want the bot to respond to all messages regardless of who is mentioned. This only applies in server channels, not DMs. |
|
||||
|
||||
:::tip
|
||||
If you want a normal bot-help channel where people can talk to Hermes without tagging it every time, add that channel to `DISCORD_FREE_RESPONSE_CHANNELS`.
|
||||
:::
|
||||
|
||||
### Discord Gateway Model
|
||||
|
||||
Hermes on Discord is not a webhook that replies statelessly. It runs through the full messaging gateway, which means each incoming message goes through:
|
||||
|
||||
1. authorization (`DISCORD_ALLOWED_USERS`)
|
||||
2. mention / free-response checks
|
||||
3. session lookup
|
||||
4. session transcript loading
|
||||
5. normal Hermes agent execution, including tools, memory, and slash commands
|
||||
6. response delivery back to Discord
|
||||
|
||||
That matters because behavior in a busy server depends on both Discord routing and Hermes session policy.
|
||||
|
||||
### Session Model in Discord
|
||||
|
||||
By default:
|
||||
|
||||
- each DM gets its own session
|
||||
- each server thread gets its own session namespace
|
||||
- each user in a shared channel gets their own session inside that channel
|
||||
|
||||
So if Alice and Bob both talk to Hermes in `#research`, Hermes treats those as separate conversations by default even though they are using the same visible Discord channel.
|
||||
|
||||
This is controlled by `config.yaml`:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
Set it to `false` only if you explicitly want one shared conversation for the entire room:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: false
|
||||
```
|
||||
|
||||
Shared sessions can be useful for a collaborative room, but they also mean:
|
||||
|
||||
- users share context growth and token costs
|
||||
- one person's long tool-heavy task can bloat everyone else's context
|
||||
- one person's in-flight run can interrupt another person's follow-up in the same room
|
||||
|
||||
### Interrupts and Concurrency
|
||||
|
||||
Hermes tracks running agents by session key.
|
||||
|
||||
With the default `group_sessions_per_user: true`:
|
||||
|
||||
- Alice interrupting her own in-flight request only affects Alice's session in that channel
|
||||
- Bob can keep talking in the same channel without inheriting Alice's history or interrupting Alice's run
|
||||
|
||||
With `group_sessions_per_user: false`:
|
||||
|
||||
- the whole room shares one running-agent slot for that channel/thread
|
||||
- follow-up messages from different people can interrupt or queue behind each other
|
||||
|
||||
This guide walks you through the full setup process — from creating your bot on Discord's Developer Portal to sending your first message.
|
||||
|
||||
## Step 1: Create a Discord Application
|
||||
|
||||
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) and sign in with your Discord account.
|
||||
2. Click **New Application** in the top-right corner.
|
||||
3. Enter a name for your application (e.g., "Hermes Agent") and accept the Developer Terms of Service.
|
||||
4. Click **Create**.
|
||||
|
||||
You'll land on the **General Information** page. Note the **Application ID** — you'll need it later to build the invite URL.
|
||||
|
||||
## Step 2: Create the Bot
|
||||
|
||||
1. In the left sidebar, click **Bot**.
|
||||
2. Discord automatically creates a bot user for your application. You'll see the bot's username, which you can customize.
|
||||
3. Under **Authorization Flow**:
|
||||
- Set **Public Bot** to **ON** — required to use the Discord-provided invite link (recommended). This allows the Installation tab to generate a default authorization URL.
|
||||
- Leave **Require OAuth2 Code Grant** set to **OFF**.
|
||||
|
||||
:::tip
|
||||
You can set a custom avatar and banner for your bot on this page. This is what users will see in Discord.
|
||||
:::
|
||||
|
||||
:::info[Private Bot Alternative]
|
||||
If you prefer to keep your bot private (Public Bot = OFF), you **must** use the **Manual URL** method in Step 5 instead of the Installation tab. The Discord-provided link requires Public Bot to be enabled.
|
||||
:::
|
||||
|
||||
## Step 3: Enable Privileged Gateway Intents
|
||||
|
||||
This is the most critical step in the entire setup. Without the correct intents enabled, your bot will connect to Discord but **will not be able to read message content**.
|
||||
|
||||
On the **Bot** page, scroll down to **Privileged Gateway Intents**. You'll see three toggles:
|
||||
|
||||
| Intent | Purpose | Required? |
|
||||
|--------|---------|-----------|
|
||||
| **Presence Intent** | See user online/offline status | Optional |
|
||||
| **Server Members Intent** | Access the member list, resolve usernames | **Required** |
|
||||
| **Message Content Intent** | Read the text content of messages | **Required** |
|
||||
|
||||
**Enable both Server Members Intent and Message Content Intent** by toggling them **ON**.
|
||||
|
||||
- Without **Message Content Intent**, your bot receives message events but the message text is empty — the bot literally cannot see what you typed.
|
||||
- Without **Server Members Intent**, the bot cannot resolve usernames for the allowed users list and may fail to identify who is messaging it.
|
||||
|
||||
:::warning[This is the #1 reason Discord bots don't work]
|
||||
If your bot is online but never responds to messages, the **Message Content Intent** is almost certainly disabled. Go back to the [Developer Portal](https://discord.com/developers/applications), select your application → Bot → Privileged Gateway Intents, and make sure **Message Content Intent** is toggled ON. Click **Save Changes**.
|
||||
:::
|
||||
|
||||
**Regarding server count:**
|
||||
- If your bot is in **fewer than 100 servers**, you can simply toggle intents on and off freely.
|
||||
- If your bot is in **100 or more servers**, Discord requires you to submit a verification application to use privileged intents. For personal use, this is not a concern.
|
||||
|
||||
Click **Save Changes** at the bottom of the page.
|
||||
|
||||
## Step 4: Get the Bot Token
|
||||
|
||||
The bot token is the credential Hermes Agent uses to log in as your bot. Still on the **Bot** page:
|
||||
|
||||
1. Under the **Token** section, click **Reset Token**.
|
||||
2. If you have two-factor authentication enabled on your Discord account, enter your 2FA code.
|
||||
3. Discord will display your new token. **Copy it immediately.**
|
||||
|
||||
:::warning[Token shown only once]
|
||||
The token is only displayed once. If you lose it, you'll need to reset it and generate a new one. Never share your token publicly or commit it to Git — anyone with this token has full control of your bot.
|
||||
:::
|
||||
|
||||
Store the token somewhere safe (a password manager, for example). You'll need it in Step 8.
|
||||
|
||||
## Step 5: Generate the Invite URL
|
||||
|
||||
You need an OAuth2 URL to invite the bot to your server. There are two ways to do this:
|
||||
|
||||
### Option A: Using the Installation Tab (Recommended)
|
||||
|
||||
:::note[Requires Public Bot]
|
||||
This method requires **Public Bot** to be set to **ON** in Step 2. If you set Public Bot to OFF, use the Manual URL method below instead.
|
||||
:::
|
||||
|
||||
1. In the left sidebar, click **Installation**.
|
||||
2. Under **Installation Contexts**, enable **Guild Install**.
|
||||
3. For **Install Link**, select **Discord Provided Link**.
|
||||
4. Under **Default Install Settings** for Guild Install:
|
||||
- **Scopes**: select `bot` and `applications.commands`
|
||||
- **Permissions**: select the permissions listed below.
|
||||
|
||||
### Option B: Manual URL
|
||||
|
||||
You can construct the invite URL directly using this format:
|
||||
|
||||
```
|
||||
https://discord.com/oauth2/authorize?client_id=YOUR_APP_ID&scope=bot+applications.commands&permissions=274878286912
|
||||
```
|
||||
|
||||
Replace `YOUR_APP_ID` with the Application ID from Step 1.
|
||||
|
||||
### Required Permissions
|
||||
|
||||
These are the minimum permissions your bot needs:
|
||||
|
||||
- **View Channels** — see the channels it has access to
|
||||
- **Send Messages** — respond to your messages
|
||||
- **Embed Links** — format rich responses
|
||||
- **Attach Files** — send images, audio, and file outputs
|
||||
- **Read Message History** — maintain conversation context
|
||||
|
||||
### Recommended Additional Permissions
|
||||
|
||||
- **Send Messages in Threads** — respond in thread conversations
|
||||
- **Add Reactions** — react to messages for acknowledgment
|
||||
|
||||
### Permission Integers
|
||||
|
||||
| Level | Permissions Integer | What's Included |
|
||||
|-------|-------------------|-----------------|
|
||||
| Minimal | `117760` | View Channels, Send Messages, Read Message History, Attach Files |
|
||||
| Recommended | `274878286912` | All of the above plus Embed Links, Send Messages in Threads, Add Reactions |
|
||||
|
||||
## Step 6: Invite to Your Server
|
||||
|
||||
1. Open the invite URL in your browser (from the Installation tab or the manual URL you constructed).
|
||||
2. In the **Add to Server** dropdown, select your server.
|
||||
3. Click **Continue**, then **Authorize**.
|
||||
4. Complete the CAPTCHA if prompted.
|
||||
|
||||
:::info
|
||||
You need the **Manage Server** permission on the Discord server to invite a bot. If you don't see your server in the dropdown, ask a server admin to use the invite link instead.
|
||||
:::
|
||||
|
||||
After authorizing, the bot will appear in your server's member list (it will show as offline until you start the Hermes gateway).
|
||||
|
||||
## Step 7: Find Your Discord User ID
|
||||
|
||||
Hermes Agent uses your Discord User ID to control who can interact with the bot. To find it:
|
||||
|
||||
1. Open Discord (desktop or web app).
|
||||
2. Go to **Settings** → **Advanced** → toggle **Developer Mode** to **ON**.
|
||||
3. Close settings.
|
||||
4. Right-click your own username (in a message, the member list, or your profile) → **Copy User ID**.
|
||||
|
||||
Your User ID is a long number like `284102345871466496`.
|
||||
|
||||
:::tip
|
||||
Developer Mode also lets you copy **Channel IDs** and **Server IDs** the same way — right-click the channel or server name and select Copy ID. You'll need a Channel ID if you want to set a home channel manually.
|
||||
:::
|
||||
|
||||
## Step 8: Configure Hermes Agent
|
||||
|
||||
### Option A: Interactive Setup (Recommended)
|
||||
|
||||
Run the guided setup command:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **Discord** when prompted, then paste your bot token and user ID when asked.
|
||||
|
||||
### Option B: Manual Configuration
|
||||
|
||||
Add the following to your `~/.hermes/.env` file:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
DISCORD_BOT_TOKEN=your-bot-token
|
||||
DISCORD_ALLOWED_USERS=284102345871466496
|
||||
|
||||
# Multiple allowed users (comma-separated)
|
||||
# DISCORD_ALLOWED_USERS=284102345871466496,198765432109876543
|
||||
```
|
||||
|
||||
Then start the gateway:
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
The bot should come online in Discord within a few seconds. Send it a message — either a DM or in a channel it can see — to test.
|
||||
|
||||
:::tip
|
||||
You can run `hermes gateway` in the background or as a systemd service for persistent operation. See the deployment docs for details.
|
||||
:::
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
Discord behavior is controlled through two files: **`~/.hermes/.env`** for credentials and env-level toggles, and **`~/.hermes/config.yaml`** for structured settings. Environment variables always take precedence over config.yaml values when both are set.
|
||||
|
||||
### Environment Variables (`.env`)
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `DISCORD_BOT_TOKEN` | **Yes** | — | Bot token from the [Discord Developer Portal](https://discord.com/developers/applications). |
|
||||
| `DISCORD_ALLOWED_USERS` | **Yes** | — | Comma-separated Discord user IDs allowed to interact with the bot. Without this **or** `DISCORD_ALLOWED_ROLES`, the gateway denies all users. |
|
||||
| `DISCORD_ALLOWED_ROLES` | No | — | Comma-separated Discord role IDs. Any member with one of these roles is authorized — OR semantics with `DISCORD_ALLOWED_USERS`. Auto-enables the **Server Members Intent** on connect. Useful when moderation teams churn: new mods get access as soon as the role is granted, no config push needed. |
|
||||
| `DISCORD_HOME_CHANNEL` | No | — | Channel ID where the bot sends proactive messages (cron output, reminders, notifications). |
|
||||
| `DISCORD_HOME_CHANNEL_NAME` | No | `"Home"` | Display name for the home channel in logs and status output. |
|
||||
| `DISCORD_COMMAND_SYNC_POLICY` | No | `"safe"` | Controls native slash-command startup sync. `"safe"` diffs existing global commands and only updates what changed, recreating commands when Discord metadata changes cannot be applied via patch. `"bulk"` preserves the old `tree.sync()` behavior. `"off"` skips startup sync entirely. |
|
||||
| `DISCORD_REQUIRE_MENTION` | No | `true` | When `true`, the bot only responds in server channels when `@mentioned`. Set to `false` to respond to all messages in every channel. |
|
||||
| `DISCORD_THREAD_REQUIRE_MENTION` | No | `false` | When `true`, the in-thread mention shortcut is disabled — threads are gated the same as channels, requiring `@mention` even after the bot has already participated. Use this when multiple bots share a thread and you want each to fire only on explicit `@mention`. |
|
||||
| `DISCORD_FREE_RESPONSE_CHANNELS` | No | — | Comma-separated channel IDs where the bot responds without requiring an `@mention`, even when `DISCORD_REQUIRE_MENTION` is `true`. |
|
||||
| `DISCORD_IGNORE_NO_MENTION` | No | `true` | When `true`, the bot stays silent if a message `@mentions` other users but does **not** mention the bot. Prevents the bot from jumping into conversations directed at other people. Only applies in server channels, not DMs. |
|
||||
| `DISCORD_AUTO_THREAD` | No | `true` | When `true`, automatically creates a new thread for every `@mention` in a text channel, so each conversation is isolated (similar to Slack behavior). Messages already inside threads or DMs are unaffected. |
|
||||
| `DISCORD_ALLOW_BOTS` | No | `"none"` | Controls how the bot handles messages from other Discord bots. `"none"` — ignore all other bots. `"mentions"` — only accept bot messages that `@mention` Hermes. `"all"` — accept all bot messages. |
|
||||
| `DISCORD_REACTIONS` | No | `true` | When `true`, the bot adds emoji reactions to messages during processing (👀 when starting, ✅ on success, ❌ on error). Set to `false` to disable reactions entirely. |
|
||||
| `DISCORD_IGNORED_CHANNELS` | No | — | Comma-separated channel IDs where the bot **never** responds, even when `@mentioned`. Takes priority over all other channel settings. |
|
||||
| `DISCORD_ALLOWED_CHANNELS` | No | — | Comma-separated channel IDs. When set, the bot **only** responds in these channels (plus DMs if allowed). Overrides `config.yaml` `discord.allowed_channels`. Combine with `DISCORD_IGNORED_CHANNELS` to express allow/deny rules. |
|
||||
| `DISCORD_NO_THREAD_CHANNELS` | No | — | Comma-separated channel IDs where the bot responds directly in the channel instead of creating a thread. Only relevant when `DISCORD_AUTO_THREAD` is `true`. |
|
||||
| `DISCORD_HISTORY_BACKFILL` | No | `true` | When `true`, prepend recent channel scrollback (since the bot's last response) to the user message when the bot is mentioned. Recovers context the bot would otherwise miss with `require_mention`. Skipped in DMs and free-response channels. Set to `false` to disable. |
|
||||
| `DISCORD_HISTORY_BACKFILL_LIMIT` | No | `50` | Maximum number of messages to scan backwards when assembling the backfill block. In practice the scan usually stops earlier — at the bot's own last message in the channel. |
|
||||
| `DISCORD_REPLY_TO_MODE` | No | `"first"` | Controls reply-reference behavior: `"off"` — never reply to the original message, `"first"` — reply-reference on the first message chunk only (default), `"all"` — reply-reference on every chunk. |
|
||||
| `DISCORD_ALLOW_MENTION_EVERYONE` | No | `false` | When `false` (default), the bot cannot ping `@everyone` or `@here` even if its response contains those tokens. Set to `true` to opt back in. See [Mention Control](#mention-control) below. |
|
||||
| `DISCORD_ALLOW_MENTION_ROLES` | No | `false` | When `false` (default), the bot cannot ping `@role` mentions. Set to `true` to allow. |
|
||||
| `DISCORD_ALLOW_MENTION_USERS` | No | `true` | When `true` (default), the bot can ping individual users by ID. |
|
||||
| `DISCORD_ALLOW_MENTION_REPLIED_USER` | No | `true` | When `true` (default), replying to a message pings the original author. |
|
||||
| `DISCORD_PROXY` | No | — | Proxy URL for Discord connections (HTTP, WebSocket, REST). Overrides `HTTPS_PROXY`/`ALL_PROXY`. Supports `http://`, `https://`, and `socks5://` schemes. |
|
||||
| `DISCORD_ALLOW_ANY_ATTACHMENT` | No | `false` | When `true`, the bot accepts attachments of any file type (not just the built-in PDF/text/zip/office allowlist). Unknown types are cached to disk and surfaced to the agent as a local path with `application/octet-stream` MIME so it can inspect them with `terminal` / `read_file` / `ffprobe` / etc. |
|
||||
| `DISCORD_MAX_ATTACHMENT_BYTES` | No | `33554432` | Maximum bytes per attachment the gateway will download and cache. Default 32 MiB. Set to `0` for no cap (attachments are held in memory while being written, so unlimited carries a real memory cost). |
|
||||
| `HERMES_DISCORD_TEXT_BATCH_DELAY_SECONDS` | No | `0.6` | Grace window the adapter waits before flushing a queued text chunk. Useful for smoothing streamed output. |
|
||||
| `HERMES_DISCORD_TEXT_BATCH_SPLIT_DELAY_SECONDS` | No | `2.0` | Delay between split chunks when a single message exceeds Discord's length limit. |
|
||||
|
||||
### Config File (`config.yaml`)
|
||||
|
||||
The `discord` section in `~/.hermes/config.yaml` mirrors the env vars above. Config.yaml settings are applied as defaults — if the equivalent env var is already set, the env var wins.
|
||||
|
||||
```yaml
|
||||
# Discord-specific settings
|
||||
discord:
|
||||
require_mention: true # Require @mention in server channels
|
||||
thread_require_mention: false # If true, require @mention in threads too (multi-bot threads)
|
||||
free_response_channels: "" # Comma-separated channel IDs (or YAML list)
|
||||
auto_thread: true # Auto-create threads on @mention
|
||||
reactions: true # Add emoji reactions during processing
|
||||
ignored_channels: [] # Channel IDs where bot never responds
|
||||
no_thread_channels: [] # Channel IDs where bot responds without threading
|
||||
history_backfill: true # Prepend recent channel scrollback on mention (default: true)
|
||||
history_backfill_limit: 50 # Max messages to scan backwards (default: 50)
|
||||
channel_prompts: {} # Per-channel ephemeral system prompts
|
||||
allow_mentions: # What the bot is allowed to ping (safe defaults)
|
||||
everyone: false # @everyone / @here pings (default: false)
|
||||
roles: false # @role pings (default: false)
|
||||
users: true # @user pings (default: true)
|
||||
replied_user: true # reply-reference pings the author (default: true)
|
||||
|
||||
# Session isolation (applies to all gateway platforms, not just Discord)
|
||||
group_sessions_per_user: true # Isolate sessions per user in shared channels
|
||||
```
|
||||
|
||||
#### `discord.require_mention`
|
||||
|
||||
**Type:** boolean — **Default:** `true`
|
||||
|
||||
When enabled, the bot only responds in server channels when directly `@mentioned`. DMs always get a response regardless of this setting.
|
||||
|
||||
#### `discord.thread_require_mention`
|
||||
|
||||
**Type:** boolean — **Default:** `false`
|
||||
|
||||
By default, once the bot has participated in a thread (auto-created on `@mention` or replied in once), it keeps responding to every subsequent message in that thread without needing to be `@mentioned` again. That's the right default for one-on-one conversations.
|
||||
|
||||
In **multi-bot threads** where users address one bot per turn, this default becomes a footgun — every other bot in the thread also fires on every message, burning credits and spamming the channel. Set `thread_require_mention: true` to disable the in-thread shortcut and gate threads the same way channels are gated. Explicit `@mentions` still work as before.
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
require_mention: true
|
||||
thread_require_mention: true # multi-bot setup
|
||||
```
|
||||
|
||||
#### `discord.free_response_channels`
|
||||
|
||||
**Type:** string or list — **Default:** `""`
|
||||
|
||||
Channel IDs where the bot responds to all messages without needing an `@mention`. Accepts either a comma-separated string or a YAML list:
|
||||
|
||||
```yaml
|
||||
# String format
|
||||
discord:
|
||||
free_response_channels: "1234567890,9876543210"
|
||||
|
||||
# List format
|
||||
discord:
|
||||
free_response_channels:
|
||||
- 1234567890
|
||||
- 9876543210
|
||||
```
|
||||
|
||||
If a thread's parent channel is in this list, the thread also becomes mention-free.
|
||||
|
||||
Free-response channels also **skip auto-threading** — the bot replies inline rather than spinning off a new thread per message. This keeps the channel usable as a lightweight chat surface. If you want threading behavior, don't list the channel as free-response (use normal `@mention` flow instead).
|
||||
|
||||
#### `discord.auto_thread`
|
||||
|
||||
**Type:** boolean — **Default:** `true`
|
||||
|
||||
When enabled, every `@mention` in a regular text channel automatically creates a new thread for the conversation. This keeps the main channel clean and gives each conversation its own isolated session history. Once a thread is created, subsequent messages in that thread don't require `@mention` — the bot knows it's already participating. Set [`thread_require_mention`](#discordthread_require_mention) to `true` to disable this in-thread shortcut for multi-bot setups.
|
||||
|
||||
Messages sent in existing threads or DMs are unaffected by this setting. Channels listed in `discord.free_response_channels` or `discord.no_thread_channels` also bypass auto-threading and get inline replies instead.
|
||||
|
||||
#### `discord.reactions`
|
||||
|
||||
**Type:** boolean — **Default:** `true`
|
||||
|
||||
Controls whether the bot adds emoji reactions to messages as visual feedback:
|
||||
- 👀 added when the bot starts processing your message
|
||||
- ✅ added when the response is delivered successfully
|
||||
- ❌ added if an error occurs during processing
|
||||
|
||||
Disable this if you find the reactions distracting or if the bot's role doesn't have the **Add Reactions** permission.
|
||||
|
||||
#### `discord.ignored_channels`
|
||||
|
||||
**Type:** string or list — **Default:** `[]`
|
||||
|
||||
Channel IDs where the bot **never** responds, even when directly `@mentioned`. This takes the highest priority — if a channel is in this list, the bot silently ignores all messages there, regardless of `require_mention`, `free_response_channels`, or any other setting.
|
||||
|
||||
```yaml
|
||||
# String format
|
||||
discord:
|
||||
ignored_channels: "1234567890,9876543210"
|
||||
|
||||
# List format
|
||||
discord:
|
||||
ignored_channels:
|
||||
- 1234567890
|
||||
- 9876543210
|
||||
```
|
||||
|
||||
If a thread's parent channel is in this list, messages in that thread are also ignored.
|
||||
|
||||
#### `discord.no_thread_channels`
|
||||
|
||||
**Type:** string or list — **Default:** `[]`
|
||||
|
||||
Channel IDs where the bot responds directly in the channel instead of auto-creating a thread. This only has an effect when `auto_thread` is `true` (the default). In these channels, the bot responds inline like a normal message rather than spawning a new thread.
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
no_thread_channels:
|
||||
- 1234567890 # Bot responds inline here
|
||||
```
|
||||
|
||||
Useful for channels dedicated to bot interaction where threads would add unnecessary noise.
|
||||
|
||||
#### `discord.channel_prompts`
|
||||
|
||||
**Type:** mapping — **Default:** `{}`
|
||||
|
||||
Per-channel ephemeral system prompts that are injected on every turn in the matching Discord channel or thread without being persisted to transcript history.
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
channel_prompts:
|
||||
"1234567890": |
|
||||
This channel is for research tasks. Prefer deep comparisons,
|
||||
citations, and concise synthesis.
|
||||
"9876543210": |
|
||||
This forum is for therapy-style support. Be warm, grounded,
|
||||
and non-judgmental.
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- Exact thread/channel ID matches win.
|
||||
- If a message arrives inside a thread or forum post and that thread has no explicit entry, Hermes falls back to the parent channel/forum ID.
|
||||
- Prompts are applied ephemerally at runtime, so changing them affects future turns immediately without rewriting past session history.
|
||||
|
||||
#### `discord.history_backfill`
|
||||
|
||||
**Type:** boolean — **Default:** `true`
|
||||
|
||||
When enabled, the bot recovers missed channel messages on each `@mention`. With `require_mention: true`, the bot only processes messages that tag it directly — everything else in the channel is invisible to the session transcript. History backfill scans backwards through recent channel history when triggered, collecting messages between the bot's last response and the current mention, and includes them as context.
|
||||
|
||||
Behavior by surface:
|
||||
|
||||
- **Server channels** (with `require_mention: true`): backfill scans the channel since the bot's last response. Useful when other participants posted while the bot wasn't addressed.
|
||||
- **Threads**: backfill scans the thread only — Discord's `channel.history()` on a thread returns only that thread's messages, not the parent channel. This is the right scope because threads are usually self-contained conversations.
|
||||
- **DMs**: skipped. Every DM message triggers the bot, so the session transcript is already complete — there's no mention gap to fill.
|
||||
- **Free-response channels** and **bot's own auto-created threads**: skipped for the same reason — no mention gating means no gap.
|
||||
|
||||
Per-user sessions (`group_sessions_per_user: true`, the default) also benefit: a user's session is missing the context posted by other channel participants and the user's own messages from before they tagged the bot. Backfill fills both gaps.
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
history_backfill: true # default
|
||||
```
|
||||
|
||||
To turn it off:
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
history_backfill: false
|
||||
```
|
||||
|
||||
> **Note:** Messages that arrive *while* the bot is processing (between a trigger and its response) are not captured. This is an accepted simplification — the user can re-send or tag again.
|
||||
|
||||
#### `discord.history_backfill_limit`
|
||||
|
||||
**Type:** integer — **Default:** `50`
|
||||
|
||||
Maximum number of messages to scan backwards when recovering channel context. In practice the scan usually stops much earlier — at the bot's own last message in the channel, which is the natural boundary between turns. This limit is a safety cap for cold starts and long gaps where no prior bot message exists in recent history.
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
history_backfill: true
|
||||
history_backfill_limit: 50
|
||||
```
|
||||
|
||||
#### `group_sessions_per_user`
|
||||
|
||||
**Type:** boolean — **Default:** `true`
|
||||
|
||||
This is a global gateway setting (not Discord-specific) that controls whether users in the same channel get isolated session histories.
|
||||
|
||||
When `true`: Alice and Bob talking in `#research` each have their own separate conversation with Hermes. When `false`: the entire channel shares one conversation transcript and one running-agent slot.
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
See the [Session Model](#session-model-in-discord) section above for the full implications of each mode.
|
||||
|
||||
#### `display.tool_progress`
|
||||
|
||||
**Type:** string — **Default:** `"all"` — **Values:** `off`, `new`, `all`, `verbose`
|
||||
|
||||
Controls whether the bot sends progress messages in the chat while processing (e.g., "Reading file...", "Running terminal command..."). This is a global gateway setting that applies to all platforms.
|
||||
|
||||
```yaml
|
||||
display:
|
||||
tool_progress: "all" # off | new | all | verbose
|
||||
```
|
||||
|
||||
- `off` — no progress messages
|
||||
- `new` — only show the first tool call per turn
|
||||
- `all` — show all tool calls (truncated to 40 characters in gateway messages)
|
||||
- `verbose` — show full tool call details (can produce long messages)
|
||||
|
||||
#### `display.tool_progress_command`
|
||||
|
||||
**Type:** boolean — **Default:** `false`
|
||||
|
||||
When enabled, makes the `/verbose` slash command available in the gateway, letting you cycle through tool progress modes (`off → new → all → verbose → off`) without editing config.yaml.
|
||||
|
||||
```yaml
|
||||
display:
|
||||
tool_progress_command: true
|
||||
```
|
||||
|
||||
## Slash Command Access Control
|
||||
|
||||
By default, every allowed user can run every slash command. To split your allowlist into **admins** (full slash command access) and **regular users** (only commands you explicitly enable), add `allow_admin_from` and `user_allowed_commands` to the Discord platform's `extra` block:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
discord:
|
||||
extra:
|
||||
# Existing user allowlist (unchanged)
|
||||
allow_from:
|
||||
- "123456789012345678" # admin user ID
|
||||
- "999888777666555444" # regular user ID
|
||||
|
||||
# NEW — admins get all slash commands (built-in + plugin)
|
||||
allow_admin_from:
|
||||
- "123456789012345678"
|
||||
|
||||
# NEW — non-admin allowed users can only run these slash commands.
|
||||
# /help and /whoami are always allowed so users can see their access.
|
||||
user_allowed_commands:
|
||||
- status
|
||||
- model
|
||||
- history
|
||||
|
||||
# Optional: separate admin / command lists for server channels
|
||||
group_allow_admin_from:
|
||||
- "123456789012345678"
|
||||
group_user_allowed_commands:
|
||||
- status
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- A user in `allow_admin_from` for a scope (DM or server channel) can run **every** registered slash command — built-in AND plugin-registered — through the live command registry.
|
||||
- A user not in `allow_admin_from` can only run commands listed in `user_allowed_commands`, plus the always-allowed floor: `/help` and `/whoami`.
|
||||
- Plain chat (non-slash messages) is unaffected. Non-admin users can still talk to the agent normally; they just can't trigger arbitrary commands.
|
||||
- **Backward compat:** if `allow_admin_from` is not set for a scope, slash command gating is disabled for that scope. Existing installs keep working with no changes.
|
||||
- DM admin status does not imply server-channel admin status. Each scope has its own admin list.
|
||||
|
||||
Use `/whoami` to see the active scope, your tier (admin / user / unrestricted), and which slash commands you can run.
|
||||
|
||||
## Interactive Model Picker
|
||||
|
||||
Send `/model` with no arguments in a Discord channel to open a dropdown-based model picker:
|
||||
|
||||
1. **Provider selection** — a Select dropdown showing available providers (up to 25).
|
||||
2. **Model selection** — a second dropdown with models for the chosen provider (up to 25).
|
||||
|
||||
The picker times out after 120 seconds. Only authorized users (those in `DISCORD_ALLOWED_USERS`) can interact with it. If you know the model name, type `/model <name>` directly.
|
||||
|
||||
## Native Slash Commands for Skills
|
||||
|
||||
Hermes automatically registers installed skills as **native Discord Application Commands**. This means skills appear in Discord's autocomplete `/` menu alongside built-in commands.
|
||||
|
||||
- Each skill becomes a Discord slash command (e.g., `/code-review`, `/ascii-art`)
|
||||
- Skills accept an optional `args` string parameter
|
||||
- Discord has a limit of 100 application commands per bot — if you have more skills than available slots, extra skills are skipped with a warning in the logs
|
||||
- Skills are registered during bot startup alongside built-in commands like `/model`, `/reset`, and `/background`
|
||||
|
||||
No extra configuration is needed — any skill installed via `hermes skills install` is automatically registered as a Discord slash command on the next gateway restart.
|
||||
|
||||
### Disabling Slash Command Registration
|
||||
|
||||
If you run multiple Hermes gateways against the same Discord application (e.g. staging + production), only one of them should own the global slash-command registration — otherwise the last startup wins and the registrations flap. Turn slash registration off on the "follower" gateway:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
discord:
|
||||
extra:
|
||||
slash_commands: false # default: true
|
||||
```
|
||||
|
||||
Leaving this at `true` on the "primary" gateway keeps the normal behavior — global `/`-menu commands for built-ins and installed skills.
|
||||
|
||||
## Sending Media (`send_message` + `MEDIA:` tags)
|
||||
|
||||
The Discord adapter supports native file uploads for every common media type via the `send_message` tool and inline `MEDIA:/path/to/file` tags emitted by the agent:
|
||||
|
||||
| Type | How it's delivered |
|
||||
|---|---|
|
||||
| Images (PNG/JPG/WebP) | Native Discord image attachment with inline preview |
|
||||
| Animated GIFs | `send_animation` uploads as `animation.gif` so Discord plays it inline (not as a static thumbnail) |
|
||||
| Video (MP4/MOV) | `send_video` — native video player |
|
||||
| Audio / Voice | `send_voice` — native voice message when possible, file attachment otherwise |
|
||||
| Documents (PDF/ZIP/docx/etc.) | `send_document` — native attachment with download button |
|
||||
|
||||
Discord's per-upload size limit depends on the server's boost tier (25 MB free, up to 500 MB). If Hermes gets an HTTP 413, the adapter falls back to a link pointing at the local cache path rather than failing silently.
|
||||
|
||||
## Receiving Arbitrary File Types
|
||||
|
||||
By default the bot caches uploads that match a built-in allowlist — images, audio, video, PDF, text/markdown/csv/log, JSON/XML/YAML/TOML, zip, docx/xlsx/pptx. Anything else (a `.wav`, a `.bin`, a custom-extension dump) gets logged as `Unsupported document type` and dropped before the agent sees it.
|
||||
|
||||
To accept arbitrary file types, enable `discord.allow_any_attachment`:
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
allow_any_attachment: true
|
||||
# Optional — raise/disable the per-file size cap. Default is 32 MiB.
|
||||
# The whole file is held in memory while being cached, so unlimited
|
||||
# uploads carry a real memory cost.
|
||||
max_attachment_bytes: 33554432 # bytes; 0 = unlimited
|
||||
```
|
||||
|
||||
When the flag is on, any uploaded file is downloaded, cached under `~/.hermes/cache/documents/`, and surfaced to the agent as a `DOCUMENT`-typed message event with `application/octet-stream` MIME. The agent receives a context note pointing at the local path (auto-translated for Docker/Modal sandboxed terminals via `to_agent_visible_cache_path`) and can inspect the file with `terminal` (`ffprobe`, `unzip`, `file`, `strings`, etc.) or `read_file`. The file body is **not** inlined into the prompt — only the path — so binary uploads don't blow up the context window.
|
||||
|
||||
Known-text formats already in the allowlist (`.txt`, `.md`, `.log`) continue to have their contents auto-injected up to 100 KiB; that behavior is unchanged when the flag is on.
|
||||
|
||||
Equivalent env vars: `DISCORD_ALLOW_ANY_ATTACHMENT=true` and `DISCORD_MAX_ATTACHMENT_BYTES=33554432` (or `0` for no cap).
|
||||
|
||||
:::warning Memory cost of unlimited
|
||||
Disabling the size cap (`max_attachment_bytes: 0`) means a user can drop a multi-GB file on the bot and the gateway will dutifully buffer it through memory while caching to disk. Only set this in trusted single-user installs. For shared bots, keep the default 32 MiB or raise it conservatively.
|
||||
:::
|
||||
|
||||
## Interactive Prompts (clarify)
|
||||
|
||||
When the agent calls the `clarify` tool — to ask which approach you prefer, get post-task feedback, or check before a non-trivial decision — Discord renders the question with **one button per choice**:
|
||||
|
||||
> Which framework should I use for the dashboard?
|
||||
>
|
||||
> [1. Next.js] [2. Remix] [3. Astro] [Other (type answer)]
|
||||
|
||||
Click a numbered button to answer, or click **Other** to type a free-form response (the next message you send in that channel becomes the answer). Open-ended `clarify` calls (no preset choices) skip the buttons and just capture your next message.
|
||||
|
||||
The buttons disable themselves once a choice is made so duplicate clicks don't double-resolve the prompt. Configure the response timeout via `agent.clarify_timeout` in `~/.hermes/config.yaml` (default `600` seconds). If you don't respond within the timeout, the agent unblocks with a sentinel message and adapts rather than hanging.
|
||||
|
||||
## Home Channel
|
||||
|
||||
You can designate a "home channel" where the bot sends proactive messages (such as cron job output, reminders, and notifications). There are two ways to set it:
|
||||
|
||||
### Using the Slash Command
|
||||
|
||||
Type `/sethome` in any Discord channel where the bot is present. That channel becomes the home channel.
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Add these to your `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
DISCORD_HOME_CHANNEL=123456789012345678
|
||||
DISCORD_HOME_CHANNEL_NAME="#bot-updates"
|
||||
```
|
||||
|
||||
Replace the ID with the actual channel ID (right-click → Copy Channel ID with Developer Mode on).
|
||||
|
||||
## Voice Messages
|
||||
|
||||
Hermes Agent supports Discord voice messages:
|
||||
|
||||
- **Incoming voice messages** are automatically transcribed using the configured STT provider: local `faster-whisper` (no key), Groq Whisper (`GROQ_API_KEY`), or OpenAI Whisper (`VOICE_TOOLS_OPENAI_KEY`).
|
||||
- **Text-to-speech**: Use `/voice tts` to have the bot send spoken audio responses alongside text replies.
|
||||
- **Discord voice channels**: Hermes can also join a voice channel, listen to users speaking, and talk back in the channel.
|
||||
|
||||
For the full setup and operational guide, see:
|
||||
- [Voice Mode](/user-guide/features/voice-mode)
|
||||
- [Use Voice Mode with Hermes](/guides/use-voice-mode-with-hermes)
|
||||
|
||||
### Voice Channel Audio Effects (ambient + verbal acks)
|
||||
|
||||
When the bot is in a voice channel, you can give it a more conversational feel: a short verbal acknowledgement ("let me look into that") before it starts working, and a subtle ambient "thinking" bed that plays underneath while tools run — the speech ducks the ambient down and swells it back when finished, similar to Grok voice mode.
|
||||
|
||||
discord.py plays only one audio stream per connection, so Hermes installs a software mixer on the outgoing stream that sums an ambient loop, acknowledgements, and TTS replies into that single stream — they overlap instead of cutting each other off.
|
||||
|
||||
This is **off by default**. Enable it in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
voice_fx:
|
||||
enabled: true # master switch
|
||||
ambient_enabled: true # idle "thinking" bed while tools run
|
||||
ambient_path: "" # custom loop file (any audio format); "" = built-in synthesised pad
|
||||
ambient_gain: 0.18 # idle bed loudness (0.0–1.0)
|
||||
duck_gain: 0.06 # ambient loudness while the bot is speaking
|
||||
speech_gain: 1.0 # TTS / acknowledgement loudness
|
||||
ack_enabled: true # speak a short phrase before the first tool call of a turn
|
||||
ack_phrases: # picked at random; set to [] to disable the spoken ack
|
||||
- "Let me look into that."
|
||||
- "One moment."
|
||||
- "Checking on that now."
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The acknowledgement fires at most once per turn, only when the bot is in a voice channel and the mixer is active. It uses your configured TTS provider.
|
||||
- `ambient_path` accepts any file `ffmpeg` can decode; it's looped seamlessly. Leave it empty to use the built-in synthesised pad (no asset needed).
|
||||
- All settings live in `config.yaml` (not `.env`) — they're behavioral, not secrets.
|
||||
- When `voice_fx.enabled` is `false`, voice playback uses the original one-shot path and nothing changes.
|
||||
|
||||
|
||||
## Forum Channels
|
||||
|
||||
Discord forum channels (type 15) don't accept direct messages — every post in a forum must be a thread. Hermes auto-detects forum channels and creates a new thread post whenever it needs to send there, so `send_message`, TTS, images, voice messages, and file attachments all work without special handling from the agent.
|
||||
|
||||
- **Thread name** is derived from the first line of the message (markdown heading prefix stripped, capped at 100 chars). When the message is attachment-only, the filename is used as the fallback thread name.
|
||||
- **Attachments** ride along on the starter message of the new thread — no separate upload step, no partial sends.
|
||||
- **One call, one thread**: each forum send creates a new thread. Successive sends to the same forum will therefore produce separate threads.
|
||||
- **Detection is three-layered**: the channel directory cache first, a process-local probe cache second, and a live `GET /channels/{id}` probe as a last resort (whose result is then memoized for the life of the process).
|
||||
|
||||
Refreshing the directory (`/channels refresh` on platforms that expose it, or a gateway restart) populates the cache with any forum channels created after the bot started.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot is online but not responding to messages
|
||||
|
||||
**Cause**: Message Content Intent is disabled.
|
||||
|
||||
**Fix**: Go to [Developer Portal](https://discord.com/developers/applications) → your app → Bot → Privileged Gateway Intents → enable **Message Content Intent** → Save Changes. Restart the gateway.
|
||||
|
||||
### "Disallowed Intents" error on startup
|
||||
|
||||
**Cause**: Your code requests intents that aren't enabled in the Developer Portal.
|
||||
|
||||
**Fix**: Enable all three Privileged Gateway Intents (Presence, Server Members, Message Content) in the Bot settings, then restart.
|
||||
|
||||
### Bot can't see messages in a specific channel
|
||||
|
||||
**Cause**: The bot's role doesn't have permission to view that channel.
|
||||
|
||||
**Fix**: In Discord, go to the channel's settings → Permissions → add the bot's role with **View Channel** and **Read Message History** enabled.
|
||||
|
||||
### 403 Forbidden errors
|
||||
|
||||
**Cause**: The bot is missing required permissions.
|
||||
|
||||
**Fix**: Re-invite the bot with the correct permissions using the URL from Step 5, or manually adjust the bot's role permissions in Server Settings → Roles.
|
||||
|
||||
### Bot is offline
|
||||
|
||||
**Cause**: The Hermes gateway isn't running, or the token is incorrect.
|
||||
|
||||
**Fix**: Check that `hermes gateway` is running. Verify `DISCORD_BOT_TOKEN` in your `.env` file. If you recently reset the token, update it.
|
||||
|
||||
### "User not allowed" / Bot ignores you
|
||||
|
||||
**Cause**: Your User ID isn't in `DISCORD_ALLOWED_USERS`.
|
||||
|
||||
**Fix**: Add your User ID to `DISCORD_ALLOWED_USERS` in `~/.hermes/.env` and restart the gateway.
|
||||
|
||||
### People in the same channel are sharing context unexpectedly
|
||||
|
||||
**Cause**: `group_sessions_per_user` is disabled, or the platform cannot provide a user ID for the messages in that context.
|
||||
|
||||
**Fix**: Set this in `~/.hermes/config.yaml` and restart the gateway:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
If you intentionally want a shared room conversation, leave it off — just expect shared transcript history and shared interrupt behavior.
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
Always set `DISCORD_ALLOWED_USERS` (or `DISCORD_ALLOWED_ROLES`) to restrict who can interact with the bot. Without either, the gateway denies all users by default as a safety measure. Only authorize people you trust — authorized users have full access to the agent's capabilities, including tool use and system access.
|
||||
:::
|
||||
|
||||
### Role-Based Access Control
|
||||
|
||||
For servers where access is managed by roles instead of individual user lists (moderator teams, support staff, internal tooling), use `DISCORD_ALLOWED_ROLES` — a comma-separated list of role IDs. Any member with one of those roles is authorized.
|
||||
|
||||
```bash
|
||||
# ~/.hermes/.env — works alongside or instead of DISCORD_ALLOWED_USERS
|
||||
DISCORD_ALLOWED_ROLES=987654321098765432,876543210987654321
|
||||
```
|
||||
|
||||
Semantics:
|
||||
|
||||
- **OR with user allowlist.** A user is authorized if their ID is in `DISCORD_ALLOWED_USERS` **or** they have any role in `DISCORD_ALLOWED_ROLES`.
|
||||
- **Server Members Intent auto-enabled.** When `DISCORD_ALLOWED_ROLES` is set, the bot enables the Members intent on connect — required for Discord to send role information with member records.
|
||||
- **Role IDs, not names.** Grab them from Discord: **User Settings → Advanced → Developer Mode ON**, then right-click any role → **Copy Role ID**.
|
||||
- **DM fallback.** In DMs the role check scans mutual guilds; a user with an allowed role in any shared server is authorized in DMs too.
|
||||
|
||||
This is the preferred pattern when the moderation team churns — new moderators get access the moment the role is granted, with no `.env` edit or gateway restart.
|
||||
|
||||
### Mention Control
|
||||
|
||||
By default, Hermes blocks the bot from pinging `@everyone`, `@here`, and role mentions, even if its reply contains those tokens. This prevents a poorly-worded prompt or echoed user content from spamming a whole server. Individual `@user` pings and reply-reference pings (the little "replying to…" chip) stay enabled so normal conversation still works.
|
||||
|
||||
You can relax these defaults via either env vars or `config.yaml`:
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/config.yaml
|
||||
discord:
|
||||
allow_mentions:
|
||||
everyone: false # allow the bot to ping @everyone / @here
|
||||
roles: false # allow the bot to ping @role mentions
|
||||
users: true # allow the bot to ping individual @users
|
||||
replied_user: true # ping the author when replying to their message
|
||||
```
|
||||
|
||||
```bash
|
||||
# ~/.hermes/.env — env vars win over config.yaml
|
||||
DISCORD_ALLOW_MENTION_EVERYONE=false
|
||||
DISCORD_ALLOW_MENTION_ROLES=false
|
||||
DISCORD_ALLOW_MENTION_USERS=true
|
||||
DISCORD_ALLOW_MENTION_REPLIED_USER=true
|
||||
```
|
||||
|
||||
:::tip
|
||||
Leave `everyone` and `roles` at `false` unless you know exactly why you need them. It is very easy for an LLM to produce the string `@everyone` inside a normal-looking response; without this protection, that would notify every member of your server.
|
||||
:::
|
||||
|
||||
For more information on securing your Hermes Agent deployment, see the [Security Guide](../security.md).
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
title: "Email"
|
||||
description: "Set up Hermes Agent as an email assistant via IMAP/SMTP"
|
||||
---
|
||||
|
||||
# Email Setup
|
||||
|
||||
Hermes can receive and reply to emails using standard IMAP and SMTP protocols. Send an email to the agent's address and it replies in-thread — no special client or bot API needed. Works with Gmail, Outlook, Yahoo, Fastmail, or any provider that supports IMAP/SMTP.
|
||||
|
||||
:::info Gateway adapter only: no external dependencies
|
||||
This page covers the Email gateway adapter, which uses Python's built-in `imaplib`, `smtplib`, and `email` modules. No additional packages or external services are required for this gateway path.
|
||||
:::
|
||||
|
||||
This is separate from the bundled [Himalaya email skill](/docs/user-guide/skills/bundled/email/email-himalaya), which lets the agent manage email through terminal commands and requires the external `himalaya` CLI plus a Himalaya config file.
|
||||
|
||||
| Use case | What to configure | External dependency |
|
||||
|---|---|---|
|
||||
| Let people email the Hermes agent and receive replies | Email gateway adapter on this page | None beyond an IMAP/SMTP email account |
|
||||
| Let the agent inspect, compose, move, and manage mailbox messages from terminal tools | Himalaya email skill | `himalaya` CLI and `~/.config/himalaya/config.toml` |
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **A dedicated email account** for your Hermes agent (don't use your personal email)
|
||||
- **IMAP enabled** on the email account
|
||||
- **An app password** if using Gmail or another provider with 2FA
|
||||
|
||||
### Gmail Setup
|
||||
|
||||
1. Enable 2-Factor Authentication on your Google Account
|
||||
2. Go to [App Passwords](https://myaccount.google.com/apppasswords)
|
||||
3. Create a new App Password (select "Mail" or "Other")
|
||||
4. Copy the 16-character password — you'll use this instead of your regular password
|
||||
|
||||
### Outlook / Microsoft 365
|
||||
|
||||
1. Go to [Security Settings](https://account.microsoft.com/security)
|
||||
2. Enable 2FA if not already active
|
||||
3. Create an App Password under "Additional security options"
|
||||
4. IMAP host: `outlook.office365.com`, SMTP host: `smtp.office365.com`
|
||||
|
||||
### Other Providers
|
||||
|
||||
Most email providers support IMAP/SMTP. Check your provider's documentation for:
|
||||
- IMAP host and port (usually port 993 with SSL)
|
||||
- SMTP host and port (usually port 587 with STARTTLS)
|
||||
- Whether app passwords are required
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Configure Hermes
|
||||
|
||||
The easiest way:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **Email** from the platform menu. The wizard prompts for your email address, password, IMAP/SMTP hosts, and allowed senders.
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Add to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
EMAIL_ADDRESS=hermes@gmail.com
|
||||
EMAIL_PASSWORD=abcd efgh ijkl mnop # App password (not your regular password)
|
||||
EMAIL_IMAP_HOST=imap.gmail.com
|
||||
EMAIL_SMTP_HOST=smtp.gmail.com
|
||||
|
||||
# Security (recommended)
|
||||
EMAIL_ALLOWED_USERS=your@email.com,colleague@work.com
|
||||
|
||||
# Optional
|
||||
EMAIL_IMAP_PORT=993 # Default: 993 (IMAP SSL)
|
||||
EMAIL_SMTP_PORT=587 # Default: 587 (SMTP STARTTLS)
|
||||
EMAIL_POLL_INTERVAL=15 # Seconds between inbox checks (default: 15)
|
||||
EMAIL_HOME_ADDRESS=your@email.com # Default delivery target for cron jobs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Start the Gateway
|
||||
|
||||
```bash
|
||||
hermes gateway # Run in foreground
|
||||
hermes gateway install # Install as a user service
|
||||
sudo hermes gateway install --system # Linux only: boot-time system service
|
||||
```
|
||||
|
||||
On startup, the adapter:
|
||||
1. Tests IMAP and SMTP connections
|
||||
2. Marks all existing inbox messages as "seen" (only processes new emails)
|
||||
3. Starts polling for new messages
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### Receiving Messages
|
||||
|
||||
The adapter polls the IMAP inbox for UNSEEN messages at a configurable interval (default: 15 seconds). For each new email:
|
||||
|
||||
- **Subject line** is included as context (e.g., `[Subject: Deploy to production]`)
|
||||
- **Reply emails** (subject starting with `Re:`) skip the subject prefix — the thread context is already established
|
||||
- **Attachments** are cached locally:
|
||||
- Images (JPEG, PNG, GIF, WebP) → available to the vision tool
|
||||
- Documents (PDF, ZIP, etc.) → available for file access
|
||||
- **HTML-only emails** have tags stripped for plain text extraction
|
||||
- **Self-messages** are filtered out to prevent reply loops
|
||||
- **Automated/noreply senders** are silently ignored — `noreply@`, `mailer-daemon@`, `bounce@`, `no-reply@`, and emails with `Auto-Submitted`, `Precedence: bulk`, or `List-Unsubscribe` headers
|
||||
|
||||
### Sending Replies
|
||||
|
||||
Replies are sent via SMTP with proper email threading:
|
||||
|
||||
- **In-Reply-To** and **References** headers maintain the thread
|
||||
- **Subject line** preserved with `Re:` prefix (no double `Re: Re:`)
|
||||
- **Message-ID** generated with the agent's domain
|
||||
- Responses are sent as plain text (UTF-8)
|
||||
|
||||
### File Attachments
|
||||
|
||||
The agent can send file attachments in replies. Include `MEDIA:/path/to/file` in the response and the file is attached to the outgoing email.
|
||||
|
||||
### Skipping Attachments
|
||||
|
||||
To ignore all incoming attachments (for malware protection or bandwidth savings), add to your `config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
email:
|
||||
skip_attachments: true
|
||||
```
|
||||
|
||||
When enabled, attachment and inline parts are skipped before payload decoding. The email body text is still processed normally.
|
||||
|
||||
---
|
||||
|
||||
## Access Control
|
||||
|
||||
Email access follows the same pattern as all other Hermes platforms:
|
||||
|
||||
1. **`EMAIL_ALLOWED_USERS` set** → only emails from those addresses are processed
|
||||
2. **No allowlist set** → unknown senders get a pairing code
|
||||
3. **`EMAIL_ALLOW_ALL_USERS=true`** → any sender is accepted (use with caution)
|
||||
|
||||
:::warning
|
||||
**Always configure `EMAIL_ALLOWED_USERS`.** Without it, anyone who knows the agent's email address could send commands. The agent has terminal access by default.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| **"IMAP connection failed"** at startup | Verify `EMAIL_IMAP_HOST` and `EMAIL_IMAP_PORT`. Ensure IMAP is enabled on the account. For Gmail, enable it in Settings → Forwarding and POP/IMAP. |
|
||||
| **"SMTP connection failed"** at startup | Verify `EMAIL_SMTP_HOST` and `EMAIL_SMTP_PORT`. Check that your password is correct (use App Password for Gmail). |
|
||||
| **Messages not received** | Check `EMAIL_ALLOWED_USERS` includes the sender's email. Check spam folder — some providers flag automated replies. |
|
||||
| **"Authentication failed"** | For Gmail, you must use an App Password, not your regular password. Ensure 2FA is enabled first. |
|
||||
| **Duplicate replies** | Ensure only one gateway instance is running. Check `hermes gateway status`. |
|
||||
| **Slow response** | The default poll interval is 15 seconds. Reduce with `EMAIL_POLL_INTERVAL=5` for faster response (but more IMAP connections). |
|
||||
| **Replies not threading** | The adapter uses In-Reply-To headers. Some email clients (especially web-based) may not thread correctly with automated messages. |
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
**Use a dedicated email account.** Don't use your personal email — the agent stores the password in `.env` and has full inbox access via IMAP.
|
||||
:::
|
||||
|
||||
- Use **App Passwords** instead of your main password (required for Gmail with 2FA)
|
||||
- Set `EMAIL_ALLOWED_USERS` to restrict who can interact with the agent
|
||||
- The password is stored in `~/.hermes/.env` — protect this file (`chmod 600`)
|
||||
- IMAP uses SSL (port 993) and SMTP uses STARTTLS (port 587) by default — connections are encrypted
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `EMAIL_ADDRESS` | Yes | — | Agent's email address |
|
||||
| `EMAIL_PASSWORD` | Yes | — | Email password or app password |
|
||||
| `EMAIL_IMAP_HOST` | Yes | — | IMAP server host (e.g., `imap.gmail.com`) |
|
||||
| `EMAIL_SMTP_HOST` | Yes | — | SMTP server host (e.g., `smtp.gmail.com`) |
|
||||
| `EMAIL_IMAP_PORT` | No | `993` | IMAP server port |
|
||||
| `EMAIL_SMTP_PORT` | No | `587` | SMTP server port |
|
||||
| `EMAIL_POLL_INTERVAL` | No | `15` | Seconds between inbox checks |
|
||||
| `EMAIL_ALLOWED_USERS` | No | — | Comma-separated allowed sender addresses |
|
||||
| `EMAIL_HOME_ADDRESS` | No | — | Default delivery target for cron jobs |
|
||||
| `EMAIL_ALLOW_ALL_USERS` | No | `false` | Allow all senders (not recommended) |
|
||||
@@ -0,0 +1,590 @@
|
||||
---
|
||||
sidebar_position: 11
|
||||
title: "Feishu / Lark"
|
||||
description: "Set up Hermes Agent as a Feishu or Lark bot"
|
||||
---
|
||||
|
||||
# Feishu / Lark Setup
|
||||
|
||||
Hermes Agent integrates with Feishu and Lark as a full-featured bot. Once connected, you can chat with the agent in direct messages or group chats, receive cron job results in a home chat, and send text, images, audio, and file attachments through the normal gateway flow.
|
||||
|
||||
The integration supports both connection modes:
|
||||
|
||||
- `websocket` — recommended; Hermes opens the outbound connection and you do not need a public webhook endpoint
|
||||
- `webhook` — useful when you want Feishu/Lark to push events into your gateway over HTTP
|
||||
|
||||
## How Hermes Behaves
|
||||
|
||||
| Context | Behavior |
|
||||
|---------|----------|
|
||||
| Direct messages | Hermes responds to every message. |
|
||||
| Group chats | Hermes responds only when the bot is @mentioned in the chat. |
|
||||
| Shared group chats | By default, session history is isolated per user inside a shared chat. |
|
||||
|
||||
This shared-chat behavior is controlled by `config.yaml`:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
Set it to `false` only if you explicitly want one shared conversation per chat.
|
||||
|
||||
## Step 1: Create a Feishu / Lark App
|
||||
|
||||
### Recommended: Scan-to-Create (one command)
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **Feishu / Lark** and scan the QR code with your Feishu or Lark mobile app. Hermes will automatically create a bot application with the correct permissions and save the credentials.
|
||||
|
||||
### Alternative: Manual Setup
|
||||
|
||||
If scan-to-create is not available, the wizard falls back to manual input:
|
||||
|
||||
1. Open the Feishu or Lark developer console:
|
||||
- Feishu: [https://open.feishu.cn/](https://open.feishu.cn/)
|
||||
- Lark: [https://open.larksuite.com/](https://open.larksuite.com/)
|
||||
2. Create a new app.
|
||||
3. In **Credentials & Basic Info**, copy the **App ID** and **App Secret**.
|
||||
4. Enable the **Bot** capability for the app.
|
||||
5. Run `hermes gateway setup`, select **Feishu / Lark**, and enter the credentials when prompted.
|
||||
|
||||
:::warning
|
||||
Keep the App Secret private. Anyone with it can impersonate your app.
|
||||
:::
|
||||
|
||||
### Configure Permissions
|
||||
|
||||
In the Feishu developer console, go to **Permission Management** and add the following scopes. You can bulk-import them in the permissions page.
|
||||
|
||||
**Required permissions:**
|
||||
|
||||
| Scope | Purpose |
|
||||
|-------|---------|
|
||||
| `im:message` | Receive and read messages |
|
||||
| `im:message:send_as_bot` | Send messages as the bot |
|
||||
| `im:resource` | Access images, files, and audio sent by users |
|
||||
| `im:chat` | Access chat/group metadata |
|
||||
| `im:chat:readonly` | Read chat list and membership |
|
||||
|
||||
**Recommended permissions (for full functionality):**
|
||||
|
||||
| Scope | Purpose |
|
||||
|-------|---------|
|
||||
| `im:message.reactions:readonly` | Receive emoji reaction events |
|
||||
| `admin:app.info:readonly` | Auto-detect bot identity for @mention gating |
|
||||
| `contact:user.id:readonly` | Resolve user IDs for allowlist matching |
|
||||
|
||||
### Configure Events
|
||||
|
||||
In **Events and Callbacks**:
|
||||
|
||||
1. Set the connection mode to **Long Connection (WebSocket)** (recommended) or configure a webhook URL
|
||||
2. In the **Event Configuration** section, subscribe to:
|
||||
- `im.message.receive_v1` — required for receiving messages
|
||||
|
||||
### Publish the App
|
||||
|
||||
After configuring permissions and events, go to **Version Management** and publish a new version of the app. The permissions won't take effect until a version is published and approved (for enterprise apps, this may require admin approval).
|
||||
|
||||
## Step 2: Choose a Connection Mode
|
||||
|
||||
### Recommended: WebSocket mode
|
||||
|
||||
Use WebSocket mode when Hermes runs on your laptop, workstation, or a private server. No public URL is required. The official Lark SDK opens and maintains a persistent outbound WebSocket connection with automatic reconnection.
|
||||
|
||||
```bash
|
||||
FEISHU_CONNECTION_MODE=websocket
|
||||
```
|
||||
|
||||
**Requirements:** The `websockets` Python package must be installed. The SDK handles connection lifecycle, heartbeats, and auto-reconnection internally.
|
||||
|
||||
**How it works:** The adapter runs the Lark SDK's WebSocket client in a background executor thread. Inbound events (messages, reactions, card actions) are dispatched to the main asyncio loop. On disconnect, the SDK will attempt to reconnect automatically.
|
||||
|
||||
### Optional: Webhook mode
|
||||
|
||||
Use webhook mode only when you already run Hermes behind a reachable HTTP endpoint.
|
||||
|
||||
```bash
|
||||
FEISHU_CONNECTION_MODE=webhook
|
||||
```
|
||||
|
||||
In webhook mode, Hermes starts an HTTP server (via `aiohttp`) and serves a Feishu endpoint at:
|
||||
|
||||
```text
|
||||
/feishu/webhook
|
||||
```
|
||||
|
||||
**Requirements:** The `aiohttp` Python package must be installed.
|
||||
|
||||
You can customize the webhook server bind address and path:
|
||||
|
||||
```bash
|
||||
FEISHU_WEBHOOK_HOST=127.0.0.1 # default: 127.0.0.1
|
||||
FEISHU_WEBHOOK_PORT=8765 # default: 8765
|
||||
FEISHU_WEBHOOK_PATH=/feishu/webhook # default: /feishu/webhook
|
||||
```
|
||||
|
||||
When Feishu sends a URL verification challenge (`type: url_verification`), the webhook responds automatically so you can complete the subscription setup in the Feishu developer console. The challenge response is gated on `FEISHU_VERIFICATION_TOKEN` when set — challenge requests with a missing or mismatched token are rejected so an unauthenticated remote cannot prove endpoint control by echoing attacker-controlled challenge data.
|
||||
|
||||
## Step 3: Configure Hermes
|
||||
|
||||
### Option A: Interactive Setup
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **Feishu / Lark** and fill in the prompts.
|
||||
|
||||
### Option B: Manual Configuration
|
||||
|
||||
Add the following to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
FEISHU_APP_ID=cli_xxx
|
||||
FEISHU_APP_SECRET=secret_xxx
|
||||
FEISHU_DOMAIN=feishu
|
||||
FEISHU_CONNECTION_MODE=websocket
|
||||
|
||||
# Optional but strongly recommended
|
||||
FEISHU_ALLOWED_USERS=ou_xxx,ou_yyy
|
||||
FEISHU_HOME_CHANNEL=oc_xxx
|
||||
```
|
||||
|
||||
`FEISHU_DOMAIN` accepts:
|
||||
|
||||
- `feishu` for Feishu China
|
||||
- `lark` for Lark international
|
||||
|
||||
## Step 4: Start the Gateway
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
Then message the bot from Feishu/Lark to confirm that the connection is live.
|
||||
|
||||
## Home Chat
|
||||
|
||||
Use `/set-home` in a Feishu/Lark chat to mark it as the home channel for cron job results and cross-platform notifications.
|
||||
|
||||
You can also preconfigure it:
|
||||
|
||||
```bash
|
||||
FEISHU_HOME_CHANNEL=oc_xxx
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### User Allowlist
|
||||
|
||||
For production use, set an allowlist of Feishu Open IDs:
|
||||
|
||||
```bash
|
||||
FEISHU_ALLOWED_USERS=ou_xxx,ou_yyy
|
||||
```
|
||||
|
||||
If you leave the allowlist empty, anyone who can reach the bot may be able to use it. In group chats, the allowlist is checked against the sender's open_id before the message is processed.
|
||||
|
||||
### Webhook Encryption Key
|
||||
|
||||
When running in webhook mode, set an encryption key to enable signature verification of inbound webhook payloads:
|
||||
|
||||
```bash
|
||||
FEISHU_ENCRYPT_KEY=your-encrypt-key
|
||||
```
|
||||
|
||||
This key is found in the **Event Subscriptions** section of your Feishu app configuration. When set, the adapter verifies every webhook request using the signature algorithm:
|
||||
|
||||
```
|
||||
SHA256(timestamp + nonce + encrypt_key + body)
|
||||
```
|
||||
|
||||
The computed hash is compared against the `x-lark-signature` header using timing-safe comparison. Requests with invalid or missing signatures are rejected with HTTP 401.
|
||||
|
||||
:::tip
|
||||
In WebSocket mode, signature verification is handled by the SDK itself, so `FEISHU_ENCRYPT_KEY` is optional. In webhook mode, it is strongly recommended for production.
|
||||
:::
|
||||
|
||||
### Verification Token
|
||||
|
||||
An additional layer of authentication that checks the `token` field inside webhook payloads:
|
||||
|
||||
```bash
|
||||
FEISHU_VERIFICATION_TOKEN=your-verification-token
|
||||
```
|
||||
|
||||
This token is also found in the **Event Subscriptions** section of your Feishu app. When set, every inbound webhook payload must contain a matching `token` in its `header` object. Mismatched tokens are rejected with HTTP 401.
|
||||
|
||||
Both `FEISHU_ENCRYPT_KEY` and `FEISHU_VERIFICATION_TOKEN` can be used together for defense in depth.
|
||||
|
||||
## Group Message Policy
|
||||
|
||||
The `FEISHU_GROUP_POLICY` environment variable controls whether and how Hermes responds in group chats:
|
||||
|
||||
```bash
|
||||
FEISHU_GROUP_POLICY=allowlist # default
|
||||
```
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `open` | Hermes responds to @mentions from any user in any group. |
|
||||
| `allowlist` | Hermes only responds to @mentions from users listed in `FEISHU_ALLOWED_USERS`. |
|
||||
| `disabled` | Hermes ignores all group messages entirely. |
|
||||
|
||||
In all modes, the bot must be explicitly @mentioned (or @all) in the group before the message is processed. Direct messages always bypass this gate.
|
||||
|
||||
Set `FEISHU_REQUIRE_MENTION=false` to let Hermes read all group traffic without requiring an @mention:
|
||||
|
||||
```bash
|
||||
FEISHU_REQUIRE_MENTION=false
|
||||
```
|
||||
|
||||
For per-chat control, set `require_mention` on a `group_rules` entry — see [Per-Group Access Control](#per-group-access-control) below.
|
||||
|
||||
### Bot Identity
|
||||
|
||||
Hermes auto-detects the bot's `open_id` and display name on startup. You only need to set these manually when auto-detection cannot reach the Feishu API, or when your app uses tenant-scoped user IDs:
|
||||
|
||||
```bash
|
||||
FEISHU_BOT_OPEN_ID=ou_xxx # only when auto-detection fails
|
||||
FEISHU_BOT_USER_ID=xxx # required if your app uses sender_id_type=user_id
|
||||
FEISHU_BOT_NAME=MyBot # only when auto-detection fails
|
||||
```
|
||||
|
||||
## Bot-to-Bot Messaging
|
||||
|
||||
By default Hermes ignores messages sent by other bots. Enable bot-to-bot messaging when you want Hermes to participate in A2A orchestration or receive notifications from other bots in the same group.
|
||||
|
||||
```bash
|
||||
FEISHU_ALLOW_BOTS=mentions # default: none
|
||||
```
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `none` | Ignore all messages from other bots (default). |
|
||||
| `mentions` | Accept only when the peer bot @mentions Hermes. |
|
||||
| `all` | Accept every peer bot message. |
|
||||
|
||||
Also configurable as `feishu.allow_bots` in `config.yaml` (env wins when both are set).
|
||||
|
||||
Peer bots do not need to be added to `FEISHU_ALLOWED_USERS` — that allowlist applies to human senders only.
|
||||
|
||||
Grant the `application:bot.basic_info:read` scope to display peer bot names; without it, peer bots still route correctly but appear as their `open_id`.
|
||||
|
||||
## Interactive Card Actions
|
||||
|
||||
When users click buttons or interact with interactive cards sent by the bot, the adapter routes these as synthetic `/card` command events:
|
||||
|
||||
- Button clicks become: `/card button {"key": "value", ...}`
|
||||
- The action's `value` payload from the card definition is included as JSON.
|
||||
- Card actions are deduplicated with a 15-minute window to prevent double processing.
|
||||
|
||||
Gateway-driven update prompts use a native Feishu `Yes` / `No` card instead of falling back to plain text replies. When `hermes update --gateway` needs confirmation, the adapter records the selected answer in Hermes's `.update_response` file and replaces the card inline with a resolved state.
|
||||
|
||||
Card action events are dispatched with `MessageType.COMMAND`, so they flow through the normal command processing pipeline.
|
||||
|
||||
This is also how **command approval** works — when the agent needs to run a dangerous command, it sends an interactive card with Allow Once / Session / Always / Deny buttons. The user clicks a button, and the card action callback delivers the approval decision back to the agent.
|
||||
|
||||
### Required Feishu App Configuration
|
||||
|
||||
Interactive cards require **three** configuration steps in the Feishu Developer Console. Missing any of them causes error **200340** when users click card buttons.
|
||||
|
||||
1. **Subscribe to the card action event:**
|
||||
In **Event Subscriptions**, add `card.action.trigger` to your subscribed events.
|
||||
|
||||
2. **Enable the Interactive Card capability:**
|
||||
In **App Features > Bot**, ensure the **Interactive Card** toggle is enabled. This tells Feishu that your app can receive card action callbacks.
|
||||
|
||||
3. **Configure the Card Request URL (webhook mode only):**
|
||||
In **App Features > Bot > Message Card Request URL**, set the URL to the same endpoint as your event webhook (e.g. `https://your-server:8765/feishu/webhook`). In WebSocket mode this is handled automatically by the SDK.
|
||||
|
||||
:::warning
|
||||
Without all three steps, Feishu will successfully *send* interactive cards (sending only requires `im:message:send` permission), but clicking any button will return error 200340. The card appears to work — the error only surfaces when a user interacts with it.
|
||||
:::
|
||||
|
||||
## Document Comment Intelligent Reply
|
||||
|
||||
Beyond chat, the adapter can also answer `@`-mentions left on **Feishu/Lark documents**. When a user comments on a document (local text selection or whole-doc comment) and @-mentions the bot, Hermes reads the document plus the surrounding comment thread and posts an LLM reply inline on the thread.
|
||||
|
||||
Powered by the `drive.notice.comment_add_v1` event, the handler:
|
||||
|
||||
- Fetches the document content and comment timeline in parallel (20 messages for whole-doc threads, 12 for local-selection threads).
|
||||
- Runs the agent with the `feishu_doc` + `feishu_drive` toolsets scoped to that single comment session.
|
||||
- Chunks replies at 4000 chars and posts them back as threaded replies.
|
||||
- Caches per-document sessions for 1 hour with a 50-message cap so follow-up comments on the same doc keep context.
|
||||
|
||||
### 3-Tier Access Control
|
||||
|
||||
Document-comment replies are **explicit-grant only** — there is no implicit allow-all mode. Permissions resolve in this order (first match wins, per field):
|
||||
|
||||
1. **Exact doc** — rule scoped to a specific document token.
|
||||
2. **Wildcard** — rule that matches a pattern of docs.
|
||||
3. **Top-level** — default rule for the workspace.
|
||||
|
||||
Two policies are available per rule:
|
||||
|
||||
- **`allowlist`** — a static list of users / tenants.
|
||||
- **`pairing`** — static list ∪ runtime-approved store. Useful for rollouts where moderators can grant access live.
|
||||
|
||||
Rules live in `~/.hermes/feishu_comment_rules.json` (pairing grants in `~/.hermes/feishu_comment_pairing.json`) with mtime-cached hot-reload — edits take effect on the next comment event without restarting the gateway.
|
||||
|
||||
CLI:
|
||||
|
||||
```bash
|
||||
# Inspect current rules and pairing state
|
||||
python -m gateway.platforms.feishu_comment_rules status
|
||||
|
||||
# Simulate an access check for a specific doc + user
|
||||
python -m gateway.platforms.feishu_comment_rules check <fileType:fileToken> <user_open_id>
|
||||
|
||||
# Manage pairing grants at runtime
|
||||
python -m gateway.platforms.feishu_comment_rules pairing list
|
||||
python -m gateway.platforms.feishu_comment_rules pairing add <user_open_id>
|
||||
python -m gateway.platforms.feishu_comment_rules pairing remove <user_open_id>
|
||||
```
|
||||
|
||||
### Required Feishu App Configuration
|
||||
|
||||
On top of the chat/card permissions already granted, add the drive comment event:
|
||||
|
||||
- Subscribe to `drive.notice.comment_add_v1` in **Event Subscriptions**.
|
||||
- Grant the `docs:doc:readonly` and `drive:drive:readonly` scopes so the handler can read document content.
|
||||
|
||||
## Meeting Invitation Events
|
||||
|
||||
You can invite the Hermes Feishu/Lark bot into a video meeting the same way you invite a human participant. When the bot receives the meeting invitation event, Hermes can automatically start an agent turn that attempts to join the meeting.
|
||||
|
||||
Powered by the `vc.bot.meeting_invited_v1` event, the flow is:
|
||||
|
||||
- A user invites the bot to a Feishu/Lark video meeting.
|
||||
- Feishu/Lark sends Hermes the meeting invitation event.
|
||||
- Hermes extracts the inviter, meeting topic, and meeting number.
|
||||
- If the inviter is authorized by the normal gateway allowlist or pairing policy, the agent receives the meeting number and tries to join automatically.
|
||||
- If the invite is malformed, or the agent cannot join, Hermes drops the event or replies to the inviter with a concise explanation.
|
||||
|
||||
Malformed invitations that do not include both an inviter and a `meeting_no` are ignored.
|
||||
|
||||
### Required Feishu App Configuration
|
||||
|
||||
On top of the chat/card permissions already granted, add the video-meeting invitation event:
|
||||
|
||||
- Subscribe to `vc.bot.meeting_invited_v1` in **Event Subscriptions**.
|
||||
- Enable the Video Conferencing permission scope prompted by the Feishu/Lark developer console for that event.
|
||||
- Keep `im:message` and `im:message:send_as_bot` enabled so Hermes can reply to the inviter.
|
||||
- Ensure the gateway user allowlist or pairing policy authorizes the inviter. Meeting invitations do not bypass normal gateway access checks.
|
||||
|
||||
## Media Support
|
||||
|
||||
### Inbound (receiving)
|
||||
|
||||
The adapter receives and caches the following media types from users:
|
||||
|
||||
| Type | Extensions | How it's processed |
|
||||
|------|-----------|-------------------|
|
||||
| **Images** | .jpg, .jpeg, .png, .gif, .webp, .bmp | Downloaded via Feishu API and cached locally |
|
||||
| **Audio** | .ogg, .mp3, .wav, .m4a, .aac, .flac, .opus, .webm | Downloaded and cached; small text files are auto-extracted |
|
||||
| **Video** | .mp4, .mov, .avi, .mkv, .webm, .m4v, .3gp | Downloaded and cached as documents |
|
||||
| **Files** | .pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, and more | Downloaded and cached as documents |
|
||||
|
||||
Media from rich-text (post) messages, including inline images and file attachments, is also extracted and cached.
|
||||
|
||||
For small text-based documents (.txt, .md), the file content is automatically injected into the message text so the agent can read it directly without needing tools.
|
||||
|
||||
### Outbound (sending)
|
||||
|
||||
| Method | What it sends |
|
||||
|--------|--------------|
|
||||
| `send` | Text or rich post messages (auto-detected based on markdown content) |
|
||||
| `send_image` / `send_image_file` | Uploads image to Feishu, then sends as native image bubble (with optional caption) |
|
||||
| `send_document` | Uploads file to Feishu API, then sends as file attachment |
|
||||
| `send_voice` | Uploads audio file as a Feishu file attachment |
|
||||
| `send_video` | Uploads video and sends as native media message |
|
||||
| `send_animation` | GIFs are downgraded to file attachments (Feishu has no native GIF bubble) |
|
||||
|
||||
File upload routing is automatic based on extension:
|
||||
|
||||
- `.ogg`, `.opus` → uploaded as `opus` audio
|
||||
- `.mp4`, `.mov`, `.avi`, `.m4v` → uploaded as `mp4` media
|
||||
- `.pdf`, `.doc(x)`, `.xls(x)`, `.ppt(x)` → uploaded with their document type
|
||||
- Everything else → uploaded as a generic stream file
|
||||
|
||||
## Markdown Rendering and Post Fallback
|
||||
|
||||
When outbound text contains markdown formatting (headings, bold, lists, code blocks, links, etc.), the adapter automatically sends it as a Feishu **post** message with an embedded `md` tag rather than as plain text. This enables rich rendering in the Feishu client.
|
||||
|
||||
If the Feishu API rejects the post payload (e.g., due to unsupported markdown constructs), the adapter automatically falls back to sending as plain text with markdown stripped. This two-stage fallback ensures messages are always delivered.
|
||||
|
||||
Plain text messages (no markdown detected) are sent as the simple `text` message type.
|
||||
|
||||
## Processing Status Reactions
|
||||
|
||||
While the agent is working, the bot shows a `Typing` reaction on your message. It's cleared when the reply arrives, or replaced with `CrossMark` if processing failed.
|
||||
|
||||
Set `FEISHU_REACTIONS=false` to turn it off.
|
||||
|
||||
## Burst Protection and Batching
|
||||
|
||||
The adapter includes debouncing for rapid message bursts to avoid overwhelming the agent:
|
||||
|
||||
### Text Batching
|
||||
|
||||
When a user sends multiple text messages in quick succession, they are merged into a single event before being dispatched:
|
||||
|
||||
| Setting | Env Var | Default |
|
||||
|---------|---------|---------|
|
||||
| Quiet period | `HERMES_FEISHU_TEXT_BATCH_DELAY_SECONDS` | 0.6s |
|
||||
| Max messages per batch | `HERMES_FEISHU_TEXT_BATCH_MAX_MESSAGES` | 8 |
|
||||
| Max characters per batch | `HERMES_FEISHU_TEXT_BATCH_MAX_CHARS` | 4000 |
|
||||
|
||||
### Media Batching
|
||||
|
||||
Multiple media attachments sent in quick succession (e.g., dragging several images) are merged into a single event:
|
||||
|
||||
| Setting | Env Var | Default |
|
||||
|---------|---------|---------|
|
||||
| Quiet period | `HERMES_FEISHU_MEDIA_BATCH_DELAY_SECONDS` | 0.8s |
|
||||
|
||||
### Per-Chat Serialization
|
||||
|
||||
Messages within the same chat are processed serially (one at a time) to maintain conversation coherence. Each chat has its own lock, so messages in different chats are processed concurrently.
|
||||
|
||||
## Rate Limiting (Webhook Mode)
|
||||
|
||||
In webhook mode, the adapter enforces per-IP rate limiting to protect against abuse:
|
||||
|
||||
- **Window:** 60-second sliding window
|
||||
- **Limit:** 120 requests per window per (app_id, path, IP) triple
|
||||
- **Tracking cap:** Up to 4096 unique keys tracked (prevents unbounded memory growth)
|
||||
|
||||
Requests that exceed the limit receive HTTP 429 (Too Many Requests).
|
||||
|
||||
### Webhook Anomaly Tracking
|
||||
|
||||
The adapter tracks consecutive error responses per IP address. After 25 consecutive errors from the same IP within a 6-hour window, a warning is logged. This helps detect misconfigured clients or probing attempts.
|
||||
|
||||
Additional webhook protections:
|
||||
- **Body size limit:** 1 MB maximum
|
||||
- **Body read timeout:** 30 seconds
|
||||
- **Content-Type enforcement:** Only `application/json` is accepted
|
||||
|
||||
## WebSocket Tuning
|
||||
|
||||
When using `websocket` mode, you can customize reconnect and ping behavior:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
feishu:
|
||||
extra:
|
||||
ws_reconnect_interval: 120 # Seconds between reconnect attempts (default: 120)
|
||||
ws_ping_interval: 30 # Seconds between WebSocket pings (optional; SDK default if unset)
|
||||
```
|
||||
|
||||
| Setting | Config key | Default | Description |
|
||||
|---------|-----------|---------|-------------|
|
||||
| Reconnect interval | `ws_reconnect_interval` | 120s | How long to wait between reconnection attempts |
|
||||
| Ping interval | `ws_ping_interval` | _(SDK default)_ | Frequency of WebSocket keepalive pings |
|
||||
|
||||
## Per-Group Access Control
|
||||
|
||||
Beyond the global `FEISHU_GROUP_POLICY`, you can set fine-grained rules per group chat using `group_rules` in config.yaml:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
feishu:
|
||||
extra:
|
||||
default_group_policy: "open" # Default for groups not in group_rules
|
||||
admins: # Users who can manage bot settings
|
||||
- "ou_admin_open_id"
|
||||
group_rules:
|
||||
"oc_group_chat_id_1":
|
||||
policy: "allowlist" # open | allowlist | blacklist | admin_only | disabled
|
||||
allowlist:
|
||||
- "ou_user_open_id_1"
|
||||
- "ou_user_open_id_2"
|
||||
"oc_group_chat_id_2":
|
||||
policy: "admin_only"
|
||||
"oc_group_chat_id_3":
|
||||
policy: "blacklist"
|
||||
blacklist:
|
||||
- "ou_blocked_user"
|
||||
"oc_free_chat":
|
||||
policy: "open"
|
||||
require_mention: false # overrides FEISHU_REQUIRE_MENTION for this chat
|
||||
```
|
||||
|
||||
| Policy | Description |
|
||||
|--------|-------------|
|
||||
| `open` | Anyone in the group can use the bot |
|
||||
| `allowlist` | Only users in the group's `allowlist` can use the bot |
|
||||
| `blacklist` | Everyone except users in the group's `blacklist` can use the bot |
|
||||
| `admin_only` | Only users in the global `admins` list can use the bot in this group |
|
||||
| `disabled` | Bot ignores all messages in this group |
|
||||
|
||||
Set `require_mention: false` on a `group_rules` entry to skip the @-mention requirement for that specific chat. When omitted, the chat inherits the global `FEISHU_REQUIRE_MENTION` value.
|
||||
|
||||
Groups not listed in `group_rules` fall back to `default_group_policy` (defaults to the value of `FEISHU_GROUP_POLICY`).
|
||||
|
||||
## Deduplication
|
||||
|
||||
Inbound messages are deduplicated using message IDs with a 24-hour TTL. The dedup state is persisted across restarts to `~/.hermes/feishu_seen_message_ids.json`.
|
||||
|
||||
| Setting | Env Var | Default |
|
||||
|---------|---------|---------|
|
||||
| Cache size | `HERMES_FEISHU_DEDUP_CACHE_SIZE` | 2048 entries |
|
||||
|
||||
## All Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `FEISHU_APP_ID` | ✅ | — | Feishu/Lark App ID |
|
||||
| `FEISHU_APP_SECRET` | ✅ | — | Feishu/Lark App Secret |
|
||||
| `FEISHU_DOMAIN` | — | `feishu` | `feishu` (China) or `lark` (international) |
|
||||
| `FEISHU_CONNECTION_MODE` | — | `websocket` | `websocket` or `webhook` |
|
||||
| `FEISHU_ALLOWED_USERS` | — | _(empty)_ | Comma-separated open_id list for user allowlist |
|
||||
| `FEISHU_ALLOW_BOTS` | — | `none` | Accept messages from other bots: `none`, `mentions`, or `all` |
|
||||
| `FEISHU_REQUIRE_MENTION` | — | `true` | Whether group messages must @mention the bot |
|
||||
| `FEISHU_HOME_CHANNEL` | — | — | Chat ID for cron/notification output |
|
||||
| `FEISHU_ENCRYPT_KEY` | — | _(empty)_ | Encrypt key for webhook signature verification |
|
||||
| `FEISHU_VERIFICATION_TOKEN` | — | _(empty)_ | Verification token for webhook payload auth |
|
||||
| `FEISHU_GROUP_POLICY` | — | `allowlist` | Group message policy: `open`, `allowlist`, `disabled` |
|
||||
| `FEISHU_BOT_OPEN_ID` | — | _(empty)_ | Bot's open_id (for @mention detection) |
|
||||
| `FEISHU_BOT_USER_ID` | — | _(empty)_ | Bot's user_id (for @mention detection) |
|
||||
| `FEISHU_BOT_NAME` | — | _(empty)_ | Bot's display name (for @mention detection) |
|
||||
| `FEISHU_WEBHOOK_HOST` | — | `127.0.0.1` | Webhook server bind address |
|
||||
| `FEISHU_WEBHOOK_PORT` | — | `8765` | Webhook server port |
|
||||
| `FEISHU_WEBHOOK_PATH` | — | `/feishu/webhook` | Webhook endpoint path |
|
||||
| `HERMES_FEISHU_DEDUP_CACHE_SIZE` | — | `2048` | Max deduplicated message IDs to track |
|
||||
| `HERMES_FEISHU_TEXT_BATCH_DELAY_SECONDS` | — | `0.6` | Text burst debounce quiet period |
|
||||
| `HERMES_FEISHU_TEXT_BATCH_MAX_MESSAGES` | — | `8` | Max messages merged per text batch |
|
||||
| `HERMES_FEISHU_TEXT_BATCH_MAX_CHARS` | — | `4000` | Max characters merged per text batch |
|
||||
| `HERMES_FEISHU_MEDIA_BATCH_DELAY_SECONDS` | — | `0.8` | Media burst debounce quiet period |
|
||||
|
||||
WebSocket and per-group ACL settings are configured via `config.yaml` under `platforms.feishu.extra` (see [WebSocket Tuning](#websocket-tuning) and [Per-Group Access Control](#per-group-access-control) above).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| `lark-oapi not installed` | Install the SDK: `pip install lark-oapi` |
|
||||
| `websockets not installed; websocket mode unavailable` | Install websockets: `pip install websockets` |
|
||||
| `aiohttp not installed; webhook mode unavailable` | Install aiohttp: `pip install aiohttp` |
|
||||
| `FEISHU_APP_ID or FEISHU_APP_SECRET not set` | Set both env vars or configure via `hermes gateway setup` |
|
||||
| `Another local Hermes gateway is already using this Feishu app_id` | Only one Hermes instance can use the same app_id at a time. Stop the other gateway first. |
|
||||
| Bot doesn't respond in groups | Ensure the bot is @mentioned, check `FEISHU_GROUP_POLICY`, and verify the sender is in `FEISHU_ALLOWED_USERS` if policy is `allowlist` |
|
||||
| `Webhook rejected: invalid verification token` | Ensure `FEISHU_VERIFICATION_TOKEN` matches the token in your Feishu app's Event Subscriptions config |
|
||||
| `Webhook rejected: invalid signature` | Ensure `FEISHU_ENCRYPT_KEY` matches the encrypt key in your Feishu app config |
|
||||
| Post messages show as plain text | The Feishu API rejected the post payload; this is normal fallback behavior. Check logs for details. |
|
||||
| Images/files not received by bot | Grant `im:message` and `im:resource` permission scopes to your Feishu app |
|
||||
| Bot identity not auto-detected | Usually a transient network issue reaching Feishu's bot info endpoint. Set `FEISHU_BOT_OPEN_ID` and `FEISHU_BOT_NAME` manually as a workaround. |
|
||||
| Peer bot messages still ignored after enabling `FEISHU_ALLOW_BOTS` | Hermes can't identify itself yet — set `FEISHU_BOT_OPEN_ID` (and `FEISHU_BOT_USER_ID` if your app uses `sender_id_type=user_id`). |
|
||||
| Peer bots show as `ou_xxxxxx` instead of by name | Grant the `application:bot.basic_info:read` scope. |
|
||||
| Error 200340 when clicking approval buttons | Enable **Interactive Card** capability and configure **Card Request URL** in the Feishu Developer Console. See [Required Feishu App Configuration](#required-feishu-app-configuration) above. |
|
||||
| `Webhook rate limit exceeded` | More than 120 requests/minute from the same IP. This is usually a misconfiguration or loop. |
|
||||
|
||||
## Toolset
|
||||
|
||||
Feishu / Lark uses the `hermes-feishu` platform preset, which includes the same core tools as Telegram and other gateway-based messaging platforms.
|
||||
@@ -0,0 +1,386 @@
|
||||
---
|
||||
sidebar_position: 12
|
||||
title: "Google Chat"
|
||||
description: "Set up Hermes Agent as a Google Chat bot using Cloud Pub/Sub"
|
||||
---
|
||||
|
||||
# Google Chat Setup
|
||||
|
||||
Connect Hermes Agent to Google Chat as a bot. The integration uses Cloud Pub/Sub
|
||||
pull subscriptions for inbound events and the Chat REST API for outbound messages.
|
||||
Equivalent ergonomics to Slack Socket Mode or Telegram long-polling: your Hermes
|
||||
process does not need a public URL, a tunnel, or a TLS certificate. It connects,
|
||||
authenticates, and listens on a subscription — the same way a Telegram bot listens
|
||||
on a token.
|
||||
|
||||
> Run `hermes gateway setup` and pick **Google Chat** for a guided walk-through.
|
||||
|
||||
:::note Workspace edition
|
||||
Google Chat is part of Google Workspace. You can use this integration with a
|
||||
personal Workspace (`@yourdomain.com` registered through Google) or a work
|
||||
Workspace where you have the Admin rights to publish an app. Gmail-only accounts
|
||||
cannot host Chat apps.
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
| Component | Value |
|
||||
|-----------|-------|
|
||||
| **Libraries** | `google-cloud-pubsub`, `google-api-python-client`, `google-auth` |
|
||||
| **Inbound transport** | Cloud Pub/Sub pull subscription (no public endpoint) |
|
||||
| **Outbound transport** | Chat REST API (`chat.googleapis.com`) |
|
||||
| **Authentication** | Service Account JSON with `roles/pubsub.subscriber` on the subscription |
|
||||
| **User identification** | Chat resource names (`users/{id}`) + email |
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create or pick a GCP project
|
||||
|
||||
You need a Google Cloud project to host the Pub/Sub topic. If you don't have one,
|
||||
create it at [console.cloud.google.com](https://console.cloud.google.com) —
|
||||
personal accounts get a free tier that easily covers bot traffic.
|
||||
|
||||
Note the project ID (e.g., `my-chat-bot-123`). You'll use it in every subsequent
|
||||
step.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Enable two APIs
|
||||
|
||||
In the console, go to **APIs & Services → Library** and enable:
|
||||
|
||||
- **Google Chat API**
|
||||
- **Cloud Pub/Sub API**
|
||||
|
||||
Both are free for the volumes a personal bot generates.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create a Service Account
|
||||
|
||||
**IAM & Admin → Service Accounts → Create Service Account.**
|
||||
|
||||
- Name: `hermes-chat-bot`
|
||||
- Skip the "Grant this service account access to project" step. IAM on the specific
|
||||
subscription is all you need — do **NOT** grant project-level Pub/Sub roles.
|
||||
|
||||
After creation, open the SA, go to **Keys → Add Key → Create new key → JSON** and
|
||||
download the file. Save it somewhere only Hermes can read (e.g.,
|
||||
`~/.hermes/google-chat-sa.json`, `chmod 600`).
|
||||
|
||||
:::caution There is NO "Chat Bot Caller" role
|
||||
A common mistake is to search for a Chat-specific IAM role and grant it at the
|
||||
project level. That role doesn't exist. Chat bot authority comes from being
|
||||
installed in a space, not from IAM. All your SA needs is Pub/Sub subscriber on
|
||||
the subscription you create in the next step.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Create the Pub/Sub topic and subscription
|
||||
|
||||
**Pub/Sub → Topics → Create topic.**
|
||||
|
||||
- Topic ID: `hermes-chat-events`
|
||||
- Leave the defaults for everything else.
|
||||
|
||||
After creation, the topic's detail page has a **Subscriptions** tab. Create one:
|
||||
|
||||
- Subscription ID: `hermes-chat-events-sub`
|
||||
- Delivery type: **Pull**
|
||||
- Message retention: **7 days** (so backlog survives a hermes restart)
|
||||
- Leave the rest default.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: IAM binding on the topic (critical)
|
||||
|
||||
On the **topic** (not the subscription), add an IAM principal:
|
||||
|
||||
- Principal: `chat-api-push@system.gserviceaccount.com`
|
||||
- Role: `Pub/Sub Publisher`
|
||||
|
||||
Without this, Google Chat cannot publish events to your topic and your bot will
|
||||
never receive anything.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: IAM binding on the subscription
|
||||
|
||||
On the **subscription**, add your own Service Account as a principal:
|
||||
|
||||
- Principal: `hermes-chat-bot@<your-project>.iam.gserviceaccount.com`
|
||||
- Role: `Pub/Sub Subscriber`
|
||||
|
||||
Also grant `Pub/Sub Viewer` on the same subscription — Hermes calls
|
||||
`subscription.get()` at startup as a reachability check.
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Configure the Chat app
|
||||
|
||||
Go to **APIs & Services → Google Chat API → Configuration**.
|
||||
|
||||
- **App name**: whatever you want users to see ("Hermes" is reasonable).
|
||||
- **Avatar URL**: any public PNG (Google has some defaults).
|
||||
- **Description**: a short sentence shown in the app directory.
|
||||
- **Functionality**: enable **Receive 1:1 messages** and **Join spaces and group
|
||||
conversations**.
|
||||
- **Connection settings**: select **Cloud Pub/Sub**, enter the topic name
|
||||
`projects/<your-project>/topics/hermes-chat-events`.
|
||||
- **Visibility**: restrict to your workspace (or specific users) — do not publish
|
||||
to everyone while you're testing.
|
||||
|
||||
Save.
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Install the bot in a test space
|
||||
|
||||
Open Google Chat in a browser. Start a DM with your app by searching for its name
|
||||
in the **+ New Chat** menu. The first time you message it, Google sends an
|
||||
`ADDED_TO_SPACE` event that Hermes uses to cache the bot's own `users/{id}` for
|
||||
self-message filtering.
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Configure Hermes
|
||||
|
||||
Add the Google Chat section to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
GOOGLE_CHAT_PROJECT_ID=my-chat-bot-123
|
||||
GOOGLE_CHAT_SUBSCRIPTION_NAME=projects/my-chat-bot-123/subscriptions/hermes-chat-events-sub
|
||||
GOOGLE_CHAT_SERVICE_ACCOUNT_JSON=/home/you/.hermes/google-chat-sa.json
|
||||
|
||||
# Authorization — paste the emails of people allowed to talk to the bot
|
||||
GOOGLE_CHAT_ALLOWED_USERS=you@yourdomain.com,coworker@yourdomain.com
|
||||
|
||||
# Optional
|
||||
GOOGLE_CHAT_HOME_CHANNEL=spaces/AAAA... # default delivery destination for cron jobs
|
||||
GOOGLE_CHAT_MAX_MESSAGES=1 # Pub/Sub FlowControl; 1 serializes commands per session
|
||||
GOOGLE_CHAT_MAX_BYTES=16777216 # 16 MiB — cap on in-flight message bytes
|
||||
```
|
||||
|
||||
The project ID also falls back to `GOOGLE_CLOUD_PROJECT`, and the SA path falls
|
||||
back to `GOOGLE_APPLICATION_CREDENTIALS` — use whichever convention you prefer.
|
||||
|
||||
Install the dependencies the Google Chat adapter needs (no Hermes extra is currently published — install them directly):
|
||||
|
||||
```bash
|
||||
pip install google-cloud-pubsub google-api-python-client google-auth google-auth-oauthlib
|
||||
```
|
||||
|
||||
Start the gateway:
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
You should see a log line like:
|
||||
|
||||
```
|
||||
[GoogleChat] Connected; project=my-chat-bot-123, subscription=<redacted>,
|
||||
bot_user_id=users/XXXX, flow_control(msgs=1, bytes=16777216)
|
||||
```
|
||||
|
||||
Send "hola" in the test DM. The bot posts a "Hermes is thinking…" marker, then
|
||||
edits that same message in place with the real response — no "message deleted"
|
||||
tombstones.
|
||||
|
||||
---
|
||||
|
||||
## Formatting and capabilities
|
||||
|
||||
Google Chat renders a limited markdown subset:
|
||||
|
||||
| Supported | Not supported |
|
||||
|-----------|---------------|
|
||||
| `*bold*`, `_italic_`, `~strike~`, `` `code` `` | Headings, lists |
|
||||
| Inline images via URL | Interactive Card v2 buttons (v1 of this gateway) |
|
||||
| Native file attachments (after `/setup-files` — see Step 10) | Native voice notes / circular video notes |
|
||||
|
||||
The agent's system prompt includes a Google Chat–specific hint so it knows these
|
||||
limits and avoids formatting that won't render.
|
||||
|
||||
Message size limit: 4000 characters per message. Longer agent responses are
|
||||
automatically split across multiple messages.
|
||||
|
||||
Thread support: when a user replies inside a thread, Hermes detects the
|
||||
`thread.name` and posts its reply in the same thread, so each thread gets a
|
||||
separate Hermes session.
|
||||
|
||||
---
|
||||
|
||||
## Step 10: Native attachment delivery (optional)
|
||||
|
||||
Out of the box the bot can post text, inline images via URL, and download cards
|
||||
for audio/video/documents. To deliver **native** Chat attachments — the same
|
||||
file widget you get when a human drags-and-drops a file — each user authorizes
|
||||
the bot once via a per-user OAuth flow.
|
||||
|
||||
### Why a separate flow
|
||||
|
||||
Google Chat's `media.upload` endpoint hard-rejects service-account auth:
|
||||
|
||||
> This method doesn't support app authentication with a service account.
|
||||
> Authenticate with a user account.
|
||||
|
||||
There's no IAM role or scope that fixes this. The endpoint only accepts user
|
||||
credentials. So the bot has to act *as a user* whenever it uploads a file —
|
||||
specifically, as the user who asked for the file.
|
||||
|
||||
### One-time setup (per profile)
|
||||
|
||||
1. Go to **APIs & Services → Credentials** in the same GCP project.
|
||||
2. **Create credentials → OAuth client ID → Desktop app**.
|
||||
3. Download the JSON. Move it onto the host that runs Hermes.
|
||||
4. Register the client with Hermes (run under the profile you want it scoped to):
|
||||
|
||||
```bash
|
||||
# Default profile:
|
||||
python -m plugins.platforms.google_chat.oauth \
|
||||
--client-secret /path/to/client_secret.json
|
||||
|
||||
# A named profile gets its own separate registration:
|
||||
hermes -p <profile> python -m plugins.platforms.google_chat.oauth \
|
||||
--client-secret /path/to/client_secret.json
|
||||
```
|
||||
|
||||
That writes the client secret into the active profile's Hermes home (e.g.
|
||||
`~/.hermes/google_chat_user_client_secret.json` for the default profile). The
|
||||
client secret is **profile-scoped, not shared across profiles** — each profile
|
||||
registers its own. This is deliberate: profiles are isolated auth boundaries, so
|
||||
two profiles can point at different Google OAuth apps / accounts. Register it
|
||||
once per profile that needs Google Chat attachment delivery.
|
||||
|
||||
### Per-user authorization (in chat)
|
||||
|
||||
Each user runs the flow once, in their own DM with the bot:
|
||||
|
||||
1. They send `/setup-files` to the bot. It replies with status and the next
|
||||
step.
|
||||
2. They send `/setup-files start`. The bot replies with an OAuth URL.
|
||||
3. They open the URL, click **Allow**, and watch the browser fail to load
|
||||
`http://localhost:1/?...&code=...`. That failure is expected — the auth
|
||||
code is in the URL bar.
|
||||
4. They copy the failed URL (or just the `code=...` value) and paste it back
|
||||
into chat as `/setup-files <PASTED_URL>`. The bot exchanges it for a
|
||||
refresh token.
|
||||
|
||||
The token lands at `~/.hermes/google_chat_user_tokens/<sanitized_email>.json`.
|
||||
Subsequent file requests in that user's DM use *their* token, so the bot
|
||||
uploads as them and the message lands in their space.
|
||||
|
||||
To revoke later: `/setup-files revoke` deletes only that user's token. Other
|
||||
users' tokens are untouched.
|
||||
|
||||
### Scope
|
||||
|
||||
The flow requests exactly one scope: `chat.messages.create`. That covers both
|
||||
`media.upload` and the `messages.create` that references the uploaded
|
||||
`attachmentDataRef`. No Drive, no broader Chat scopes — this is least-privilege
|
||||
on purpose.
|
||||
|
||||
### Multi-user behavior
|
||||
|
||||
When the asker has no per-user token yet, the bot falls back to a legacy
|
||||
single-user token at `~/.hermes/google_chat_user_token.json` (if present from
|
||||
a pre-multi-user install). When neither is available, the bot posts a clear
|
||||
text notice telling the asker to run `/setup-files`.
|
||||
|
||||
A user revoking only clears their own slot. A 401/403 from one user's token
|
||||
evicts only that user's cache. Users don't disrupt each other.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Bot stays silent after sending "hola."**
|
||||
|
||||
1. Check the Pub/Sub subscription has undelivered messages in the console.
|
||||
If it does, Hermes isn't authenticated — verify `GOOGLE_CHAT_SERVICE_ACCOUNT_JSON`
|
||||
and that the SA is listed as `Pub/Sub Subscriber` on the subscription.
|
||||
2. If the subscription has zero messages, Google Chat isn't publishing.
|
||||
Double-check the IAM binding on the **topic**:
|
||||
`chat-api-push@system.gserviceaccount.com` must have `Pub/Sub Publisher`.
|
||||
3. Check `hermes gateway` logs for `[GoogleChat] Connected`. If you see
|
||||
`[GoogleChat] Config validation failed`, the error message tells you which
|
||||
env var to fix.
|
||||
|
||||
**Bot replies but an error message appears instead of the agent's answer.**
|
||||
|
||||
Check logs for `[GoogleChat] Pub/Sub stream died` — if these repeat, your SA
|
||||
credentials may have been rotated or the subscription deleted. After 10 attempts
|
||||
the adapter marks itself fatal.
|
||||
|
||||
**"403 Forbidden" on every outbound message.**
|
||||
|
||||
The bot was removed from the space, or you revoked it in the Chat API console.
|
||||
Re-install it in the space (the next `ADDED_TO_SPACE` event will re-enable
|
||||
messaging automatically).
|
||||
|
||||
**Too many "Rate limit hit" warnings.**
|
||||
|
||||
The Chat API's default quotas allow 60 messages per space per minute. If your
|
||||
agent produces long streaming responses that exceed that, the adapter retries
|
||||
with exponential backoff — but you'll still see user-visible latency. Consider
|
||||
concise responses or raising the quota in the GCP console.
|
||||
|
||||
**Bot keeps posting the "/setup-files" notice instead of files.**
|
||||
|
||||
The asker has no per-user OAuth token and there's no legacy fallback. Run
|
||||
`/setup-files` in their DM and follow Step 10. After the exchange completes
|
||||
the next file request uploads natively without a gateway restart.
|
||||
|
||||
**`/setup-files start` says "No client credentials stored."**
|
||||
|
||||
The one-time setup wasn't done *for this profile* (the client secret is
|
||||
profile-scoped, so a registration under one profile won't be seen by another).
|
||||
From a terminal, run it under the profile the gateway uses:
|
||||
|
||||
```bash
|
||||
# Default profile:
|
||||
python -m plugins.platforms.google_chat.oauth \
|
||||
--client-secret /path/to/client_secret.json
|
||||
|
||||
# Named profile:
|
||||
hermes -p <profile> python -m plugins.platforms.google_chat.oauth \
|
||||
--client-secret /path/to/client_secret.json
|
||||
```
|
||||
|
||||
Then send `/setup-files start` again.
|
||||
|
||||
**`/setup-files <PASTED_URL>` says "Token exchange failed."**
|
||||
|
||||
The auth code is single-use and short-lived (typically a few minutes). Send
|
||||
`/setup-files start` to get a fresh URL and retry.
|
||||
|
||||
---
|
||||
|
||||
## Security notes
|
||||
|
||||
- **Service Account scope**: the adapter requests `chat.bot` and `pubsub` scopes.
|
||||
IAM should be the actual enforcement — grant your SA the minimum
|
||||
(`roles/pubsub.subscriber` + `roles/pubsub.viewer` on the subscription), not
|
||||
project-level or org-level Pub/Sub roles.
|
||||
- **Attachment download protection**: Hermes will only attach the SA bearer
|
||||
token to URLs whose host matches a short allowlist of Google-owned domains
|
||||
(`googleapis.com`, `drive.google.com`, `lh[3-6].googleusercontent.com`, and
|
||||
a few others). Any other host is rejected before the HTTP request, to
|
||||
protect against SSRF scenarios where a crafted event could redirect the
|
||||
bearer token to the GCE metadata service.
|
||||
- **Redaction**: Service Account emails, subscription paths, and topic paths
|
||||
are stripped from log output by `agent/redact.py`. The debug envelope dump
|
||||
(`GOOGLE_CHAT_DEBUG_RAW=1`) routes through the same redaction filter and
|
||||
logs at DEBUG level.
|
||||
- **Compliance**: if you plan to connect this bot to a regulated workspace
|
||||
(anything with a data-residency or AI-governance policy), get that approval
|
||||
before the first install.
|
||||
- **User OAuth scope**: the per-user attachment flow requests *only*
|
||||
`chat.messages.create` — the minimum that covers `media.upload` plus the
|
||||
follow-up `messages.create`. Tokens are persisted as plain JSON at
|
||||
`~/.hermes/google_chat_user_tokens/<sanitized_email>.json` (filesystem
|
||||
permissions are the protection — same model as the SA key file). Each
|
||||
token is owned by exactly one user; revoke is scoped to that user.
|
||||
@@ -0,0 +1,275 @@
|
||||
---
|
||||
title: Home Assistant
|
||||
description: Control your smart home with Hermes Agent via Home Assistant integration.
|
||||
sidebar_label: Home Assistant
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Home Assistant Integration
|
||||
|
||||
Hermes Agent integrates with [Home Assistant](https://www.home-assistant.io/) in two ways:
|
||||
|
||||
1. **Gateway platform** — subscribes to real-time state changes via WebSocket and responds to events
|
||||
2. **Smart home tools** — four LLM-callable tools for querying and controlling devices via the REST API
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Create a Long-Lived Access Token
|
||||
|
||||
1. Open your Home Assistant instance
|
||||
2. Go to your **Profile** (click your name in the sidebar)
|
||||
3. Scroll to **Long-Lived Access Tokens**
|
||||
4. Click **Create Token**, give it a name like "Hermes Agent"
|
||||
5. Copy the token
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
```bash
|
||||
# Add to ~/.hermes/.env
|
||||
|
||||
# Required: your Long-Lived Access Token
|
||||
HASS_TOKEN=your-long-lived-access-token
|
||||
|
||||
# Optional: HA URL (default: http://homeassistant.local:8123)
|
||||
HASS_URL=http://192.168.1.100:8123
|
||||
```
|
||||
|
||||
:::info
|
||||
The `homeassistant` toolset is automatically enabled when `HASS_TOKEN` is set. Both the gateway platform and the device control tools activate from this single token.
|
||||
:::
|
||||
|
||||
### 3. Start the Gateway
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
Home Assistant will appear as a connected platform alongside any other messaging platforms (Telegram, Discord, etc.).
|
||||
|
||||
## Available Tools
|
||||
|
||||
Hermes Agent registers four tools for smart home control:
|
||||
|
||||
### `ha_list_entities`
|
||||
|
||||
List Home Assistant entities, optionally filtered by domain or area.
|
||||
|
||||
**Parameters:**
|
||||
- `domain` *(optional)* — Filter by entity domain: `light`, `switch`, `climate`, `sensor`, `binary_sensor`, `cover`, `fan`, `media_player`, etc.
|
||||
- `area` *(optional)* — Filter by area/room name (matches against friendly names): `living room`, `kitchen`, `bedroom`, etc.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
List all lights in the living room
|
||||
```
|
||||
|
||||
Returns entity IDs, states, and friendly names.
|
||||
|
||||
### `ha_get_state`
|
||||
|
||||
Get detailed state of a single entity, including all attributes (brightness, color, temperature setpoint, sensor readings, etc.).
|
||||
|
||||
**Parameters:**
|
||||
- `entity_id` *(required)* — The entity to query, e.g., `light.living_room`, `climate.thermostat`, `sensor.temperature`
|
||||
|
||||
**Example:**
|
||||
```
|
||||
What's the current state of climate.thermostat?
|
||||
```
|
||||
|
||||
Returns: state, all attributes, last changed/updated timestamps.
|
||||
|
||||
### `ha_list_services`
|
||||
|
||||
List available services (actions) for device control. Shows what actions can be performed on each device type and what parameters they accept.
|
||||
|
||||
**Parameters:**
|
||||
- `domain` *(optional)* — Filter by domain, e.g., `light`, `climate`, `switch`
|
||||
|
||||
**Example:**
|
||||
```
|
||||
What services are available for climate devices?
|
||||
```
|
||||
|
||||
### `ha_call_service`
|
||||
|
||||
Call a Home Assistant service to control a device.
|
||||
|
||||
**Parameters:**
|
||||
- `domain` *(required)* — Service domain: `light`, `switch`, `climate`, `cover`, `media_player`, `fan`, `scene`, `script`
|
||||
- `service` *(required)* — Service name: `turn_on`, `turn_off`, `toggle`, `set_temperature`, `set_hvac_mode`, `open_cover`, `close_cover`, `set_volume_level`
|
||||
- `entity_id` *(optional)* — Target entity, e.g., `light.living_room`
|
||||
- `data` *(optional)* — Additional parameters as a JSON object
|
||||
|
||||
**Examples:**
|
||||
|
||||
```
|
||||
Turn on the living room lights
|
||||
→ ha_call_service(domain="light", service="turn_on", entity_id="light.living_room")
|
||||
```
|
||||
|
||||
```
|
||||
Set the thermostat to 22 degrees in heat mode
|
||||
→ ha_call_service(domain="climate", service="set_temperature",
|
||||
entity_id="climate.thermostat", data={"temperature": 22, "hvac_mode": "heat"})
|
||||
```
|
||||
|
||||
```
|
||||
Set living room lights to blue at 50% brightness
|
||||
→ ha_call_service(domain="light", service="turn_on",
|
||||
entity_id="light.living_room", data={"brightness": 128, "color_name": "blue"})
|
||||
```
|
||||
|
||||
## Gateway Platform: Real-Time Events
|
||||
|
||||
The Home Assistant gateway adapter connects via WebSocket and subscribes to `state_changed` events. When a device state changes and matches your filters, it's forwarded to the agent as a message.
|
||||
|
||||
### Event Filtering
|
||||
|
||||
:::warning Required Configuration
|
||||
By default, **no events are forwarded**. You must configure at least one of `watch_domains`, `watch_entities`, or `watch_all` to receive events. Without filters, a warning is logged at startup and all state changes are silently dropped.
|
||||
:::
|
||||
|
||||
Configure which events the agent sees in `~/.hermes/config.yaml` under the Home Assistant platform's `extra` section:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
homeassistant:
|
||||
enabled: true
|
||||
extra:
|
||||
watch_domains:
|
||||
- climate
|
||||
- binary_sensor
|
||||
- alarm_control_panel
|
||||
- light
|
||||
watch_entities:
|
||||
- sensor.front_door_battery
|
||||
ignore_entities:
|
||||
- sensor.uptime
|
||||
- sensor.cpu_usage
|
||||
- sensor.memory_usage
|
||||
cooldown_seconds: 30
|
||||
```
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `watch_domains` | *(none)* | Only watch these entity domains (e.g., `climate`, `light`, `binary_sensor`) |
|
||||
| `watch_entities` | *(none)* | Only watch these specific entity IDs |
|
||||
| `watch_all` | `false` | Set to `true` to receive **all** state changes (not recommended for most setups) |
|
||||
| `ignore_entities` | *(none)* | Always ignore these entities (applied before domain/entity filters) |
|
||||
| `cooldown_seconds` | `30` | Minimum seconds between events for the same entity |
|
||||
|
||||
:::tip
|
||||
Start with a focused set of domains — `climate`, `binary_sensor`, and `alarm_control_panel` cover the most useful automations. Add more as needed. Use `ignore_entities` to suppress noisy sensors like CPU temperature or uptime counters.
|
||||
:::
|
||||
|
||||
### Event Formatting
|
||||
|
||||
State changes are formatted as human-readable messages based on domain:
|
||||
|
||||
| Domain | Format |
|
||||
|--------|--------|
|
||||
| `climate` | "HVAC mode changed from 'off' to 'heat' (current: 21, target: 23)" |
|
||||
| `sensor` | "changed from 21°C to 22°C" |
|
||||
| `binary_sensor` | "triggered" / "cleared" |
|
||||
| `light`, `switch`, `fan` | "turned on" / "turned off" |
|
||||
| `alarm_control_panel` | "alarm state changed from 'armed_away' to 'triggered'" |
|
||||
| *(other)* | "changed from 'old' to 'new'" |
|
||||
|
||||
### Agent Responses
|
||||
|
||||
Outbound messages from the agent are delivered as **Home Assistant persistent notifications** (via `persistent_notification.create`). These appear in the HA notification panel with the title "Hermes Agent".
|
||||
|
||||
### Connection Management
|
||||
|
||||
- **WebSocket** with 30-second heartbeat for real-time events
|
||||
- **Automatic reconnection** with backoff: 5s → 10s → 30s → 60s
|
||||
- **REST API** for outbound notifications (separate session to avoid WebSocket conflicts)
|
||||
- **Authorization** — HA events are always authorized (no user allowlist needed, since the `HASS_TOKEN` authenticates the connection)
|
||||
|
||||
## Security
|
||||
|
||||
The Home Assistant tools enforce security restrictions:
|
||||
|
||||
:::warning Blocked Domains
|
||||
The following service domains are **blocked** to prevent arbitrary code execution on the HA host:
|
||||
|
||||
- `shell_command` — arbitrary shell commands
|
||||
- `command_line` — sensors/switches that execute commands
|
||||
- `python_script` — scripted Python execution
|
||||
- `pyscript` — broader scripting integration
|
||||
- `hassio` — addon control, host shutdown/reboot
|
||||
- `rest_command` — HTTP requests from HA server (SSRF vector)
|
||||
|
||||
Attempting to call services in these domains returns an error.
|
||||
:::
|
||||
|
||||
Entity IDs are validated against the pattern `^[a-z_][a-z0-9_]*\.[a-z0-9_]+$` to prevent injection attacks.
|
||||
|
||||
## Example Automations
|
||||
|
||||
### Morning Routine
|
||||
|
||||
```
|
||||
User: Start my morning routine
|
||||
|
||||
Agent:
|
||||
1. ha_call_service(domain="light", service="turn_on",
|
||||
entity_id="light.bedroom", data={"brightness": 128})
|
||||
2. ha_call_service(domain="climate", service="set_temperature",
|
||||
entity_id="climate.thermostat", data={"temperature": 22})
|
||||
3. ha_call_service(domain="media_player", service="turn_on",
|
||||
entity_id="media_player.kitchen_speaker")
|
||||
```
|
||||
|
||||
### Security Check
|
||||
|
||||
```
|
||||
User: Is the house secure?
|
||||
|
||||
Agent:
|
||||
1. ha_list_entities(domain="binary_sensor")
|
||||
→ checks door/window sensors
|
||||
2. ha_get_state(entity_id="alarm_control_panel.home")
|
||||
→ checks alarm status
|
||||
3. ha_list_entities(domain="lock")
|
||||
→ checks lock states
|
||||
4. Reports: "All doors closed, alarm is armed_away, all locks engaged."
|
||||
```
|
||||
|
||||
### Reactive Automation (via Gateway Events)
|
||||
|
||||
When connected as a gateway platform, the agent can react to events:
|
||||
|
||||
```
|
||||
[Home Assistant] Front Door: triggered (was cleared)
|
||||
|
||||
Agent automatically:
|
||||
1. ha_get_state(entity_id="binary_sensor.front_door")
|
||||
2. ha_call_service(domain="light", service="turn_on",
|
||||
entity_id="light.hallway")
|
||||
3. Sends notification: "Front door opened. Hallway lights turned on."
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Environment variables not picked up.**
|
||||
The adapter reads credentials from `~/.hermes/.env` (auto-merged at startup) or
|
||||
from `config.yaml`. Double-check the file lives under the active Hermes profile
|
||||
home and that there's no stray quoting around the URL/token. Restart the gateway
|
||||
after editing — env changes are only applied on process start.
|
||||
|
||||
**`conversation entity not found` / agent never replies.**
|
||||
Home Assistant's conversation API requires a configured *Assist* conversation
|
||||
agent. In HA, open **Settings → Voice assistants → Add assistant** and note the
|
||||
resulting entity id (looks like `conversation.home_assistant` or
|
||||
`conversation.openai_<name>`). Set that entity id in the adapter's
|
||||
`conversation_entity` setting; the default may not exist on your instance.
|
||||
|
||||
**REST auth failing (`401 Unauthorized`).**
|
||||
The token must be a *Long-Lived Access Token* created from your HA user profile
|
||||
page (**Profile → Security → Long-lived access tokens**). Short-lived UI
|
||||
session tokens won't work. Also verify the base URL includes the scheme and
|
||||
port (e.g. `http://homeassistant.local:8123`) and is reachable from the host
|
||||
running Hermes — `curl -H "Authorization: Bearer <token>" <url>/api/` should
|
||||
return `{"message": "API running."}`.
|
||||
@@ -0,0 +1,619 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: "Messaging Gateway"
|
||||
description: "Chat with Hermes from Telegram, Discord, Slack, WhatsApp, Signal, SMS, Email, Home Assistant, Mattermost, Matrix, DingTalk, Yuanbao, Microsoft Teams, LINE, Webhooks, or any OpenAI-compatible frontend via the API server — architecture and setup overview"
|
||||
---
|
||||
|
||||
# Messaging Gateway
|
||||
|
||||
Chat with Hermes from Telegram, Discord, Slack, WhatsApp, Signal, SMS, Email, Home Assistant, Mattermost, Matrix, DingTalk, Feishu/Lark, WeCom, Weixin, BlueBubbles (iMessage), QQ, Yuanbao, Microsoft Teams, LINE, ntfy, or your browser. The gateway is a single background process that connects to all your configured platforms, handles sessions, runs cron jobs, and delivers voice messages.
|
||||
|
||||
For the full voice feature set — including CLI microphone mode, spoken replies in messaging, and Discord voice-channel conversations — see [Voice Mode](/user-guide/features/voice-mode) and [Use Voice Mode with Hermes](/guides/use-voice-mode-with-hermes).
|
||||
|
||||
:::tip
|
||||
Bots need both a model provider and tool providers (TTS, web). A [Nous Portal](/integrations/nous-portal) subscription bundles all of them.
|
||||
:::
|
||||
|
||||
## Platform Comparison
|
||||
|
||||
| Platform | Voice | Images | Files | Threads | Reactions | Typing | Streaming |
|
||||
|----------|:-----:|:------:|:-----:|:-------:|:---------:|:------:|:---------:|
|
||||
| Telegram | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ |
|
||||
| Discord | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Slack | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Google Chat | — | ✅ | ✅ | ✅ | — | ✅ | — |
|
||||
| WhatsApp | — | ✅ | ✅ | — | — | ✅ | ✅ |
|
||||
| Signal | — | ✅ | ✅ | — | — | ✅ | ✅ |
|
||||
| SMS | — | — | — | — | — | — | — |
|
||||
| Email | — | ✅ | ✅ | ✅ | — | — | — |
|
||||
| Home Assistant | — | — | — | — | — | — | — |
|
||||
| Mattermost | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ |
|
||||
| Matrix | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| DingTalk | — | ✅ | ✅ | — | ✅ | — | ✅ |
|
||||
| Feishu/Lark | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| WeCom | ✅ | ✅ | ✅ | — | — | — | — |
|
||||
| WeCom Callback | — | — | — | — | — | — | — |
|
||||
| Weixin | ✅ | ✅ | ✅ | — | — | ✅ | ✅ |
|
||||
| BlueBubbles | — | ✅ | ✅ | — | ✅ | ✅ | — |
|
||||
| QQ | ✅ | ✅ | ✅ | — | — | ✅ | — |
|
||||
| Yuanbao | ✅ | ✅ | ✅ | — | — | ✅ | ✅ |
|
||||
| Microsoft Teams | — | ✅ | — | ✅ | — | ✅ | — |
|
||||
| LINE | — | ✅ | ✅ | — | — | ✅ | — |
|
||||
| ntfy | — | — | — | — | — | — | — |
|
||||
|
||||
**Voice** = TTS audio replies and/or voice message transcription. **Images** = send/receive images. **Files** = send/receive file attachments. **Threads** = threaded conversations. **Reactions** = emoji reactions on messages. **Typing** = typing indicator while processing. **Streaming** = progressive message updates via editing.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Gateway["Hermes Gateway"]
|
||||
subgraph Adapters["Platform adapters"]
|
||||
tg[Telegram]
|
||||
dc[Discord]
|
||||
wa[WhatsApp]
|
||||
sl[Slack]
|
||||
gc[Google Chat]
|
||||
sig[Signal]
|
||||
sms[SMS]
|
||||
em[Email]
|
||||
ha[Home Assistant]
|
||||
mm[Mattermost]
|
||||
mx[Matrix]
|
||||
dt[DingTalk]
|
||||
fs[Feishu/Lark]
|
||||
wc[WeCom]
|
||||
wcb[WeCom Callback]
|
||||
wx[Weixin]
|
||||
bb[BlueBubbles]
|
||||
qq[QQ]
|
||||
yb[Yuanbao]
|
||||
ms[Microsoft Teams]
|
||||
api["API Server<br/>(OpenAI-compatible)"]
|
||||
wh[Webhooks]
|
||||
end
|
||||
|
||||
store["Session store<br/>per chat"]
|
||||
agent["AIAgent<br/>run_agent.py"]
|
||||
cron["Cron scheduler<br/>ticks every 60s"]
|
||||
end
|
||||
|
||||
tg --> store
|
||||
dc --> store
|
||||
wa --> store
|
||||
sl --> store
|
||||
gc --> store
|
||||
sig --> store
|
||||
sms --> store
|
||||
em --> store
|
||||
ha --> store
|
||||
mm --> store
|
||||
mx --> store
|
||||
dt --> store
|
||||
fs --> store
|
||||
wc --> store
|
||||
wcb --> store
|
||||
wx --> store
|
||||
bb --> store
|
||||
qq --> store
|
||||
yb --> store
|
||||
ms --> store
|
||||
api --> store
|
||||
wh --> store
|
||||
store --> agent
|
||||
cron --> store
|
||||
```
|
||||
|
||||
Each platform adapter receives messages, routes them through a per-chat session store, and dispatches them to the AIAgent for processing. The gateway also runs the cron scheduler, ticking every 60 seconds to execute any due jobs.
|
||||
|
||||
## Intentional Silence Tokens
|
||||
|
||||
For group chats, hooks, and automation flows, Hermes supports explicit silence tokens. If the agent's final response is exactly one supported token, the gateway suppresses outbound delivery and sends nothing to the chat.
|
||||
|
||||
Supported tokens:
|
||||
|
||||
- `[SILENT]`
|
||||
- `SILENT`
|
||||
- `NO_REPLY`
|
||||
- `NO REPLY`
|
||||
|
||||
Whitespace and case are normalized, but the whole final response must be the token. A sentence like "Use `[SILENT]` when nothing changed" is delivered normally.
|
||||
|
||||
Silence is a delivery decision only. Hermes keeps the assistant silence turn in the session transcript, so the conversation still alternates normally:
|
||||
|
||||
```text
|
||||
user: side-channel chatter
|
||||
assistant: [SILENT] # stored, not delivered
|
||||
user: next message
|
||||
```
|
||||
|
||||
Failed turns still surface as errors; Hermes does not hide failures just because the text resembles a silence token.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
The easiest way to configure messaging platforms is the interactive wizard:
|
||||
|
||||
```bash
|
||||
hermes gateway setup # Interactive setup for all messaging platforms
|
||||
```
|
||||
|
||||
This walks you through configuring each platform with arrow-key selection, shows which platforms are already configured, and offers to start/restart the gateway when done.
|
||||
|
||||
## Gateway Commands
|
||||
|
||||
```bash
|
||||
hermes gateway # Run in foreground
|
||||
hermes gateway setup # Configure messaging platforms interactively
|
||||
hermes gateway install # Install as a user service (Linux) / launchd service (macOS)
|
||||
sudo hermes gateway install --system # Linux only: install a boot-time system service
|
||||
hermes gateway start # Start the default service
|
||||
hermes gateway stop # Stop the default service
|
||||
hermes gateway status # Check default service status
|
||||
hermes gateway status --system # Linux only: inspect the system service explicitly
|
||||
```
|
||||
|
||||
## Chat Commands (Inside Messaging)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/new` or `/reset` | Start a fresh conversation |
|
||||
| `/model [provider:model]` | Show or change the model (supports `provider:model` syntax) |
|
||||
| `/personality [name]` | Set a personality |
|
||||
| `/retry` | Retry the last message |
|
||||
| `/undo` | Remove the last exchange |
|
||||
| `/status` | Show session info |
|
||||
| `/whoami` | Show your slash command access on this scope (admin / user / unrestricted) |
|
||||
| `/stop` | Stop the running agent |
|
||||
| `/approve` | Approve a pending dangerous command |
|
||||
| `/deny` | Reject a pending dangerous command |
|
||||
| `/sethome` | Set this chat as the home channel |
|
||||
| `/compress` | Manually compress conversation context |
|
||||
| `/title [name]` | Set or show the session title |
|
||||
| `/resume [name]` | Resume a previously named session |
|
||||
| `/usage` | Show token usage for this session |
|
||||
| `/insights [days]` | Show usage insights and analytics |
|
||||
| `/reasoning [level\|show\|hide]` | Change reasoning effort or toggle reasoning display |
|
||||
| `/voice [on\|off\|tts\|join\|leave\|status]` | Control messaging voice replies and Discord voice-channel behavior |
|
||||
| `/rollback [number]` | List or restore filesystem checkpoints |
|
||||
| `/background <prompt>` | Run a prompt in a separate background session |
|
||||
| `/reload-mcp` | Reload MCP servers from config |
|
||||
| `/update` | Update Hermes Agent to the latest version |
|
||||
| `/help` | Show available commands |
|
||||
| `/<skill-name>` | Invoke any installed skill |
|
||||
|
||||
## Session Management
|
||||
|
||||
### Session Persistence
|
||||
|
||||
Sessions persist across messages until they reset. The agent remembers your conversation context.
|
||||
|
||||
### Reset Policies
|
||||
|
||||
Sessions reset based on configurable policies:
|
||||
|
||||
| Policy | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| Daily | 4:00 AM | Reset at a specific hour each day |
|
||||
| Idle | 1440 min | Reset after N minutes of inactivity |
|
||||
| Both | (combined) | Whichever triggers first |
|
||||
|
||||
Configure per-platform overrides in `~/.hermes/gateway.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"reset_by_platform": {
|
||||
"telegram": { "mode": "idle", "idle_minutes": 240 },
|
||||
"discord": { "mode": "idle", "idle_minutes": 60 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
**By default, the gateway denies all users who are not in an allowlist or paired via DM.** This is the safe default for a bot with terminal access.
|
||||
|
||||
```bash
|
||||
# Restrict to specific users (recommended):
|
||||
TELEGRAM_ALLOWED_USERS=123456789,987654321
|
||||
DISCORD_ALLOWED_USERS=123456789012345678
|
||||
SIGNAL_ALLOWED_USERS=+155****4567,+155****6543
|
||||
SMS_ALLOWED_USERS=+155****4567,+155****6543
|
||||
EMAIL_ALLOWED_USERS=trusted@example.com,colleague@work.com
|
||||
MATTERMOST_ALLOWED_USERS=3uo8dkh1p7g1mfk49ear5fzs5c
|
||||
MATRIX_ALLOWED_USERS=@alice:matrix.org
|
||||
DINGTALK_ALLOWED_USERS=user-id-1
|
||||
FEISHU_ALLOWED_USERS=ou_xxxxxxxx,ou_yyyyyyyy
|
||||
WECOM_ALLOWED_USERS=user-id-1,user-id-2
|
||||
WECOM_CALLBACK_ALLOWED_USERS=user-id-1,user-id-2
|
||||
TEAMS_ALLOWED_USERS=aad-object-id-1,aad-object-id-2
|
||||
|
||||
# Or allow
|
||||
GATEWAY_ALLOWED_USERS=123456789,987654321
|
||||
|
||||
# Or explicitly allow all users (NOT recommended for bots with terminal access):
|
||||
GATEWAY_ALLOW_ALL_USERS=true
|
||||
```
|
||||
|
||||
### DM Pairing (Alternative to Allowlists)
|
||||
|
||||
Instead of manually configuring user IDs, unknown users receive a one-time pairing code when they DM the bot:
|
||||
|
||||
```bash
|
||||
# The user sees: "Pairing code: XKGH5N7P"
|
||||
# You approve them with:
|
||||
hermes pairing approve telegram XKGH5N7P
|
||||
|
||||
# Other pairing commands:
|
||||
hermes pairing list # View pending + approved users
|
||||
hermes pairing revoke telegram 123456789 # Remove access
|
||||
```
|
||||
|
||||
Pairing codes expire after 1 hour, are rate-limited, and use cryptographic randomness.
|
||||
|
||||
### Admins vs Regular Users
|
||||
|
||||
Allowlists answer "can this person reach the bot at all?" The **admin / user split** answers "now that they're in, what are they allowed to do?"
|
||||
|
||||
Every allowed user falls into one of two tiers per scope (DM vs group/channel):
|
||||
|
||||
- **Admin** — full access. Can run every registered slash command (built-in + plugin) and use every gated capability.
|
||||
- **Regular user** — restricted access. Can chat with the agent normally, but can only run the slash commands you explicitly enable. The always-allowed floor is `/help` and `/whoami`.
|
||||
|
||||
The tiers are configured per platform and per scope. DM admin status does not imply group/channel admin status — each scope has its own admin list.
|
||||
|
||||
**What the tiers gate today:** slash commands. The split runs through the live command registry, so it covers built-ins and plugin-registered commands without per-feature wiring. Plain chat is not affected — non-admins can still talk to the agent.
|
||||
|
||||
**What may be gated in the future:** more capability surfaces (tool access, model switching, expensive operations) will hang off the same admin / user distinction as we add them. Configuring the split now means those future restrictions land cleanly without you having to re-model who's an admin.
|
||||
|
||||
#### Configuration
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
discord:
|
||||
extra:
|
||||
allow_from: ["111", "222", "333"]
|
||||
allow_admin_from: ["111"] # admins → all slash commands
|
||||
user_allowed_commands: [status, model] # what non-admins may run
|
||||
# Optional: separate group/channel scope
|
||||
group_allow_admin_from: ["111"]
|
||||
group_user_allowed_commands: [status]
|
||||
```
|
||||
|
||||
**Backward compat:** if `allow_admin_from` is not set for a scope, the tier split is disabled for that scope and every allowed user has full access. Existing installs keep working with no changes — opt in when you want the distinction.
|
||||
|
||||
#### Inspecting your access
|
||||
|
||||
Use `/whoami` from any platform to see the active scope, your tier (admin / user / unrestricted), and which slash commands you can run. See the [Telegram](/user-guide/messaging/telegram#slash-command-access-control) and [Discord](/user-guide/messaging/discord#slash-command-access-control) pages for platform-specific examples.
|
||||
|
||||
## Interrupting the Agent
|
||||
|
||||
Send any message while the agent is working to interrupt it. Key behaviors:
|
||||
|
||||
- **In-progress terminal commands are killed immediately** (SIGTERM, then SIGKILL after 1s)
|
||||
- **Tool calls are cancelled** — only the currently-executing one runs, the rest are skipped
|
||||
- **Multiple messages are combined** — messages sent during interruption are joined into one prompt
|
||||
- **`/stop` command** — interrupts without queuing a follow-up message
|
||||
|
||||
### Queue vs interrupt vs steer (busy-input mode)
|
||||
|
||||
By default, messaging a busy agent interrupts it. Two other modes are available:
|
||||
|
||||
- `queue` — follow-up messages wait and run as the next turn after the current task finishes.
|
||||
- `steer` — follow-up messages are injected into the current run via `/steer`, arriving at the agent after the next tool call. No interrupt, no new turn. Falls back to `queue` behavior if the agent hasn't started yet.
|
||||
|
||||
```yaml
|
||||
display:
|
||||
busy_input_mode: steer # or queue, or interrupt (default)
|
||||
busy_ack_enabled: true # set to false to suppress the ⚡/⏳/⏩ chat reply entirely
|
||||
```
|
||||
|
||||
The first time you message a busy agent on any platform, Hermes appends a one-line reminder to the busy-ack explaining the knob (`"💡 First-time tip — …"`). The reminder fires once per install — a flag under `onboarding.seen.busy_input_prompt` latches it. Delete that key to see the tip again.
|
||||
|
||||
If you find the busy-ack noisy — especially with voice input or rapid-fire messages — set `display.busy_ack_enabled: false`. Your input is still queued/steered/interrupts as normal, only the chat reply is silenced.
|
||||
|
||||
## Tool Progress Notifications
|
||||
|
||||
Control how much tool activity is displayed in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
tool_progress: all # off | new | all | verbose
|
||||
tool_progress_command: false # set to true to enable /verbose in messaging
|
||||
```
|
||||
|
||||
When enabled, the bot sends status messages as it works:
|
||||
|
||||
```text
|
||||
💻 `ls -la`...
|
||||
🔍 web_search...
|
||||
📄 web_extract...
|
||||
🐍 execute_code...
|
||||
```
|
||||
|
||||
## Background Sessions
|
||||
|
||||
Run a prompt in a separate background session so the agent works on it independently while your main chat stays responsive:
|
||||
|
||||
```
|
||||
/background Check all servers in the cluster and report any that are down
|
||||
```
|
||||
|
||||
Hermes confirms immediately:
|
||||
|
||||
```
|
||||
🔄 Background task started: "Check all servers in the cluster..."
|
||||
Task ID: bg_143022_a1b2c3
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
Each `/background` prompt spawns a **separate agent instance** that runs asynchronously:
|
||||
|
||||
- **Isolated session** — the background agent has its own session with its own conversation history. It has no knowledge of your current chat context and receives only the prompt you provide.
|
||||
- **Same configuration** — inherits your model, provider, toolsets, reasoning settings, and provider routing from the current gateway setup.
|
||||
- **Non-blocking** — your main chat stays fully interactive. Send messages, run other commands, or start more background tasks while it works.
|
||||
- **Result delivery** — when the task finishes, the result is sent back to the **same chat or channel** where you issued the command, prefixed with "✅ Background task complete". If it fails, you'll see "❌ Background task failed" with the error.
|
||||
|
||||
### Background Process Notifications
|
||||
|
||||
When the agent running a background session uses `terminal(background=true)` to start long-running processes (servers, builds, etc.), the gateway can push status updates to your chat. Control this with `display.background_process_notifications` in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
background_process_notifications: all # all | result | error | off
|
||||
```
|
||||
|
||||
| Mode | What you receive |
|
||||
|------|-----------------|
|
||||
| `all` | Running-output updates **and** the final completion message (default) |
|
||||
| `result` | Only the final completion message (regardless of exit code) |
|
||||
| `error` | Only the final message when the exit code is non-zero |
|
||||
| `off` | No process watcher messages at all |
|
||||
|
||||
You can also set this via environment variable:
|
||||
|
||||
```bash
|
||||
HERMES_BACKGROUND_NOTIFICATIONS=result
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
- **Server monitoring** — "/background Check the health of all services and alert me if anything is down"
|
||||
- **Long builds** — "/background Build and deploy the staging environment" while you continue chatting
|
||||
- **Research tasks** — "/background Research competitor pricing and summarize in a table"
|
||||
- **File operations** — "/background Organize the photos in ~/Downloads by date into folders"
|
||||
|
||||
:::tip
|
||||
Background tasks on messaging platforms are fire-and-forget — you don't need to wait or check on them. Results arrive in the same chat automatically when the task finishes.
|
||||
:::
|
||||
|
||||
## Service Management
|
||||
|
||||
### Linux (systemd)
|
||||
|
||||
```bash
|
||||
hermes gateway install # Install as user service
|
||||
hermes gateway start # Start the service
|
||||
hermes gateway stop # Stop the service
|
||||
hermes gateway status # Check status
|
||||
journalctl --user -u hermes-gateway -f # View logs
|
||||
|
||||
# Enable lingering (keeps running after logout)
|
||||
sudo loginctl enable-linger $USER
|
||||
|
||||
# Or install a boot-time system service that still runs as your user
|
||||
sudo hermes gateway install --system
|
||||
sudo hermes gateway start --system
|
||||
sudo hermes gateway status --system
|
||||
journalctl -u hermes-gateway -f
|
||||
```
|
||||
|
||||
Use the user service on laptops and dev boxes. Use the system service on VPS or headless hosts that should come back at boot without relying on systemd linger.
|
||||
|
||||
:::tip Headless VMs: user service + linger avoids root prompts
|
||||
A system service needs root for every restart — including the automatic gateway restart at the end of `hermes update`. When `hermes update` runs as a non-root user, it tries passwordless `sudo systemctl`; if that's unavailable, it skips the restart and prints the manual `sudo systemctl restart hermes-gateway` command (it never blocks on an interactive password prompt).
|
||||
|
||||
For a headless VM you never log into, a **user** service with lingering enabled gives you the same start-at-boot behavior with zero root involvement:
|
||||
|
||||
```bash
|
||||
hermes gateway install # user service
|
||||
sudo loginctl enable-linger $USER # one-time: start at boot, survive logout
|
||||
```
|
||||
|
||||
After that, `hermes update` can restart the gateway without any privileges. If you prefer to keep the system service, either run updates with `sudo hermes update`, or grant the service account passwordless sudo for systemctl, e.g. in `sudo visudo -f /etc/sudoers.d/hermes-gateway`:
|
||||
|
||||
```
|
||||
hermes ALL=(root) NOPASSWD: /usr/bin/systemctl --no-ask-password reset-failed hermes-gateway*, /usr/bin/systemctl --no-ask-password start hermes-gateway*, /usr/bin/systemctl --no-ask-password restart hermes-gateway*
|
||||
```
|
||||
:::
|
||||
|
||||
Avoid keeping both the user and system gateway units installed at once unless you really mean to. Hermes will warn if it detects both because start/stop/status behavior gets ambiguous.
|
||||
|
||||
:::info Multiple installations
|
||||
If you run multiple Hermes installations on the same machine (with different `HERMES_HOME` directories), each gets its own systemd service name. The default `~/.hermes` uses `hermes-gateway`; other installations use `hermes-gateway-<hash>`. The `hermes gateway` commands automatically target the correct service for your current `HERMES_HOME`.
|
||||
:::
|
||||
|
||||
### macOS (launchd)
|
||||
|
||||
```bash
|
||||
hermes gateway install # Install as launchd agent
|
||||
hermes gateway start # Start the service
|
||||
hermes gateway stop # Stop the service
|
||||
hermes gateway status # Check status
|
||||
tail -f ~/.hermes/logs/gateway.log # View logs
|
||||
```
|
||||
|
||||
The generated plist lives at `~/Library/LaunchAgents/ai.hermes.gateway.plist`. It includes three environment variables:
|
||||
|
||||
- **PATH** — your full shell PATH at install time, with the venv `bin/` and `node_modules/.bin` prepended. This ensures user-installed tools (Node.js, ffmpeg, etc.) are available to gateway subprocesses like the WhatsApp bridge.
|
||||
- **VIRTUAL_ENV** — points to the Python virtualenv so tools can resolve packages correctly.
|
||||
- **HERMES_HOME** — scopes the gateway to your Hermes installation.
|
||||
|
||||
:::tip PATH changes after install
|
||||
launchd plists are static — if you install new tools (e.g. a new Node.js version via nvm, or ffmpeg via Homebrew) after setting up the gateway, run `hermes gateway install` again to capture the updated PATH. The gateway will detect the stale plist and reload automatically.
|
||||
:::
|
||||
|
||||
:::info Multiple installations
|
||||
Like the Linux systemd service, each `HERMES_HOME` directory gets its own launchd label. The default `~/.hermes` uses `ai.hermes.gateway`; other installations use `ai.hermes.gateway-<suffix>`.
|
||||
:::
|
||||
|
||||
## Platform-Specific Toolsets
|
||||
|
||||
Each platform has its own toolset:
|
||||
|
||||
| Platform | Toolset | Capabilities |
|
||||
|----------|---------|--------------|
|
||||
| CLI | `hermes-cli` | Full access |
|
||||
| Telegram | `hermes-telegram` | Full tools including terminal |
|
||||
| Discord | `hermes-discord` | Full tools including terminal |
|
||||
| WhatsApp | `hermes-whatsapp` | Full tools including terminal |
|
||||
| WhatsApp Cloud API | `hermes-whatsapp` | Full tools including terminal (shares toolset with the Baileys bridge) |
|
||||
| Slack | `hermes-slack` | Full tools including terminal |
|
||||
| Google Chat | `hermes-google_chat` | Full tools including terminal |
|
||||
| Signal | `hermes-signal` | Full tools including terminal |
|
||||
| SMS | `hermes-sms` | Full tools including terminal |
|
||||
| Email | `hermes-email` | Full tools including terminal |
|
||||
| Home Assistant | `hermes-homeassistant` | Full tools + HA device control (ha_list_entities, ha_get_state, ha_call_service, ha_list_services) |
|
||||
| Mattermost | `hermes-mattermost` | Full tools including terminal |
|
||||
| Matrix | `hermes-matrix` | Full tools including terminal |
|
||||
| DingTalk | `hermes-dingtalk` | Full tools including terminal |
|
||||
| Feishu/Lark | `hermes-feishu` | Full tools including terminal |
|
||||
| WeCom | `hermes-wecom` | Full tools including terminal |
|
||||
| WeCom Callback | `hermes-wecom-callback` | Full tools including terminal |
|
||||
| Weixin | `hermes-weixin` | Full tools including terminal |
|
||||
| BlueBubbles | `hermes-bluebubbles` | Full tools including terminal |
|
||||
| QQBot | `hermes-qqbot` | Full tools including terminal |
|
||||
| Yuanbao | `hermes-yuanbao` | Full tools including terminal |
|
||||
| Microsoft Teams | `hermes-teams` | Full tools including terminal |
|
||||
| API Server | `hermes-api-server` | Full tools (drops `clarify`, `send_message`, `text_to_speech` — programmatic access doesn't have an interactive user) |
|
||||
| Webhooks | `hermes-webhook` | Full tools including terminal |
|
||||
|
||||
## Operating a multi-platform gateway
|
||||
|
||||
A gateway typically runs several adapters at once (Telegram + Discord + Slack, etc.). The sections below cover day-2 operations that span all platforms.
|
||||
|
||||
### `/platform` command
|
||||
|
||||
Once the gateway is running, use the `/platform` slash command from any connected CLI session or chat to inspect and steer individual adapters without restarting the whole gateway:
|
||||
|
||||
```
|
||||
/platform list # show all adapters and their state
|
||||
/platform pause <name> # stop dispatching new messages to one adapter
|
||||
/platform resume <name> # re-enable a paused adapter
|
||||
```
|
||||
|
||||
`/platform list` shows whether each adapter is `running`, `paused` (manually), or `paused-by-breaker` (see below). Pausing keeps the adapter loaded and its background loops alive — incoming messages are dropped on the floor, but the connection itself stays open so resume is instant.
|
||||
|
||||
See also the broader status summary command [`/platforms`](../../reference/slash-commands.md#info).
|
||||
|
||||
### Automatic circuit breaker
|
||||
|
||||
Each adapter is wrapped in a circuit breaker. Repeated retryable failures (network blips, rate-limit replies, 5xx upstream responses, websocket disconnects) cause the breaker to trip — the adapter is auto-paused, an operator notification is sent to the home channel of another live platform when one is configured, and a structured log line is emitted.
|
||||
|
||||
The breaker does **not** auto-resume — it stays open until you run `/platform resume <name>` manually. This is intentional: if a platform is in a sustained outage, you don't want the gateway thrashing reconnects.
|
||||
|
||||
### Where to look when a platform is paused
|
||||
|
||||
When an adapter is paused, check:
|
||||
|
||||
1. **Gateway log** (`~/.hermes/logs/gateway.log` or the systemd / launchd unit log). Search for the platform name and `circuit breaker`, `paused`, or `disabled`. The trip event includes the failure count and the last error.
|
||||
2. **`/platform list`** output — shows the current state and last reason.
|
||||
3. **The provider's status page** (Telegram bot API status, Discord status, etc.). The breaker tripped because the platform was unhealthy; don't try to resume until it's back.
|
||||
|
||||
Once upstream is healthy, `/platform resume <name>` clears the breaker and re-arms the adapter.
|
||||
|
||||
### Restart notifications
|
||||
|
||||
When the gateway restarts (or is shut down with in-flight sessions), it can send a one-shot "the agent is back" / "the agent was interrupted" message to each platform's home channel. This is controlled per-platform by the `gateway_restart_notification` flag in `gateway-config.yaml`, which defaults to `true`:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
telegram:
|
||||
home_chat_id: "123456789"
|
||||
gateway_restart_notification: false # opt out for this platform
|
||||
discord:
|
||||
home_chat_id: "987654321"
|
||||
# gateway_restart_notification omitted → defaults to true
|
||||
```
|
||||
|
||||
Disable it on noisy or low-priority platforms while leaving it on for your primary chat. The notification is sent once per restart, regardless of how many sessions were in flight.
|
||||
|
||||
### Session resume across gateway restarts
|
||||
|
||||
When the gateway shuts down with an in-flight tool call or generation, the affected sessions are flagged as `restart_interrupted`. On the next startup, the gateway schedules an auto-resume for each one — the user gets a short heads-up in the chat ("Send any message after restart and I'll try to resume where you left off.") and the session picks up from the last committed turn when they reply.
|
||||
|
||||
This behaviour is on by default and is logged at gateway start:
|
||||
|
||||
```
|
||||
Scheduled auto-resume for N restart-interrupted session(s)
|
||||
```
|
||||
|
||||
No configuration is required. If you don't want the heads-up, set `gateway_restart_notification: false` on the platform.
|
||||
|
||||
### Mobile-friendly progress defaults
|
||||
|
||||
Telegram is usually a mobile inbox, so the defaults are tuned for that surface:
|
||||
|
||||
- **`tool_progress`** defaults to **`off`** — no per-tool breadcrumb stream filling up the chat.
|
||||
- **`busy_ack_detail`** defaults to **`off`** — busy-state acknowledgments and long-running heartbeats stay terse (no `iteration 21/60` debug detail).
|
||||
- **`interim_assistant_messages`** stays **on** — real mid-turn assistant commentary (the model literally telling you what it's about to do) is signal, not noise.
|
||||
- **`long_running_notifications`** stays **on** — a single edit-in-place "⏳ Working — N min" bubble updates every few minutes so you have a heartbeat instead of staring at `typing…` for half an hour.
|
||||
|
||||
Opt out of either of the kept-on defaults or opt back into verbose progress per platform:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
platforms:
|
||||
telegram:
|
||||
# Re-enable the tool-progress stream
|
||||
tool_progress: new
|
||||
# Show "iteration N/M, running: tool" in heartbeats and busy acks
|
||||
busy_ack_detail: true
|
||||
# Or quiet them entirely
|
||||
interim_assistant_messages: false
|
||||
long_running_notifications: false
|
||||
```
|
||||
|
||||
### Progress bubble cleanup (opt-in)
|
||||
|
||||
Tool-progress messages, the "still working…" heartbeat, and status-callback bubbles can also be auto-deleted after the final response lands. Enable per-platform via `display.platforms.<platform>.cleanup_progress`:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
platforms:
|
||||
telegram:
|
||||
cleanup_progress: true
|
||||
discord:
|
||||
cleanup_progress: true
|
||||
```
|
||||
|
||||
Defaults to `false`. Only platforms whose adapter implements `delete_message` honor the setting (currently Telegram and Discord). Failed runs **skip** cleanup so the bubbles remain as breadcrumbs.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Telegram Setup](telegram.md)
|
||||
- [Discord Setup](discord.md)
|
||||
- [Slack Setup](slack.md)
|
||||
- [Google Chat Setup](google_chat.md)
|
||||
- [WhatsApp Setup](whatsapp.md)
|
||||
- [WhatsApp Business Cloud API Setup](whatsapp-cloud.md)
|
||||
- [Signal Setup](signal.md)
|
||||
- [SMS Setup (Twilio)](sms.md)
|
||||
- [Email Setup](email.md)
|
||||
- [Home Assistant Integration](homeassistant.md)
|
||||
- [Mattermost Setup](mattermost.md)
|
||||
- [Matrix Setup](matrix.md)
|
||||
- [DingTalk Setup](dingtalk.md)
|
||||
- [Feishu/Lark Setup](feishu.md)
|
||||
- [WeCom Setup](wecom.md)
|
||||
- [WeCom Callback Setup](wecom-callback.md)
|
||||
- [Weixin Setup (WeChat)](weixin.md)
|
||||
- [BlueBubbles Setup (iMessage)](bluebubbles.md)
|
||||
- [QQBot Setup](qqbot.md)
|
||||
- [Yuanbao Setup](yuanbao.md)
|
||||
- [Microsoft Teams Setup](teams.md)
|
||||
- [Teams Meetings Pipeline](teams-meetings.md)
|
||||
- [Open WebUI + API Server](open-webui.md)
|
||||
- [Webhooks](webhooks.md)
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
sidebar_position: 17
|
||||
title: "LINE"
|
||||
description: "Set up Hermes Agent as a LINE Messaging API bot"
|
||||
---
|
||||
|
||||
# LINE Setup
|
||||
|
||||
Run Hermes Agent as a [LINE](https://line.me/) bot via the official LINE Messaging API. The adapter lives as a bundled platform plugin under `plugins/platforms/line/` — no core edits, just enable it like any other platform.
|
||||
|
||||
LINE is the dominant messaging app in Japan, Taiwan, and Thailand. If your users live there, this is how they reach you.
|
||||
|
||||
> Run `hermes gateway setup` and pick **LINE** for a guided walk-through.
|
||||
|
||||
## How the bot responds
|
||||
|
||||
| Context | Behavior |
|
||||
|---------|----------|
|
||||
| **1:1 chat** (`U` IDs) | Responds to every message |
|
||||
| **Group chat** (`C` IDs) | Responds when the group is on the allowlist |
|
||||
| **Multi-user room** (`R` IDs) | Responds when the room is on the allowlist |
|
||||
|
||||
Inbound text, images, audio, video, files, stickers, and locations are all handled. Outbound text uses the **free reply token first** (single-use, ~60s window) and falls back to the metered Push API when the token has expired.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create a LINE Messaging API channel
|
||||
|
||||
1. Go to the [LINE Developers Console](https://developers.line.biz/console/).
|
||||
2. Create a Provider, then under it a **Messaging API** channel.
|
||||
3. From the channel's **Basic settings** tab, copy the **Channel secret**.
|
||||
4. From the **Messaging API** tab, scroll to **Channel access token (long-lived)** and click **Issue**. Copy the token.
|
||||
5. In the **Messaging API** tab, also disable **Auto-reply messages** and **Greeting messages** so they don't fight your bot's replies.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Expose the webhook port
|
||||
|
||||
LINE delivers webhooks over public HTTPS. The default port is `8646` — override with `LINE_PORT` if needed.
|
||||
|
||||
```bash
|
||||
# Cloudflare Tunnel (recommended for production — fixed hostname)
|
||||
cloudflared tunnel --url http://localhost:8646
|
||||
|
||||
# ngrok (good for dev)
|
||||
ngrok http 8646
|
||||
|
||||
# devtunnel
|
||||
devtunnel create hermes-line --allow-anonymous
|
||||
devtunnel port create hermes-line -p 8646 --protocol https
|
||||
devtunnel host hermes-line
|
||||
```
|
||||
|
||||
Copy the `https://...` URL — you'll set it as the webhook URL below. **Leave the tunnel running** while testing. For production, set up a fixed Cloudflare named tunnel so the webhook URL doesn't change on restart.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Configure Hermes
|
||||
|
||||
Add to `~/.hermes/.env`:
|
||||
|
||||
```env
|
||||
LINE_CHANNEL_ACCESS_TOKEN=YOUR_LONG_LIVED_TOKEN
|
||||
LINE_CHANNEL_SECRET=YOUR_CHANNEL_SECRET
|
||||
|
||||
# Allowlist — at least one of these (or LINE_ALLOW_ALL_USERS=true for dev)
|
||||
LINE_ALLOWED_USERS=U1234567890abcdef... # comma-separated U-prefixed IDs
|
||||
LINE_ALLOWED_GROUPS=C1234567890abcdef... # optional group IDs
|
||||
LINE_ALLOWED_ROOMS=R1234567890abcdef... # optional room IDs
|
||||
|
||||
# Required for image / audio / video sends — the public HTTPS base URL
|
||||
# the tunnel resolves to. Without it, send_image/voice/video will refuse.
|
||||
LINE_PUBLIC_URL=https://my-tunnel.example.com
|
||||
```
|
||||
|
||||
Then in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
line:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
That's enough — the bundled-plugin scan in `gateway/config.py` automatically picks up `plugins/platforms/line/`. No `Platform.LINE` enum edit, no `_create_adapter` registration.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Set the webhook URL
|
||||
|
||||
Back in the LINE console:
|
||||
|
||||
1. Open your channel → **Messaging API** tab.
|
||||
2. Under **Webhook settings** → **Webhook URL**, paste `https://<your-tunnel>/line/webhook` (note the `/line/webhook` path — the adapter listens there).
|
||||
3. Click **Verify**. LINE pings the URL; you should see a 200.
|
||||
4. Toggle **Use webhook** to **On**.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Run the gateway
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
The agent log shows:
|
||||
|
||||
```
|
||||
LINE: webhook listening on 0.0.0.0:8646/line/webhook (public: https://my-tunnel.example.com)
|
||||
```
|
||||
|
||||
Add the bot as a friend from the LINE app (scan the QR in the channel's **Messaging API** tab) and send it a message.
|
||||
|
||||
---
|
||||
|
||||
## Slow LLM responses
|
||||
|
||||
LINE's reply token is single-use and expires roughly 60 seconds after the inbound event. Slow LLMs can't reply in time, which would normally force a paid Push API call.
|
||||
|
||||
When the LLM is still running past `LINE_SLOW_RESPONSE_THRESHOLD` seconds (default `45`), the adapter consumes the original reply token to send a **Template Buttons** bubble:
|
||||
|
||||
> 🤔 Still thinking. Tap below to fetch the answer when it's ready.
|
||||
>
|
||||
> [ Get answer ]
|
||||
|
||||
The user taps **Get answer** when convenient — that postback delivers a *fresh* reply token, which the adapter uses to send the cached answer (still free).
|
||||
|
||||
State machine: `PENDING → READY → DELIVERED`, plus `ERROR` for cancelled runs (the orphan PENDING resolves to "Run was interrupted before completion." after `/stop` so the persistent button doesn't loop).
|
||||
|
||||
To disable the postback button and always Push-fallback instead:
|
||||
|
||||
```env
|
||||
LINE_SLOW_RESPONSE_THRESHOLD=0
|
||||
```
|
||||
|
||||
For the postback flow to fire reliably, suppress chatter that would consume the reply token before the threshold:
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/config.yaml
|
||||
display:
|
||||
interim_assistant_messages: false
|
||||
platforms:
|
||||
line:
|
||||
tool_progress: off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cron / notification delivery
|
||||
|
||||
```env
|
||||
LINE_HOME_CHANNEL=Uxxxxxxxxxxxxxxxxxxxx # default delivery target
|
||||
```
|
||||
|
||||
Cron jobs with `deliver: line` route to `LINE_HOME_CHANNEL`. The adapter ships a standalone Push-only sender so cron jobs work even when cron runs in a separate process from the gateway.
|
||||
|
||||
---
|
||||
|
||||
## Environment variable reference
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `LINE_CHANNEL_ACCESS_TOKEN` | yes | — | Long-lived channel access token |
|
||||
| `LINE_CHANNEL_SECRET` | yes | — | Channel secret (HMAC-SHA256 webhook verification) |
|
||||
| `LINE_HOST` | no | `0.0.0.0` | Webhook bind host |
|
||||
| `LINE_PORT` | no | `8646` | Webhook bind port |
|
||||
| `LINE_PUBLIC_URL` | for media | — | Public HTTPS base URL; required for image/voice/video sends |
|
||||
| `LINE_ALLOWED_USERS` | one of | — | Comma-separated user IDs (U-prefixed) |
|
||||
| `LINE_ALLOWED_GROUPS` | one of | — | Comma-separated group IDs (C-prefixed) |
|
||||
| `LINE_ALLOWED_ROOMS` | one of | — | Comma-separated room IDs (R-prefixed) |
|
||||
| `LINE_ALLOW_ALL_USERS` | dev only | `false` | Skip allowlist entirely |
|
||||
| `LINE_HOME_CHANNEL` | no | — | Default cron / notification delivery target |
|
||||
| `LINE_SLOW_RESPONSE_THRESHOLD` | no | `45` | Seconds before the postback button fires (`0` = disabled) |
|
||||
| `LINE_PENDING_TEXT` | no | "🤔 Still thinking…" | Bubble text shown alongside the postback button |
|
||||
| `LINE_BUTTON_LABEL` | no | "Get answer" | Button label |
|
||||
| `LINE_DELIVERED_TEXT` | no | "Already replied ✅" | Reply when an already-delivered button is tapped again |
|
||||
| `LINE_INTERRUPTED_TEXT` | no | "Run was interrupted before completion." | Reply when a `/stop` orphan button is tapped |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"invalid signature" on webhook verify.** The `Channel secret` was copied wrong, or your tunnel rewrote the request body. Verify with `curl -i https://<tunnel>/line/webhook/health` first — that should return `{"status":"ok","platform":"line"}`.
|
||||
|
||||
**Bot receives nothing in groups.** Check `LINE_ALLOWED_GROUPS` includes the `C...` group ID. To find a group ID, send a test message and grep `~/.hermes/logs/gateway.log` for `LINE: rejecting unauthorized source` — the rejected source dict has the IDs.
|
||||
|
||||
**`send_image` fails with "LINE_PUBLIC_URL must be set".** LINE's Messaging API does not accept binary uploads — images, audio, and video must be reachable HTTPS URLs. Set `LINE_PUBLIC_URL` to the tunnel's public hostname and the adapter will serve files from `/line/media/<token>/<filename>` automatically.
|
||||
|
||||
**Postback button never appears.** Either the LLM responded faster than `LINE_SLOW_RESPONSE_THRESHOLD`, or another bubble (tool-progress, streaming) consumed the reply token first. See the suppression block under "Slow LLM responses".
|
||||
|
||||
**"already in use by another profile".** The same channel access token is bound to another running Hermes profile. Stop the other gateway or use a separate channel.
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
* **Bubble and length caps.** Each LINE text bubble is capped at 5000 characters. Longer responses are smart-chunked at ~4500 characters across up to 5 bubbles per Reply/Push call, splitting on natural boundaries where possible.
|
||||
* **No native message editing.** LINE has no edit-message API — streaming responses always send fresh bubbles, never edit prior ones.
|
||||
* **No Markdown rendering.** Bold (`**`), italics (`*`), code fences, and headings render as literal characters. The adapter strips them before sending; URLs are preserved (`[label](url)` becomes `label (url)`).
|
||||
* **Loading indicator is DM-only.** LINE rejects the chat/loading API for groups and rooms, so the typing indicator only shows in 1:1 chats.
|
||||
@@ -0,0 +1,938 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
title: "Matrix"
|
||||
description: "Set up Hermes Agent as a Matrix bot"
|
||||
---
|
||||
|
||||
# Matrix Setup
|
||||
|
||||
Hermes Agent integrates with Matrix, the open, federated messaging protocol. Matrix lets you run your own homeserver or use a public one like matrix.org — either way, you keep control of your communications. The bot connects via the `mautrix` Python SDK, processes messages through the Hermes Agent pipeline (including tool use, memory, and reasoning), and responds in real time. It supports text, file attachments, images, audio, video, and optional end-to-end encryption (E2EE).
|
||||
|
||||
Hermes works with any Matrix homeserver — Synapse, Conduit, Dendrite, or matrix.org.
|
||||
|
||||
Before setup, here's the part most people want to know: how Hermes behaves once it's connected.
|
||||
|
||||
## How Hermes Behaves
|
||||
|
||||
| Context | Behavior |
|
||||
|---------|----------|
|
||||
| **DMs** | Hermes responds to every message. No `@mention` needed. Each DM has its own session. Set `MATRIX_DM_MENTION_THREADS=true` to start a thread when the bot is `@mentioned` in a DM. |
|
||||
| **Rooms** | By default, Hermes requires an `@mention` to respond. Set `MATRIX_REQUIRE_MENTION=false` or add room IDs to `MATRIX_FREE_RESPONSE_ROOMS` for free-response rooms. Room invites are auto-accepted. |
|
||||
| **Threads** | Hermes supports Matrix threads (MSC3440). If you reply in a thread, Hermes keeps the thread context isolated from the main room timeline. Threads where the bot has already participated do not require a mention. |
|
||||
| **Auto-threading** | By default, Hermes auto-creates a thread for each message it responds to in a room. This keeps conversations isolated. Set `MATRIX_AUTO_THREAD=false` to disable. Set `MATRIX_DM_AUTO_THREAD=true` (default false) to also auto-create threads for DM messages — this is distinct from `MATRIX_DM_MENTION_THREADS`, which only starts a thread when the bot is `@mentioned` in a DM. |
|
||||
| **Commands** | Hermes accepts normal `/commands` when your Matrix client sends them. If your client reserves `/` for local commands, use `!commands` instead; Hermes normalizes known `!command` aliases to `/command`. |
|
||||
| **Interactive controls** | Dangerous-command approval and `/model` selection can use Matrix reactions. Approval reactions can be limited to the user who requested the action. |
|
||||
| **Thinking and tool activity** | Matrix uses threaded, editable thinking/tool-activity panes when gateway progress is enabled, so updates do not flood the main room timeline. |
|
||||
| **Shared rooms with multiple users** | By default, Hermes isolates session history per user inside the room. Two people talking in the same room do not share one transcript unless you explicitly disable that. |
|
||||
|
||||
:::tip
|
||||
The bot automatically joins rooms when invited. Just invite the bot's Matrix user to any room and it will join and start responding.
|
||||
:::
|
||||
|
||||
## Capability Matrix
|
||||
|
||||
This table is backed by the Matrix adapter capability declaration and Matrix test
|
||||
coverage. E2EE is mode-based because deployments choose whether encrypted rooms
|
||||
are disabled, opportunistic, or required.
|
||||
|
||||
| Capability | Matrix |
|
||||
|------------|--------|
|
||||
| text | yes |
|
||||
| threads | yes |
|
||||
| reactions | yes |
|
||||
| approvals | yes |
|
||||
| model picker | yes |
|
||||
| thinking panes | yes |
|
||||
| images | yes |
|
||||
| multiple images | yes |
|
||||
| files | yes |
|
||||
| voice/audio | yes |
|
||||
| video | yes |
|
||||
| E2EE | off / optional / required |
|
||||
| diagnostics | yes |
|
||||
|
||||
### Session Model in Matrix
|
||||
|
||||
By default:
|
||||
|
||||
- each DM gets its own session
|
||||
- each thread gets its own session namespace
|
||||
- each user in a shared room gets their own session inside that room
|
||||
|
||||
This is controlled by `config.yaml`:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
Set it to `false` only if you explicitly want one shared conversation for the entire room:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: false
|
||||
```
|
||||
|
||||
Shared sessions can be useful for a collaborative room, but they also mean:
|
||||
|
||||
- users share context growth and token costs
|
||||
- one person's long tool-heavy task can bloat everyone else's context
|
||||
- one person's in-flight run can interrupt another person's follow-up in the same room
|
||||
|
||||
### Mention and Threading Configuration
|
||||
|
||||
You can configure mention and auto-threading behavior via environment variables or `config.yaml`:
|
||||
|
||||
```yaml
|
||||
matrix:
|
||||
require_mention: true # Require @mention in rooms (default: true)
|
||||
allowed_users: # Matrix users allowed to trigger agent turns
|
||||
- "@alice:matrix.org"
|
||||
allowed_rooms: # Matrix rooms allowed to trigger agent turns
|
||||
- "!abc123:matrix.org"
|
||||
free_response_rooms: # Rooms exempt from mention requirement
|
||||
- "!abc123:matrix.org"
|
||||
ignore_user_patterns: # Bridge/appservice ghost users to ignore
|
||||
- "^@telegram_"
|
||||
- "^@whatsapp_"
|
||||
process_notices: false # Ignore m.notice by default
|
||||
session_scope: room # auto|room|thread; room is recommended for project rooms
|
||||
auto_thread: true # Auto-create threads for responses (default: true)
|
||||
dm_mention_threads: false # Create thread when @mentioned in DM (default: false)
|
||||
```
|
||||
|
||||
Or via environment variables:
|
||||
|
||||
```bash
|
||||
MATRIX_REQUIRE_MENTION=true
|
||||
MATRIX_ALLOWED_USERS=@alice:matrix.org
|
||||
MATRIX_ALLOWED_ROOMS=!abc123:matrix.org
|
||||
MATRIX_FREE_RESPONSE_ROOMS=!abc123:matrix.org,!def456:matrix.org
|
||||
MATRIX_IGNORE_USER_PATTERNS='^@telegram_,^@whatsapp_'
|
||||
MATRIX_PROCESS_NOTICES=false
|
||||
MATRIX_SESSION_SCOPE=room # recommended for stable project-room context
|
||||
MATRIX_AUTO_THREAD=true
|
||||
MATRIX_DM_MENTION_THREADS=false
|
||||
MATRIX_REACTIONS=true # default: true — emoji reactions during processing
|
||||
MATRIX_ALLOW_ROOM_MENTIONS=false
|
||||
```
|
||||
|
||||
:::tip Disabling reactions
|
||||
`MATRIX_REACTIONS=false` turns off the processing-lifecycle emoji reactions (👀/✅/❌) the bot posts on inbound messages. Useful for rooms where reaction events are noisy or aren't supported by all participating clients.
|
||||
:::
|
||||
|
||||
:::tip Room-wide mentions
|
||||
Hermes sends structured Matrix user mentions for explicit Matrix IDs such as `@alice:example.org`. Room-wide `@room` notifications are disabled by default; set `MATRIX_ALLOW_ROOM_MENTIONS=true` only in rooms where the bot is allowed to notify everyone.
|
||||
:::
|
||||
|
||||
:::note
|
||||
If you are upgrading from a version that did not have `MATRIX_REQUIRE_MENTION`, the bot previously responded to all messages in rooms. To preserve that behavior, set `MATRIX_REQUIRE_MENTION=false`.
|
||||
:::
|
||||
|
||||
### Project Room Isolation
|
||||
|
||||
If you use the same Matrix bot in multiple project rooms, configure stable
|
||||
room-scoped sessions:
|
||||
|
||||
```bash
|
||||
MATRIX_SESSION_SCOPE=room
|
||||
MATRIX_AUTO_THREAD=false
|
||||
```
|
||||
|
||||
`MATRIX_SESSION_SCOPE` accepts:
|
||||
|
||||
| Scope | Behavior |
|
||||
|-------|----------|
|
||||
| `auto` | Backward-compatible default. Existing `MATRIX_AUTO_THREAD` behavior controls synthetic threads. |
|
||||
| `room` | Unthreaded room messages stay in one stable room session. Real Matrix threads still use their thread root. |
|
||||
| `thread` | Unthreaded room messages synthesize a thread/session from the triggering event ID. |
|
||||
|
||||
Hermes now includes the current Matrix room name, room ID, topic, message ID,
|
||||
and a Matrix room-boundary note in the agent prompt. `/status` also shows the
|
||||
current Matrix room/session scope, and `/resume` will not silently resume a
|
||||
named session from another Matrix room unless you explicitly use
|
||||
`/resume --cross-room <session name>`.
|
||||
|
||||
`MATRIX_SESSION_SCOPE=room` controls the room/thread lane. The existing
|
||||
`group_sessions_per_user` setting still controls whether users inside that room
|
||||
share the lane. With `group_sessions_per_user: true` (default), Alice and Bob get
|
||||
separate Project B sessions. With `group_sessions_per_user: false`, the room has
|
||||
one shared Project B transcript.
|
||||
|
||||
This guide walks you through the full setup process — from creating your bot account to sending your first message.
|
||||
|
||||
## Step 1: Create a Bot Account
|
||||
|
||||
You need a Matrix user account for the bot. There are several ways to do this:
|
||||
|
||||
### Option A: Register on Your Homeserver (Recommended)
|
||||
|
||||
If you run your own homeserver (Synapse, Conduit, Dendrite):
|
||||
|
||||
1. Use the admin API or registration tool to create a new user:
|
||||
|
||||
```bash
|
||||
# Synapse example
|
||||
register_new_matrix_user -c /etc/synapse/homeserver.yaml http://localhost:8008
|
||||
```
|
||||
|
||||
2. Choose a username like `hermes` — the full user ID will be `@hermes:your-server.org`.
|
||||
|
||||
### Option B: Use matrix.org or Another Public Homeserver
|
||||
|
||||
1. Go to [Element Web](https://app.element.io) and create a new account.
|
||||
2. Pick a username for your bot (e.g., `hermes-bot`).
|
||||
|
||||
### Option C: Use Your Own Account
|
||||
|
||||
You can also run Hermes as your own user. This means the bot posts as you — useful for personal assistants.
|
||||
|
||||
## Step 2: Get an Access Token
|
||||
|
||||
Hermes needs an access token to authenticate with the homeserver. You have two options:
|
||||
|
||||
### Option A: Access Token (Recommended)
|
||||
|
||||
The most reliable way to get a token:
|
||||
|
||||
**Via Element:**
|
||||
1. Log in to [Element](https://app.element.io) with the bot account.
|
||||
2. Go to **Settings** → **Help & About**.
|
||||
3. Scroll down and expand **Advanced** — the access token is displayed there.
|
||||
4. **Copy it immediately.**
|
||||
|
||||
**Via the API:**
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server/_matrix/client/v3/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "m.login.password",
|
||||
"user": "@hermes:your-server.org",
|
||||
"password": "your-password"
|
||||
}'
|
||||
```
|
||||
|
||||
The response includes an `access_token` field — copy it.
|
||||
|
||||
:::warning[Keep your access token safe]
|
||||
The access token gives full access to the bot's Matrix account. Never share it publicly or commit it to Git. If compromised, revoke it by logging out all sessions for that user.
|
||||
:::
|
||||
|
||||
### Option B: Password Login
|
||||
|
||||
Instead of providing an access token, you can give Hermes the bot's user ID and password. Hermes will log in automatically on startup. This is simpler but means the password is stored in your `.env` file.
|
||||
|
||||
```bash
|
||||
MATRIX_USER_ID=@hermes:your-server.org
|
||||
MATRIX_PASSWORD=your-password
|
||||
```
|
||||
|
||||
## Step 3: Find Your Matrix User ID
|
||||
|
||||
Hermes Agent uses your Matrix User ID to control who can interact with the bot. Matrix User IDs follow the format `@username:server`.
|
||||
|
||||
To find yours:
|
||||
|
||||
1. Open [Element](https://app.element.io) (or your preferred Matrix client).
|
||||
2. Click your avatar → **Settings**.
|
||||
3. Your User ID is displayed at the top of the profile (e.g., `@alice:matrix.org`).
|
||||
|
||||
:::tip
|
||||
Matrix User IDs always start with `@` and contain a `:` followed by the server name. For example: `@alice:matrix.org`, `@bob:your-server.com`.
|
||||
:::
|
||||
|
||||
## Step 4: Configure Hermes Agent
|
||||
|
||||
### Option A: Interactive Setup (Recommended)
|
||||
|
||||
Run the guided setup command:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **Matrix** when prompted, then provide your homeserver URL, access token (or user ID + password), and allowed user IDs when asked.
|
||||
|
||||
### Option B: Manual Configuration
|
||||
|
||||
Add the following to your `~/.hermes/.env` file:
|
||||
|
||||
**Using an access token:**
|
||||
|
||||
```bash
|
||||
# Required
|
||||
MATRIX_HOMESERVER=https://matrix.example.org
|
||||
MATRIX_ACCESS_TOKEN=***
|
||||
|
||||
# Optional: user ID (auto-detected from token if omitted)
|
||||
# MATRIX_USER_ID=@hermes:matrix.example.org
|
||||
|
||||
# Security: restrict who can interact with the bot
|
||||
MATRIX_ALLOWED_USERS=@alice:matrix.example.org
|
||||
|
||||
# Optional: restrict which rooms can trigger the bot
|
||||
MATRIX_ALLOWED_ROOMS=!abc123:matrix.example.org
|
||||
|
||||
# Multiple allowed users (comma-separated)
|
||||
# MATRIX_ALLOWED_USERS=@alice:matrix.example.org,@bob:matrix.example.org
|
||||
```
|
||||
|
||||
**Using password login:**
|
||||
|
||||
```bash
|
||||
# Required
|
||||
MATRIX_HOMESERVER=https://matrix.example.org
|
||||
MATRIX_USER_ID=@hermes:matrix.example.org
|
||||
MATRIX_PASSWORD=***
|
||||
|
||||
# Security
|
||||
MATRIX_ALLOWED_USERS=@alice:matrix.example.org
|
||||
```
|
||||
|
||||
## Private Deployment Hardening
|
||||
|
||||
For private Matrix deployments, set both user and room allowlists. If
|
||||
`MATRIX_ALLOWED_USERS` is unset, any sender who can reach the bot in a joined
|
||||
room can trigger an agent turn. If `MATRIX_ALLOWED_ROOMS` is unset, any room the
|
||||
bot joins can trigger an agent turn. A locked-down deployment should set both:
|
||||
|
||||
```bash
|
||||
MATRIX_ALLOWED_USERS=@alice:matrix.example.org,@bob:matrix.example.org
|
||||
MATRIX_ALLOWED_ROOMS=!ops:matrix.example.org,!dmroom:matrix.example.org
|
||||
```
|
||||
|
||||
Bridge and appservice deployments need extra loop protection. Hermes always
|
||||
ignores its own events, Matrix appservice-style users whose localpart starts
|
||||
with `_`, duplicate event IDs, old startup events, edit replacement events, and
|
||||
`m.notice` events by default. Add deployment-specific bridge ghost patterns when
|
||||
your bridge uses a different naming convention:
|
||||
|
||||
```bash
|
||||
MATRIX_IGNORE_USER_PATTERNS='^@telegram_,^@slack_,^@whatsapp_'
|
||||
```
|
||||
|
||||
Only enable notices when a trusted human workflow really sends `m.notice`:
|
||||
|
||||
```bash
|
||||
MATRIX_PROCESS_NOTICES=true
|
||||
```
|
||||
|
||||
Outbound whole-room notifications are disabled by default. Keep
|
||||
`MATRIX_ALLOW_ROOM_MENTIONS=false` unless the bot is explicitly allowed to wake
|
||||
the whole room with `@room`.
|
||||
|
||||
Diagnostics and debug payloads redact Matrix access tokens, recovery keys,
|
||||
device identifiers, and message bodies. Media downloads are limited to Matrix
|
||||
`mxc://` content URIs and rejected when they exceed `MATRIX_MAX_MEDIA_BYTES`.
|
||||
Treat federated rooms and untrusted homeservers as untrusted input: keep room
|
||||
allowlists tight, prefer DMs or private rooms for tool-heavy work, and avoid
|
||||
authorizing bridge ghosts or appservice puppets as allowed users.
|
||||
|
||||
Optional behavior settings in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
- `group_sessions_per_user: true` keeps each participant's context isolated inside shared rooms
|
||||
|
||||
### Start the Gateway
|
||||
|
||||
Once configured, start the Matrix gateway:
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
The bot should connect to your homeserver and start syncing within a few seconds. Send it a message — either a DM or in a room it has joined — to test.
|
||||
|
||||
:::tip
|
||||
You can run `hermes gateway` in the background or as a systemd service for persistent operation. See the deployment docs for details.
|
||||
:::
|
||||
|
||||
## End-to-End Encryption (E2EE)
|
||||
|
||||
Hermes supports Matrix end-to-end encryption, so you can chat with your bot in encrypted rooms.
|
||||
|
||||
### Requirements
|
||||
|
||||
E2EE requires the `mautrix` library with encryption extras and the `libolm` C library:
|
||||
|
||||
```bash
|
||||
# Install mautrix with E2EE support
|
||||
pip install 'mautrix[encryption]'
|
||||
|
||||
# Or install with hermes extras
|
||||
pip install 'hermes-agent[matrix]'
|
||||
```
|
||||
|
||||
You also need `libolm` installed on your system:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install libolm-dev
|
||||
|
||||
# macOS
|
||||
brew install libolm
|
||||
|
||||
# Fedora
|
||||
sudo dnf install libolm-devel
|
||||
```
|
||||
|
||||
### Enable E2EE
|
||||
|
||||
Add to your `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
MATRIX_E2EE_MODE=required
|
||||
```
|
||||
|
||||
`MATRIX_E2EE_MODE` accepts:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `off` | Do not initialize Matrix E2EE. |
|
||||
| `optional` | Try E2EE when dependencies are available, but keep unencrypted rooms working if crypto cannot initialize. |
|
||||
| `required` | Fail closed if E2EE dependencies or crypto setup are not available. |
|
||||
|
||||
Optional mode may fall back to non-E2EE operation when crypto setup is unavailable. Required mode fails closed instead of silently downgrading.
|
||||
|
||||
For backwards compatibility, `MATRIX_ENCRYPTION=true` still enables required E2EE behavior.
|
||||
|
||||
When E2EE is enabled, Hermes:
|
||||
|
||||
- Stores encryption keys in `~/.hermes/platforms/matrix/store/` (legacy installs: `~/.hermes/matrix/store/`)
|
||||
- Uploads device keys on first connection
|
||||
- Decrypts incoming messages and encrypts outgoing messages automatically
|
||||
- Auto-joins encrypted rooms when invited
|
||||
|
||||
### Matrix Tools and Controls
|
||||
|
||||
In Matrix conversations, Hermes exposes Matrix-specific tools to the agent:
|
||||
|
||||
- `matrix_send_reaction`
|
||||
- `matrix_redact_message`
|
||||
- `matrix_create_room`
|
||||
- `matrix_invite_user`
|
||||
- `matrix_fetch_history`
|
||||
- `matrix_set_presence`
|
||||
|
||||
These tools are scoped to Matrix contexts and are not available in non-Matrix toolsets. Admin-style tools are disabled by default: redaction requires `MATRIX_TOOLS_ALLOW_REDACTION=true`, invites require `MATRIX_TOOLS_ALLOW_INVITES=true`, and room creation requires `MATRIX_TOOLS_ALLOW_ROOM_CREATE=true`. Public room creation also requires `MATRIX_ALLOW_PUBLIC_ROOMS=true`.
|
||||
Matrix tools are limited to the current Matrix room by default. Explicit
|
||||
cross-room targets require `MATRIX_TOOLS_ALLOW_CROSS_ROOM=true`; redaction and
|
||||
invite-like cross-room actions additionally require
|
||||
`MATRIX_TOOLS_ALLOW_CROSS_ROOM_DESTRUCTIVE=true`. If `MATRIX_ALLOWED_ROOMS` is
|
||||
set, Matrix tools may only target those rooms.
|
||||
|
||||
Reaction controls use:
|
||||
|
||||
- ✅ approve once
|
||||
- ♾️ approve always
|
||||
- ❌ deny
|
||||
- number reactions for `/model` choices
|
||||
|
||||
Set `MATRIX_APPROVAL_REQUIRE_SENDER=false` if you intentionally want any authorized Matrix user in the room to operate an approval/model picker prompt. The default is requester-bound when Hermes knows who requested the action.
|
||||
|
||||
### Media Limits
|
||||
|
||||
Hermes uploads and downloads Matrix images, files, audio, and video through Matrix media APIs. Multiple generated images are sent as one ordered logical batch, preserving captions and thread context across the batch.
|
||||
|
||||
By default, Matrix media over 100 MB is rejected before upload/download. Override with:
|
||||
|
||||
```bash
|
||||
MATRIX_MAX_MEDIA_BYTES=104857600
|
||||
```
|
||||
|
||||
Inbound media must use Matrix `mxc://` content URIs. Hermes rejects arbitrary
|
||||
HTTP(S) media URLs in Matrix events to avoid turning a federated room into an
|
||||
unrestricted downloader.
|
||||
|
||||
## Synapse Integration Tests
|
||||
|
||||
Hermes includes an opt-in Synapse harness for local validation:
|
||||
|
||||
```bash
|
||||
docker compose -f tests/e2e/matrix_synapse_gateway/docker-compose.yml up -d
|
||||
HERMES_MATRIX_SYNAPSE_INTEGRATION=1 \
|
||||
scripts/run_tests.sh -m "integration and matrix_synapse" \
|
||||
tests/e2e/matrix_synapse_gateway/test_gateway.py
|
||||
docker compose -f tests/e2e/matrix_synapse_gateway/docker-compose.yml down -v
|
||||
```
|
||||
|
||||
The harness creates temporary users through Synapse shared-secret registration
|
||||
and covers private-room send/receive, named-room invite/join, media
|
||||
upload/download, bot response delivery, and startup old-event filtering. E2EE
|
||||
smoke coverage is separately marked with `matrix_e2ee` so it can stay opt-in on
|
||||
developer machines.
|
||||
|
||||
### Cross-Signing Verification (Recommended)
|
||||
|
||||
If your Matrix account has cross-signing enabled (the default in Element), set the recovery key so the bot can self-sign its device on startup. Without this, other Matrix clients may refuse to share encryption sessions with the bot after a device key rotation.
|
||||
|
||||
```bash
|
||||
MATRIX_RECOVERY_KEY=EsT... your recovery key here
|
||||
```
|
||||
|
||||
**Where to find it:** In Element, go to **Settings** → **Security & Privacy** → **Encryption** → your recovery key (also called the "Security Key"). This is the key you were asked to save when you first set up cross-signing.
|
||||
|
||||
On each startup, if `MATRIX_RECOVERY_KEY` is set, Hermes imports cross-signing keys from the homeserver's secure secret storage and signs the current device. This is idempotent and safe to leave enabled permanently.
|
||||
|
||||
If Hermes bootstraps a new Matrix recovery key, it never logs the raw key. Set
|
||||
`MATRIX_RECOVERY_KEY_OUTPUT_FILE=/secure/path/matrix-recovery-key.txt` before
|
||||
startup to write a generated key once with file mode `0600`; the file is not
|
||||
overwritten if it already exists.
|
||||
|
||||
:::warning[Deleting the crypto store]
|
||||
If you delete `~/.hermes/platforms/matrix/store/crypto.db`, the bot loses its encryption identity. Simply restarting with the same device ID will **not** fully recover — the homeserver still holds one-time keys signed with the old identity key, and peers cannot establish new Olm sessions.
|
||||
|
||||
Hermes detects this condition on startup and refuses to enable E2EE, logging: `device XXXX has stale one-time keys on the server signed with a previous identity key`.
|
||||
|
||||
**Easiest recovery: generate a new access token** (which gets a fresh device ID with no stale key history). See the "Upgrading from a previous version with E2EE" section below. This is the most reliable path and avoids touching the homeserver database.
|
||||
|
||||
**Manual recovery** (advanced — keeps the same device ID):
|
||||
|
||||
1. Stop Synapse and delete the old device from its database:
|
||||
```bash
|
||||
sudo systemctl stop matrix-synapse
|
||||
sudo sqlite3 /var/lib/matrix-synapse/homeserver.db "
|
||||
DELETE FROM e2e_device_keys_json WHERE device_id = 'DEVICE_ID' AND user_id = '@hermes:your-server';
|
||||
DELETE FROM e2e_one_time_keys_json WHERE device_id = 'DEVICE_ID' AND user_id = '@hermes:your-server';
|
||||
DELETE FROM e2e_fallback_keys_json WHERE device_id = 'DEVICE_ID' AND user_id = '@hermes:your-server';
|
||||
DELETE FROM devices WHERE device_id = 'DEVICE_ID' AND user_id = '@hermes:your-server';
|
||||
"
|
||||
sudo systemctl start matrix-synapse
|
||||
```
|
||||
Or via the Synapse admin API (note the URL-encoded user ID):
|
||||
```bash
|
||||
curl -X DELETE -H "Authorization: Bearer ADMIN_TOKEN" \
|
||||
'https://your-server/_synapse/admin/v2/users/%40hermes%3Ayour-server/devices/DEVICE_ID'
|
||||
```
|
||||
Note: deleting a device via the admin API may also invalidate the associated access token. You may need to generate a new token afterward.
|
||||
|
||||
2. Delete the local crypto store and restart Hermes:
|
||||
```bash
|
||||
rm -f ~/.hermes/platforms/matrix/store/crypto.db*
|
||||
# restart hermes
|
||||
```
|
||||
|
||||
Other Matrix clients (Element, matrix-commander) may cache the old device keys. After recovery, type `/discardsession` in Element to force a new encryption session with the bot.
|
||||
:::
|
||||
|
||||
:::info
|
||||
If `mautrix[encryption]` is not installed or `libolm` is missing, the bot falls back to a plain (unencrypted) client automatically. You'll see a warning in the logs.
|
||||
:::
|
||||
|
||||
## Home Room
|
||||
|
||||
You can designate a "home room" where the bot sends proactive messages (such as cron job output, reminders, and notifications). There are two ways to set it:
|
||||
|
||||
### Using the Slash Command
|
||||
|
||||
Type `/sethome` in any Matrix room where the bot is present. That room becomes the home room.
|
||||
If your Matrix client intercepts slash commands, type `!sethome` instead.
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Add this to your `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
MATRIX_HOME_ROOM=!abc123def456:matrix.example.org
|
||||
```
|
||||
|
||||
## Room allowlist (`allowed_rooms`)
|
||||
|
||||
Restrict the bot to a fixed set of Matrix rooms. When set, the bot **only** responds in rooms whose ID appears in the list — messages from any other room are silently ignored, even if the bot is mentioned.
|
||||
|
||||
**DMs (direct chat rooms) are exempt** from this filter, so authorized users can always reach the bot one-on-one.
|
||||
|
||||
```yaml
|
||||
matrix:
|
||||
allowed_rooms:
|
||||
- "!abc123def456:matrix.example.org"
|
||||
- "!opsroom789:matrix.example.org"
|
||||
```
|
||||
|
||||
Or via env var (comma-separated):
|
||||
|
||||
```bash
|
||||
MATRIX_ALLOWED_ROOMS="!abc123def456:matrix.example.org,!opsroom789:matrix.example.org"
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- Empty / unset → no restriction (default).
|
||||
- Non-empty → room ID must be on the list. The check runs **before** any other gating (mention requirement, sender allowlist, etc.).
|
||||
- Use the room's **internal ID** (`!abc...:server`), not its alias (`#room:server`). You can find a room's internal ID in Element via Room → Settings → Advanced.
|
||||
|
||||
See also: [admin/user slash command split](../../reference/slash-commands.md#permissions-and-adminuser-split).
|
||||
|
||||
|
||||
:::tip
|
||||
To find a Room ID: in Element, go to the room → **Settings** → **Advanced** → the **Internal room ID** is shown there (starts with `!`).
|
||||
:::
|
||||
|
||||
## Commands in Matrix
|
||||
|
||||
Hermes supports the same gateway commands in Matrix that it supports on other
|
||||
messaging platforms, including `/commands`, `/model`, `/stop`, `/queue`,
|
||||
`/steer`, `/goal`, `/subgoal`, `/background`, `/bg`, `/btw`, `/tasks`, and
|
||||
`/yolo`.
|
||||
|
||||
Some Matrix clients reserve leading `/` for local client commands and may not
|
||||
send unknown slash commands to the room. In that case, use `!` as a Matrix-safe
|
||||
alias:
|
||||
|
||||
```text
|
||||
!commands
|
||||
!model
|
||||
!model gpt-5.5 --provider openrouter
|
||||
!queue continue with the next task
|
||||
!stop
|
||||
```
|
||||
|
||||
Hermes only normalizes `!command` when the command is known to the gateway, a
|
||||
registered plugin command, or an installed skill command. Ordinary exclamations
|
||||
such as `!important` remain normal chat messages.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot is not responding to messages
|
||||
|
||||
**Cause**: The bot hasn't joined the room, `MATRIX_ALLOWED_USERS` doesn't include your User ID, `MATRIX_ALLOWED_ROOMS` doesn't include the room, or a room message did not mention the bot.
|
||||
|
||||
**Fix**: Invite the bot to the room — it auto-joins on invite. Verify your User ID is in `MATRIX_ALLOWED_USERS` (use the full `@user:server` format) and the room ID is in `MATRIX_ALLOWED_ROOMS` if that allowlist is configured. In rooms, mention the bot or add the room to `MATRIX_FREE_RESPONSE_ROOMS`. Restart the gateway.
|
||||
|
||||
### Bot joins rooms but silently drops every message (clock skew)
|
||||
|
||||
**Cause**: The host's system clock is set ahead of real time. The Matrix adapter applies a 5-second startup-grace filter (`event_ts < startup_ts - 5`) to ignore events replayed from initial sync. When the wall clock is ahead, every incoming event looks "older than startup" and is dropped before reaching the message handler — the bot appears connected but never replies. See [#12614](https://github.com/NousResearch/hermes-agent/issues/12614).
|
||||
|
||||
**Symptom**: Gateway log shows `Matrix: dropped N live events as 'too old' more than 30s after startup`.
|
||||
|
||||
**Fix**: Sync the host clock with NTP and restart the bot:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo timedatectl set-ntp true
|
||||
timedatectl status # confirm "System clock synchronized: yes"
|
||||
|
||||
# macOS
|
||||
sudo sntp -sS time.apple.com
|
||||
```
|
||||
|
||||
### "Failed to authenticate" / "whoami failed" on startup
|
||||
|
||||
**Cause**: The access token or homeserver URL is incorrect.
|
||||
|
||||
**Fix**: Verify `MATRIX_HOMESERVER` points to your homeserver (include `https://`, no trailing slash). Check that `MATRIX_ACCESS_TOKEN` is valid — try it with curl:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
https://your-server/_matrix/client/v3/account/whoami
|
||||
```
|
||||
|
||||
If this returns your user info, the token is valid. If it returns an error, generate a new token.
|
||||
|
||||
### "mautrix not installed" error
|
||||
|
||||
**Cause**: The `mautrix` Python package is not installed.
|
||||
|
||||
**Fix**: Install it:
|
||||
|
||||
```bash
|
||||
pip install 'mautrix[encryption]'
|
||||
```
|
||||
|
||||
Or with Hermes extras:
|
||||
|
||||
```bash
|
||||
pip install 'hermes-agent[matrix]'
|
||||
```
|
||||
|
||||
### Encryption errors / "could not decrypt event"
|
||||
|
||||
**Cause**: Missing encryption keys, `libolm` not installed, or the bot's device isn't trusted.
|
||||
|
||||
**Fix**:
|
||||
1. Verify `libolm` is installed on your system (see the E2EE section above).
|
||||
2. Make sure `MATRIX_ENCRYPTION=true` is set in your `.env`.
|
||||
3. In your Matrix client (Element), go to the bot's profile -> Sessions -> verify/trust the bot's device.
|
||||
4. If the bot just joined an encrypted room, it can only decrypt messages sent *after* it joined. Older messages are inaccessible.
|
||||
|
||||
### Upgrading from a previous version with E2EE
|
||||
|
||||
:::tip
|
||||
If you also manually deleted `crypto.db`, see the "Deleting the crypto store" warning in the E2EE section above — there are additional steps to clear stale one-time keys from the homeserver.
|
||||
:::
|
||||
|
||||
If you previously used Hermes with `MATRIX_ENCRYPTION=true` and are upgrading to
|
||||
a version that uses the new SQLite-based crypto store, the bot's encryption
|
||||
identity has changed. Your Matrix client (Element) may cache the old device keys
|
||||
and refuse to share encryption sessions with the bot.
|
||||
|
||||
**Symptoms**: The bot connects and shows "E2EE enabled" in the logs, but all
|
||||
messages show "could not decrypt event" and the bot never responds.
|
||||
|
||||
**What's happening**: The old encryption state (from the previous `matrix-nio` or
|
||||
serialization-based `mautrix` backend) is incompatible with the new SQLite crypto
|
||||
store. The bot creates a fresh encryption identity, but your Matrix client still
|
||||
has the old keys cached and won't share the room's encryption session with a
|
||||
device whose keys changed. This is a Matrix security feature -- clients treat
|
||||
changed identity keys for the same device as suspicious.
|
||||
|
||||
**Fix** (one-time migration):
|
||||
|
||||
1. **Generate a new access token** to get a fresh device ID. The simplest way:
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-server/_matrix/client/v3/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "m.login.password",
|
||||
"identifier": {"type": "m.id.user", "user": "@hermes:your-server.org"},
|
||||
"password": "***",
|
||||
"initial_device_display_name": "Hermes Agent"
|
||||
}'
|
||||
```
|
||||
|
||||
Copy the new `access_token` and update `MATRIX_ACCESS_TOKEN` in `~/.hermes/.env`.
|
||||
|
||||
2. **Delete old encryption state**:
|
||||
|
||||
```bash
|
||||
rm -f ~/.hermes/platforms/matrix/store/crypto.db
|
||||
rm -f ~/.hermes/platforms/matrix/store/crypto_store.*
|
||||
```
|
||||
|
||||
3. **Set your recovery key** (if you use cross-signing — most Element users do). Add to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
MATRIX_RECOVERY_KEY=EsT... your recovery key here
|
||||
```
|
||||
|
||||
This lets the bot self-sign with cross-signing keys on startup, so Element trusts the new device immediately. Without this, Element may see the new device as unverified and refuse to share encryption sessions. Find your recovery key in Element under **Settings** → **Security & Privacy** → **Encryption**.
|
||||
|
||||
4. **Force your Matrix client to rotate the encryption session**. In Element,
|
||||
open the DM room with the bot and type `/discardsession`. This forces Element
|
||||
to create a new encryption session and share it with the bot's new device.
|
||||
|
||||
5. **Restart the gateway**:
|
||||
|
||||
```bash
|
||||
hermes gateway run
|
||||
```
|
||||
|
||||
If `MATRIX_RECOVERY_KEY` is set, you should see `Matrix: cross-signing verified via recovery key` in the logs.
|
||||
|
||||
6. **Send a new message**. The bot should decrypt and respond normally.
|
||||
|
||||
:::note
|
||||
After migration, messages sent *before* the upgrade cannot be decrypted -- the old
|
||||
encryption keys are gone. This only affects the transition; new messages work
|
||||
normally.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
**New installations are not affected.** This migration is only needed if you had
|
||||
a working E2EE setup with a previous version of Hermes and are upgrading.
|
||||
|
||||
**Why a new access token?** Each Matrix access token is bound to a specific device
|
||||
ID. Reusing the same device ID with new encryption keys causes other Matrix
|
||||
clients to distrust the device (they see changed identity keys as a potential
|
||||
security breach). A new access token gets a new device ID with no stale key
|
||||
history, so other clients trust it immediately.
|
||||
:::
|
||||
|
||||
## Proxy Mode (E2EE on macOS)
|
||||
|
||||
Matrix E2EE requires `libolm`, which doesn't compile on macOS ARM64 (Apple Silicon). The `hermes-agent[matrix]` extra is gated to Linux only. If you're on macOS, proxy mode lets you run E2EE in a Docker container on a Linux VM while the actual agent runs natively on macOS with full access to your local files, memory, and skills.
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
macOS (Host):
|
||||
└─ hermes gateway
|
||||
├─ api_server adapter ← listens on 0.0.0.0:8642
|
||||
├─ AIAgent ← single source of truth
|
||||
├─ Sessions, memory, skills
|
||||
└─ Local file access (Obsidian, projects, etc.)
|
||||
|
||||
Linux VM (Docker):
|
||||
└─ hermes gateway (proxy mode)
|
||||
├─ Matrix adapter ← E2EE decryption/encryption
|
||||
└─ HTTP forward → macOS:8642/v1/chat/completions
|
||||
(no LLM API keys, no agent, no inference)
|
||||
```
|
||||
|
||||
The Docker container only handles Matrix protocol + E2EE. When a message arrives, it decrypts it and forwards the text to the host via a standard HTTP request. The host runs the agent, calls tools, generates a response, and streams it back. The container encrypts and sends the response to Matrix. All sessions are unified — CLI, Matrix, Telegram, and any other platform share the same memory and conversation history.
|
||||
|
||||
### Step 1: Configure the Host (macOS)
|
||||
|
||||
Enable the API server so the host accepts incoming requests from the Docker container.
|
||||
|
||||
Add to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
API_SERVER_ENABLED=true
|
||||
API_SERVER_KEY=your-secret-key-here
|
||||
API_SERVER_HOST=0.0.0.0
|
||||
```
|
||||
|
||||
- `API_SERVER_HOST=0.0.0.0` binds to all interfaces so the Docker container can reach it.
|
||||
- `API_SERVER_KEY` is required for non-loopback binding. Pick a strong random string.
|
||||
- The API server runs on port 8642 by default (change with `API_SERVER_PORT` if needed).
|
||||
|
||||
Start the gateway:
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
You should see the API server start alongside any other platforms you have configured. Verify it's reachable from the VM:
|
||||
|
||||
```bash
|
||||
# From the Linux VM
|
||||
curl http://<mac-ip>:8642/health
|
||||
```
|
||||
|
||||
### Step 2: Configure the Docker Container (Linux VM)
|
||||
|
||||
The container needs Matrix credentials and the proxy URL. It does NOT need LLM API keys.
|
||||
|
||||
**`docker-compose.yml`:**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
hermes-matrix:
|
||||
build: .
|
||||
environment:
|
||||
# Matrix credentials
|
||||
MATRIX_HOMESERVER: "https://matrix.example.org"
|
||||
MATRIX_ACCESS_TOKEN: "syt_..."
|
||||
MATRIX_ALLOWED_USERS: "@you:matrix.example.org"
|
||||
MATRIX_ENCRYPTION: "true"
|
||||
MATRIX_DEVICE_ID: "HERMES_BOT"
|
||||
|
||||
# Proxy mode — forward to host agent
|
||||
GATEWAY_PROXY_URL: "http://192.168.1.100:8642"
|
||||
GATEWAY_PROXY_KEY: "your-secret-key-here"
|
||||
volumes:
|
||||
- ./matrix-store:/root/.hermes/platforms/matrix/store
|
||||
```
|
||||
|
||||
**`Dockerfile`:**
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y libolm-dev && rm -rf /var/lib/apt/lists/*
|
||||
RUN pip install 'hermes-agent[matrix]'
|
||||
|
||||
CMD ["hermes", "gateway"]
|
||||
```
|
||||
|
||||
That's the entire container. No API keys for OpenRouter, Anthropic, or any inference provider.
|
||||
|
||||
### Step 3: Start Both
|
||||
|
||||
1. Start the host gateway first:
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
2. Start the Docker container:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3. Send a message in an encrypted Matrix room. The container decrypts it, forwards it to the host, and streams the response back.
|
||||
|
||||
### Configuration Reference
|
||||
|
||||
Proxy mode is configured on the **container side** (the thin gateway):
|
||||
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| `GATEWAY_PROXY_URL` | URL of the remote Hermes API server (e.g., `http://192.168.1.100:8642`) |
|
||||
| `GATEWAY_PROXY_KEY` | Bearer token for authentication (must match `API_SERVER_KEY` on the host) |
|
||||
| `gateway.proxy_url` | Same as `GATEWAY_PROXY_URL` but in `config.yaml` |
|
||||
|
||||
The host side needs:
|
||||
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| `API_SERVER_ENABLED` | Set to `true` |
|
||||
| `API_SERVER_KEY` | Bearer token (shared with the container) |
|
||||
| `API_SERVER_HOST` | Set to `0.0.0.0` for network access |
|
||||
| `API_SERVER_PORT` | Port number (default: `8642`) |
|
||||
|
||||
### Works for Any Platform
|
||||
|
||||
Proxy mode is not limited to Matrix. Any platform adapter can use it — set `GATEWAY_PROXY_URL` on any gateway instance and it will forward to the remote agent instead of running one locally. This is useful for any deployment where the platform adapter needs to run in a different environment from the agent (network isolation, E2EE requirements, resource constraints).
|
||||
|
||||
:::tip
|
||||
Session continuity is maintained via the `X-Hermes-Session-Id` header. The host's API server tracks sessions by this ID, so conversations persist across messages just like they would with a local agent.
|
||||
:::
|
||||
|
||||
:::note
|
||||
**Limitations (v1):** Tool progress messages from the remote agent are not relayed back — the user sees the streamed final response only, not individual tool calls. Dangerous command approval prompts are handled on the host side, not relayed to the Matrix user. These can be addressed in future updates.
|
||||
:::
|
||||
|
||||
### Bot connects and sends, but ignores inbound messages
|
||||
|
||||
**Cause**: Matrix event handlers only fire when sync payloads are dispatched through
|
||||
mautrix's `handle_sync()` machinery. A raw `client.sync()` poll that never calls
|
||||
`handle_sync()` can leave the adapter connected (send works) while inbound
|
||||
messages never reach `_on_room_message`.
|
||||
|
||||
**Fix**: Hermes uses an explicit sync loop that calls `client.handle_sync()` on
|
||||
both the initial sync and every incremental sync response. This matches the
|
||||
diagnosis in upstream issue #7914 and closed PR #37807, but keeps Hermes's own
|
||||
background maintenance tasks (joined-room tracking, invite handling, E2EE key
|
||||
share) instead of delegating the full lifecycle to `client.start()`. If inbound
|
||||
messages still fail after a gateway restart, verify handlers are registered before
|
||||
the first sync and check logs for `sync event dispatch error`.
|
||||
|
||||
### Sync issues / bot falls behind
|
||||
|
||||
**Cause**: Long-running tool executions can delay the sync loop, or the homeserver is slow.
|
||||
|
||||
**Fix**: The sync loop automatically retries every 5 seconds on error. Check the Hermes logs for sync-related warnings. If the bot consistently falls behind, ensure your homeserver has adequate resources.
|
||||
|
||||
### Bot is offline
|
||||
|
||||
**Cause**: The Hermes gateway isn't running, or it failed to connect.
|
||||
|
||||
**Fix**: Check that `hermes gateway` is running. Look at the terminal output for error messages. Common issues: wrong homeserver URL, expired access token, homeserver unreachable.
|
||||
|
||||
### "User not allowed" / Bot ignores you
|
||||
|
||||
**Cause**: Your User ID isn't in `MATRIX_ALLOWED_USERS`.
|
||||
|
||||
**Fix**: Add your User ID to `MATRIX_ALLOWED_USERS` in `~/.hermes/.env` and restart the gateway. Use the full `@user:server` format.
|
||||
|
||||
### Bot ignores an entire room
|
||||
|
||||
**Cause**: `MATRIX_ALLOWED_ROOMS` is set and the current room ID is not listed, or the room requires a mention and the message did not mention the bot.
|
||||
|
||||
**Fix**: Add the room ID to `MATRIX_ALLOWED_ROOMS`, or remove the room allowlist if this is a personal deployment. To find a Room ID in Element, open room settings and check **Advanced**.
|
||||
|
||||
### Bridge messages loop or echo
|
||||
|
||||
**Cause**: A bridge/appservice puppet is relaying bot output back as a new user message, or a bridge uses non-standard ghost user IDs.
|
||||
|
||||
**Fix**: Keep bridge ghosts out of `MATRIX_ALLOWED_USERS`, add a matching `MATRIX_IGNORE_USER_PATTERNS` entry, and leave `MATRIX_PROCESS_NOTICES=false` unless notices are part of a trusted workflow.
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
Always set `MATRIX_ALLOWED_USERS` and, for shared/private deployments, `MATRIX_ALLOWED_ROOMS`. Without them, anyone who can message the bot in a joined room may trigger the agent. Only authorize people and rooms you trust — authorized users have full access to the agent's capabilities, including tool use and system access.
|
||||
:::
|
||||
|
||||
For more information on securing your Hermes Agent deployment, see the [Security Guide](../security.md).
|
||||
|
||||
## Notes
|
||||
|
||||
- **Any homeserver**: Works with Synapse, Conduit, Dendrite, matrix.org, or any spec-compliant Matrix homeserver. No specific homeserver software required.
|
||||
- **Federation**: If you're on a federated homeserver, the bot can communicate with users from other servers — just add their full `@user:server` IDs to `MATRIX_ALLOWED_USERS`.
|
||||
- **Auto-join**: The bot automatically accepts room invites and joins. It starts responding immediately after joining.
|
||||
- **Media support**: Hermes can send and receive images, audio, video, and file attachments. Media is uploaded to your homeserver using the Matrix content repository API.
|
||||
- **Native voice messages (MSC3245)**: The Matrix adapter automatically tags outgoing voice messages with the `org.matrix.msc3245.voice` flag. This means TTS responses and voice audio are rendered as **native voice bubbles** in Element and other clients that support MSC3245, rather than as generic audio file attachments. Incoming voice messages with the MSC3245 flag are also correctly identified and routed to speech-to-text transcription. No configuration is needed — this works automatically.
|
||||
@@ -0,0 +1,340 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
title: "Mattermost"
|
||||
description: "Set up Hermes Agent as a Mattermost bot"
|
||||
---
|
||||
|
||||
# Mattermost Setup
|
||||
|
||||
Hermes Agent integrates with Mattermost as a bot, letting you chat with your AI assistant through direct messages or team channels. Mattermost is a self-hosted, open-source Slack alternative — you run it on your own infrastructure, keeping full control of your data. The bot connects via Mattermost's REST API (v4) and WebSocket for real-time events, processes messages through the Hermes Agent pipeline (including tool use, memory, and reasoning), and responds in real time. It supports text, file attachments, images, and slash commands.
|
||||
|
||||
No external Mattermost library is required — the adapter uses `aiohttp`, which is already a Hermes dependency.
|
||||
|
||||
Before setup, here's the part most people want to know: how Hermes behaves once it's in your Mattermost instance.
|
||||
|
||||
## How Hermes Behaves
|
||||
|
||||
| Context | Behavior |
|
||||
|---------|----------|
|
||||
| **DMs** | Hermes responds to every message. No `@mention` needed. Each DM has its own session. |
|
||||
| **Public/private channels** | Hermes responds when you `@mention` it. Without a mention, Hermes ignores the message. |
|
||||
| **Threads** | If `MATTERMOST_REPLY_MODE=thread`, Hermes replies in a thread under your message. Thread context stays isolated from the parent channel. |
|
||||
| **Shared channels with multiple users** | By default, Hermes isolates session history per user inside the channel. Two people talking in the same channel do not share one transcript unless you explicitly disable that. |
|
||||
|
||||
:::tip
|
||||
If you want Hermes to reply as threaded conversations (nested under your original message), set `MATTERMOST_REPLY_MODE=thread`. The default is `off`, which sends flat messages in the channel.
|
||||
:::
|
||||
|
||||
### Session Model in Mattermost
|
||||
|
||||
By default:
|
||||
|
||||
- each DM gets its own session
|
||||
- each thread gets its own session namespace
|
||||
- each user in a shared channel gets their own session inside that channel
|
||||
|
||||
This is controlled by `config.yaml`:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
Set it to `false` only if you explicitly want one shared conversation for the entire channel:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: false
|
||||
```
|
||||
|
||||
Shared sessions can be useful for a collaborative channel, but they also mean:
|
||||
|
||||
- users share context growth and token costs
|
||||
- one person's long tool-heavy task can bloat everyone else's context
|
||||
- one person's in-flight run can interrupt another person's follow-up in the same channel
|
||||
|
||||
This guide walks you through the full setup process — from creating your bot on Mattermost to sending your first message.
|
||||
|
||||
## Step 1: Enable Bot Accounts
|
||||
|
||||
Bot accounts must be enabled on your Mattermost server before you can create one.
|
||||
|
||||
1. Log in to Mattermost as a **System Admin**.
|
||||
2. Go to **System Console** → **Integrations** → **Bot Accounts**.
|
||||
3. Set **Enable Bot Account Creation** to **true**.
|
||||
4. Click **Save**.
|
||||
|
||||
:::info
|
||||
If you don't have System Admin access, ask your Mattermost administrator to enable bot accounts and create one for you.
|
||||
:::
|
||||
|
||||
## Step 2: Create a Bot Account
|
||||
|
||||
1. In Mattermost, click the **☰** menu (top-left) → **Integrations** → **Bot Accounts**.
|
||||
2. Click **Add Bot Account**.
|
||||
3. Fill in the details:
|
||||
- **Username**: e.g., `hermes`
|
||||
- **Display Name**: e.g., `Hermes Agent`
|
||||
- **Description**: optional
|
||||
- **Role**: `Member` is sufficient
|
||||
4. Click **Create Bot Account**.
|
||||
5. Mattermost will display the **bot token**. **Copy it immediately.**
|
||||
|
||||
:::warning[Token shown only once]
|
||||
The bot token is only displayed once when you create the bot account. If you lose it, you'll need to regenerate it from the bot account settings. Never share your token publicly or commit it to Git — anyone with this token has full control of the bot.
|
||||
:::
|
||||
|
||||
Store the token somewhere safe (a password manager, for example). You'll need it in Step 5.
|
||||
|
||||
:::tip
|
||||
You can also use a **personal access token** instead of a bot account. Go to **Profile** → **Security** → **Personal Access Tokens** → **Create Token**. This is useful if you want Hermes to post as your own user rather than a separate bot user.
|
||||
:::
|
||||
|
||||
## Step 3: Add the Bot to Channels
|
||||
|
||||
The bot needs to be a member of any channel where you want it to respond:
|
||||
|
||||
1. Open the channel where you want the bot.
|
||||
2. Click the channel name → **Add Members**.
|
||||
3. Search for your bot username (e.g., `hermes`) and add it.
|
||||
|
||||
For DMs, simply open a direct message with the bot — it will be able to respond immediately.
|
||||
|
||||
## Step 4: Find Your Mattermost User ID
|
||||
|
||||
Hermes Agent uses your Mattermost User ID to control who can interact with the bot. To find it:
|
||||
|
||||
1. Click your **avatar** (top-left corner) → **Profile**.
|
||||
2. Your User ID is displayed in the profile dialog — click it to copy.
|
||||
|
||||
Your User ID is a 26-character alphanumeric string like `3uo8dkh1p7g1mfk49ear5fzs5c`.
|
||||
|
||||
:::warning
|
||||
Your User ID is **not** your username. The username is what appears after `@` (e.g., `@alice`). The User ID is a long alphanumeric identifier that Mattermost uses internally.
|
||||
:::
|
||||
|
||||
**Alternative**: You can also get your User ID via the API:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
https://your-mattermost-server/api/v4/users/me | jq .id
|
||||
```
|
||||
|
||||
:::tip
|
||||
To get a **Channel ID**: click the channel name → **View Info**. The Channel ID is shown in the info panel. You'll need this if you want to set a home channel manually.
|
||||
:::
|
||||
|
||||
## Step 5: Configure Hermes Agent
|
||||
|
||||
### Option A: Interactive Setup (Recommended)
|
||||
|
||||
Run the guided setup command:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **Mattermost** when prompted, then paste your server URL, bot token, and user ID when asked.
|
||||
|
||||
### Option B: Manual Configuration
|
||||
|
||||
Add the following to your `~/.hermes/.env` file:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
MATTERMOST_URL=https://mm.example.com
|
||||
MATTERMOST_TOKEN=***
|
||||
MATTERMOST_ALLOWED_USERS=3uo8dkh1p7g1mfk49ear5fzs5c
|
||||
|
||||
# Multiple allowed users (comma-separated)
|
||||
# MATTERMOST_ALLOWED_USERS=3uo8dkh1p7g1mfk49ear5fzs5c,8fk2jd9s0a7bncm1xqw4tp6r3e
|
||||
|
||||
# Optional: reply mode (thread or off, default: off)
|
||||
# MATTERMOST_REPLY_MODE=thread
|
||||
|
||||
# Optional: respond without @mention (default: true = require mention)
|
||||
# MATTERMOST_REQUIRE_MENTION=false
|
||||
|
||||
# Optional: channels where bot responds without @mention (comma-separated channel IDs)
|
||||
# MATTERMOST_FREE_RESPONSE_CHANNELS=channel_id_1,channel_id_2
|
||||
```
|
||||
|
||||
Optional behavior settings in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
- `group_sessions_per_user: true` keeps each participant's context isolated inside shared channels and threads
|
||||
|
||||
### Start the Gateway
|
||||
|
||||
Once configured, start the Mattermost gateway:
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
The bot should connect to your Mattermost server within a few seconds. Send it a message — either a DM or in a channel where it's been added — to test.
|
||||
|
||||
:::tip
|
||||
You can run `hermes gateway` in the background or as a systemd service for persistent operation. See the deployment docs for details.
|
||||
:::
|
||||
|
||||
## Home Channel
|
||||
|
||||
You can designate a "home channel" where the bot sends proactive messages (such as cron job output, reminders, and notifications). There are two ways to set it:
|
||||
|
||||
### Using the Slash Command
|
||||
|
||||
Type `/sethome` in any Mattermost channel where the bot is present. That channel becomes the home channel.
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Add this to your `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
MATTERMOST_HOME_CHANNEL=abc123def456ghi789jkl012mn
|
||||
```
|
||||
|
||||
Replace the ID with the actual channel ID (click the channel name → View Info → copy the ID).
|
||||
|
||||
## Reply Mode
|
||||
|
||||
The `MATTERMOST_REPLY_MODE` setting controls how Hermes posts responses:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `off` (default) | Hermes posts flat messages in the channel, like a normal user. |
|
||||
| `thread` | Hermes replies in a thread under your original message. Keeps channels clean when there's lots of back-and-forth. |
|
||||
|
||||
Set it in your `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
MATTERMOST_REPLY_MODE=thread
|
||||
```
|
||||
|
||||
## Mention Behavior
|
||||
|
||||
By default, the bot only responds in channels when `@mentioned`. You can change this:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MATTERMOST_REQUIRE_MENTION` | `true` | Set to `false` to respond to all messages in channels (DMs always work). |
|
||||
| `MATTERMOST_FREE_RESPONSE_CHANNELS` | _(none)_ | Comma-separated channel IDs where the bot responds without `@mention`, even when require_mention is true. |
|
||||
|
||||
To find a channel ID in Mattermost: open the channel, click the channel name header, and look for the ID in the URL or channel details.
|
||||
|
||||
When the bot is `@mentioned`, the mention is automatically stripped from the message before processing.
|
||||
|
||||
## Channel allowlist (`allowed_channels`)
|
||||
|
||||
Restrict the bot to a fixed set of Mattermost channels. When set, the bot **only** responds in channels whose ID appears in the list — messages from any other channel are silently ignored, even if the bot is `@mentioned`.
|
||||
|
||||
**DMs are exempt** from this filter, so authorized users can always reach the bot in a direct message.
|
||||
|
||||
```yaml
|
||||
mattermost:
|
||||
allowed_channels:
|
||||
- "abc123def456ghi789jkl012mno" # #ops
|
||||
- "xyz987uvw654rst321opq098nml" # #incident-response
|
||||
```
|
||||
|
||||
Or via env var (comma-separated):
|
||||
|
||||
```bash
|
||||
MATTERMOST_ALLOWED_CHANNELS="abc123def456ghi789jkl012mno,xyz987uvw654rst321opq098nml"
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- Empty / unset → no restriction (fully backward compatible).
|
||||
- Non-empty → channel ID must be on the list, or the message is dropped before any other gating (mention requirement, `MATTERMOST_FREE_RESPONSE_CHANNELS`, etc.) runs.
|
||||
- Find a channel ID via the Mattermost UI → channel header → "View Info", or read it from the channel URL.
|
||||
|
||||
See also: [admin/user slash command split](../../reference/slash-commands.md#permissions-and-adminuser-split).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot is not responding to messages
|
||||
|
||||
**Cause**: The bot is not a member of the channel, or `MATTERMOST_ALLOWED_USERS` doesn't include your User ID.
|
||||
|
||||
**Fix**: Add the bot to the channel (channel name → Add Members → search for the bot). Verify your User ID is in `MATTERMOST_ALLOWED_USERS`. Restart the gateway.
|
||||
|
||||
### 403 Forbidden errors
|
||||
|
||||
**Cause**: The bot token is invalid, or the bot doesn't have permission to post in the channel.
|
||||
|
||||
**Fix**: Check that `MATTERMOST_TOKEN` in your `.env` file is correct. Make sure the bot account hasn't been deactivated. Verify the bot has been added to the channel. If using a personal access token, ensure your account has the required permissions.
|
||||
|
||||
### WebSocket disconnects / reconnection loops
|
||||
|
||||
**Cause**: Network instability, Mattermost server restarts, or firewall/proxy issues with WebSocket connections.
|
||||
|
||||
**Fix**: The adapter automatically reconnects with exponential backoff (2s → 60s). Check your server's WebSocket configuration — reverse proxies (nginx, Apache) need WebSocket upgrade headers configured. Verify no firewall is blocking WebSocket connections on your Mattermost server.
|
||||
|
||||
For nginx, ensure your config includes:
|
||||
|
||||
```nginx
|
||||
location /api/v4/websocket {
|
||||
proxy_pass http://mattermost-backend;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
```
|
||||
|
||||
### "Failed to authenticate" on startup
|
||||
|
||||
**Cause**: The token or server URL is incorrect.
|
||||
|
||||
**Fix**: Verify `MATTERMOST_URL` points to your Mattermost server (include `https://`, no trailing slash). Check that `MATTERMOST_TOKEN` is valid — try it with curl:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
https://your-server/api/v4/users/me
|
||||
```
|
||||
|
||||
If this returns your bot's user info, the token is valid. If it returns an error, regenerate the token.
|
||||
|
||||
### Bot is offline
|
||||
|
||||
**Cause**: The Hermes gateway isn't running, or it failed to connect.
|
||||
|
||||
**Fix**: Check that `hermes gateway` is running. Look at the terminal output for error messages. Common issues: wrong URL, expired token, Mattermost server unreachable.
|
||||
|
||||
### "User not allowed" / Bot ignores you
|
||||
|
||||
**Cause**: Your User ID isn't in `MATTERMOST_ALLOWED_USERS`.
|
||||
|
||||
**Fix**: Add your User ID to `MATTERMOST_ALLOWED_USERS` in `~/.hermes/.env` and restart the gateway. Remember: the User ID is a 26-character alphanumeric string, not your `@username`.
|
||||
|
||||
## Per-Channel Prompts
|
||||
|
||||
Assign ephemeral system prompts to specific Mattermost channels. The prompt is injected at runtime on every turn — never persisted to transcript history — so changes take effect immediately.
|
||||
|
||||
```yaml
|
||||
mattermost:
|
||||
channel_prompts:
|
||||
"channel_id_abc123": |
|
||||
You are a research assistant. Focus on academic sources,
|
||||
citations, and concise synthesis.
|
||||
"channel_id_def456": |
|
||||
Code review mode. Be precise about edge cases and
|
||||
performance implications.
|
||||
```
|
||||
|
||||
Keys are Mattermost channel IDs (find them in the channel URL or via the API). All messages in the matching channel get the prompt injected as an ephemeral system instruction.
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
Always set `MATTERMOST_ALLOWED_USERS` to restrict who can interact with the bot. Without it, the gateway denies all users by default as a safety measure. Only add User IDs of people you trust — authorized users have full access to the agent's capabilities, including tool use and system access.
|
||||
:::
|
||||
|
||||
For more information on securing your Hermes Agent deployment, see the [Security Guide](../security.md).
|
||||
|
||||
## Notes
|
||||
|
||||
- **Self-hosted friendly**: Works with any self-hosted Mattermost instance. No Mattermost Cloud account or subscription required.
|
||||
- **No extra dependencies**: The adapter uses `aiohttp` for HTTP and WebSocket, which is already included with Hermes Agent.
|
||||
- **Team Edition compatible**: Works with both Mattermost Team Edition (free) and Enterprise Edition.
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
sidebar_position: 23
|
||||
title: "Microsoft Graph Webhook Listener"
|
||||
description: "Receive Microsoft Graph change notifications (meetings, calendar, chat, etc.) in Hermes"
|
||||
---
|
||||
|
||||
# Microsoft Graph Webhook Listener
|
||||
|
||||
The `msgraph_webhook` gateway platform is an inbound event listener. It's how Hermes receives **change notifications** from Microsoft Graph — "a Teams meeting ended," "a new message landed in this chat," "this calendar event was updated." Different from the `teams` platform (which is a chat bot users type to) — this one is M365 telling Hermes something happened, not a person.
|
||||
|
||||
Right now the primary consumer is the Teams meeting summary pipeline: Graph notifies when a meeting produces a transcript, the pipeline fetches it, and Hermes posts a summary back into Teams. Other Graph resources (`/chats/.../messages`, `/users/.../events`) use the same listener — the pipeline consumers land with their own PRs.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Microsoft Graph application credentials — [Register a Microsoft Graph Application](/guides/microsoft-graph-app-registration)
|
||||
- A **public HTTPS URL** that Microsoft Graph can reach (Graph does not call private endpoints). A dev tunnel works for testing; production needs a real domain with a valid certificate.
|
||||
- A strong shared secret to use as the `clientState` value. Generate with `openssl rand -hex 32` and put it in `~/.hermes/.env` as `MSGRAPH_WEBHOOK_CLIENT_STATE`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Minimum `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
msgraph_webhook:
|
||||
enabled: true
|
||||
extra:
|
||||
host: 127.0.0.1
|
||||
port: 8646
|
||||
client_state: "replace-with-a-strong-secret"
|
||||
accepted_resources:
|
||||
- "communications/onlineMeetings"
|
||||
```
|
||||
|
||||
Or via env vars in `~/.hermes/.env` (auto-merged on startup):
|
||||
|
||||
```bash
|
||||
MSGRAPH_WEBHOOK_ENABLED=true
|
||||
MSGRAPH_WEBHOOK_PORT=8646
|
||||
MSGRAPH_WEBHOOK_CLIENT_STATE=<generate-with-openssl-rand-hex-32>
|
||||
MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES=communications/onlineMeetings
|
||||
```
|
||||
|
||||
Note: the bind host is read from `extra.host` in `config.yaml` (see the example above); there is no `MSGRAPH_WEBHOOK_HOST` env-var override.
|
||||
|
||||
Start the gateway: `hermes gateway run`. The listener exposes:
|
||||
|
||||
- `POST /msgraph/webhook` — change notifications from Graph
|
||||
- `GET /msgraph/webhook?validationToken=...` — Graph subscription validation handshake
|
||||
- `GET /health` — readiness probe with accepted/duplicate counters
|
||||
|
||||
Expose the listener publicly (reverse proxy, dev tunnel, ingress). Your notification URL for Graph subscriptions is your public HTTPS origin followed by `/msgraph/webhook`:
|
||||
|
||||
```
|
||||
https://ops.example.com/msgraph/webhook
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All settings go under `platforms.msgraph_webhook.extra`:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `host` | `0.0.0.0` | Bind address for the HTTP listener. Non-loopback binds require `allowed_source_cidrs`; loopback (`127.0.0.1` / `::1`) is the easiest dev-tunnel / reverse-proxy setup. |
|
||||
| `port` | `8646` | Bind port. |
|
||||
| `webhook_path` | `/msgraph/webhook` | URL path Graph POSTs to. |
|
||||
| `health_path` | `/health` | Readiness endpoint. |
|
||||
| `client_state` | — | Shared secret Graph echoes in every notification. Compared with `hmac.compare_digest` — generate with `openssl rand -hex 32`. |
|
||||
| `accepted_resources` | `[]` (accept all) | Allowlist of Graph resource paths/patterns. Trailing `*` acts as prefix match. Leading `/` is tolerated. Example: `["communications/onlineMeetings", "chats/*/messages"]`. |
|
||||
| `max_seen_receipts` | `5000` | Dedupe cache size for notification IDs. Oldest entries evicted when the cap is hit. |
|
||||
| `allowed_source_cidrs` | `[]` | Required for non-loopback binds. Leave empty only when the listener is bound to loopback and fronted by a local tunnel / reverse proxy. |
|
||||
|
||||
Most settings also have an equivalent env var (`MSGRAPH_WEBHOOK_*`) that merges into the config at gateway startup (the exception is `host`, which is config-only — see the note above) — see the [environment variables reference](/reference/environment-variables#microsoft-graph-teams-meetings).
|
||||
|
||||
## Security Hardening
|
||||
|
||||
### clientState is the primary auth check
|
||||
|
||||
Every Graph notification includes the `clientState` string your subscription registered with. The listener rejects any notification whose `clientState` doesn't match, using timing-safe comparison. This is Microsoft's documented mechanism — treat the value as a strong shared secret.
|
||||
|
||||
If `client_state` is unset, the listener refuses to start.
|
||||
|
||||
### Source-IP allowlisting (production deployments)
|
||||
|
||||
For production, restrict the listener to Microsoft's published Graph webhook source IP ranges. Microsoft documents the egress ranges under the [Office 365 IP Address and URL Web service](https://learn.microsoft.com/en-us/microsoft-365/enterprise/urls-and-ip-address-ranges). Configure them as:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
msgraph_webhook:
|
||||
enabled: true
|
||||
extra:
|
||||
host: 0.0.0.0
|
||||
client_state: "..."
|
||||
allowed_source_cidrs:
|
||||
- "52.96.0.0/14"
|
||||
- "52.104.0.0/14"
|
||||
# ...add the current Microsoft 365 "Common" + "Teams" category egress ranges
|
||||
```
|
||||
|
||||
Or as an env var:
|
||||
|
||||
```bash
|
||||
MSGRAPH_WEBHOOK_ALLOWED_SOURCE_CIDRS="52.96.0.0/14,52.104.0.0/14"
|
||||
```
|
||||
|
||||
Binding a non-loopback host such as `0.0.0.0`, `::`, or a LAN IP without `allowed_source_cidrs` is refused at startup. If you're using a dev tunnel or reverse proxy on the same machine, bind Hermes to `127.0.0.1` or `::1` and leave the allowlist empty there. Invalid CIDR strings log a warning and are ignored. **Review the Microsoft IP list quarterly** — it changes.
|
||||
|
||||
### HTTPS termination
|
||||
|
||||
The listener speaks plain HTTP. Terminate TLS at your reverse proxy (Caddy, Nginx, Cloudflare Tunnel, AWS ALB) and proxy to the listener over the local network. Graph refuses to deliver to non-HTTPS endpoints, so there's no path for unencrypted traffic to reach you from Graph itself.
|
||||
|
||||
### Response hygiene
|
||||
|
||||
On success the listener returns `202 Accepted` with an empty body — internal counters stay out of the wire response. Operators can observe counts via `/health`, which is guarded by the same source-IP rules as the webhook path.
|
||||
|
||||
Status code table:
|
||||
|
||||
| Outcome | Status |
|
||||
|---------|--------|
|
||||
| Notification(s) accepted or deduped | 202 |
|
||||
| Validation handshake (GET with `validationToken`) | 200 (echoes the token) |
|
||||
| Every item in batch failed clientState | 403 |
|
||||
| Malformed JSON / missing `value` array / unknown resource | 400 |
|
||||
| Source IP not in allowlist | 403 |
|
||||
| Bare GET without `validationToken` | 400 |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | What to check |
|
||||
|---------|---------------|
|
||||
| Graph subscription validation fails | Public URL is reachable, `/msgraph/webhook` path matches, GET with `validationToken` echoes the token verbatim as `text/plain` within 10 seconds. |
|
||||
| Notifications POST but nothing ingests | `client_state` matches what you registered the subscription with. Re-run `openssl rand -hex 32` and create a new subscription if the value drifted. Check `accepted_resources` includes the resource path Graph is sending. |
|
||||
| Every notification 403s | `clientState` mismatch (forged, or subscription registered with a different value). Re-create the subscription with `hermes teams-pipeline subscribe --client-state "$MSGRAPH_WEBHOOK_CLIENT_STATE" ...` (ships with the pipeline runtime PR). |
|
||||
| Listener refuses to start on `0.0.0.0` | Set `allowed_source_cidrs` to Microsoft's current webhook egress ranges, or bind Hermes to `127.0.0.1` / `::1` behind your tunnel or reverse proxy. |
|
||||
| Listener starts but `curl http://localhost:8646/health` hangs | Port binding collision. Check `ss -tlnp \| grep 8646` and change `port:` if needed. |
|
||||
| Real Graph requests from Microsoft get 403'd | Source IP allowlist is too narrow. Widen the list to include the current Microsoft egress ranges. If you're still validating the tunnel path, bind Hermes to loopback and let the tunnel handle public exposure. |
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Register a Microsoft Graph Application](/guides/microsoft-graph-app-registration) — Azure app registration prereq
|
||||
- [Environment Variables → Microsoft Graph](/reference/environment-variables#microsoft-graph-teams-meetings) — full env var list
|
||||
- [Microsoft Teams bot setup](/user-guide/messaging/teams) — the different platform that lets users chat with Hermes in Teams
|
||||
@@ -0,0 +1,157 @@
|
||||
# ntfy
|
||||
|
||||
[ntfy](https://ntfy.sh/) is a simple HTTP-based pub-sub notification service. It works with the free public server at `ntfy.sh` or any self-hosted instance, and supports any client that can make HTTP requests — phones, browsers, scripts, watches.
|
||||
|
||||
ntfy makes a great lightweight push channel for Hermes: subscribe to a topic from the [ntfy mobile app](https://ntfy.sh/docs/subscribe/phone/), send messages to the topic to talk to the agent, get the response back on your phone.
|
||||
|
||||
> Run `hermes gateway setup` and pick **ntfy** for a guided walk-through.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A topic name (any unique string — `hermes-myname-2026` works fine)
|
||||
- The [ntfy mobile app](https://ntfy.sh/docs/subscribe/phone/) installed and subscribed to that topic
|
||||
- Optional: a self-hosted ntfy server, or an `ntfy.sh` account token for private/reserved topics
|
||||
|
||||
That's it. No SDK, no daemon, no Node.js. The adapter uses `httpx` which is already a Hermes dependency.
|
||||
|
||||
## Configure Hermes
|
||||
|
||||
### Via setup wizard
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **ntfy** and follow the prompts.
|
||||
|
||||
### Via environment variables
|
||||
|
||||
Add these to `~/.hermes/.env`:
|
||||
|
||||
```
|
||||
NTFY_TOPIC=hermes-myname-2026
|
||||
NTFY_ALLOWED_USERS=hermes-myname-2026
|
||||
NTFY_HOME_CHANNEL=hermes-myname-2026
|
||||
```
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `NTFY_TOPIC` | Yes | Topic to subscribe to (incoming messages) |
|
||||
| `NTFY_SERVER_URL` | Optional | Server URL (default: `https://ntfy.sh`) — point to a self-hosted ntfy for privacy |
|
||||
| `NTFY_TOKEN` | Optional | Bearer token (e.g. `tk_xyz`) or `user:pass` for Basic auth |
|
||||
| `NTFY_PUBLISH_TOPIC` | Optional | Different topic for outgoing replies (defaults to `NTFY_TOPIC`) |
|
||||
| `NTFY_MARKDOWN` | Optional | Set `true` to send replies with `X-Markdown: true` header |
|
||||
| `NTFY_ALLOWED_USERS` | Recommended | Comma-separated topic names allowed (treated as user IDs; see below) |
|
||||
| `NTFY_ALLOW_ALL_USERS` | Optional | Set `true` to allow every publisher — only safe for private topics with read tokens |
|
||||
| `NTFY_HOME_CHANNEL` | Optional | Default topic for cron / notification delivery |
|
||||
| `NTFY_HOME_CHANNEL_NAME` | Optional | Human label for the home channel |
|
||||
|
||||
## Identity model — read this before deploying
|
||||
|
||||
ntfy has no native authenticated user identity. The `title` field on a published message is **publisher-controlled** and can be anything the sender wants. The Hermes adapter does NOT use `title` for authorization — it would let any publisher who knows the topic spoof an allowed user.
|
||||
|
||||
Instead, **the topic name itself is the identity**. Every message published to the topic is treated as coming from the same logical user (the topic). `NTFY_ALLOWED_USERS` is therefore typically just the topic name itself — a single-entry allowlist that gates the whole channel.
|
||||
|
||||
This means **anyone who knows the topic can talk to the agent**. To make that a real trust boundary:
|
||||
|
||||
- **Self-host ntfy** and lock the topic down with [Access Control](https://docs.ntfy.sh/config/#access-control). Only authorized clients with the read/write token can publish.
|
||||
- Or **use a private topic on ntfy.sh** ([reserved topics](https://docs.ntfy.sh/publish/#reserved-topics) require an account) and protect it with a `NTFY_TOKEN`.
|
||||
- Or **pick a long, unguessable topic name** (`hermes-7d4f9c8b-2026`) and treat it as the shared secret. This is the lightest setup but the topic name leaks via any logs or screenshots.
|
||||
|
||||
In all cases, do not put sensitive data through ntfy unless the underlying topic is access-controlled.
|
||||
|
||||
## Quick start — talk to your agent from your phone
|
||||
|
||||
1. Pick a topic name: `hermes-myname-2026`
|
||||
2. On your phone: install the [ntfy app](https://ntfy.sh/docs/subscribe/phone/), tap **+**, enter `hermes-myname-2026`
|
||||
3. On the host:
|
||||
```bash
|
||||
echo 'NTFY_TOPIC=hermes-myname-2026' >> ~/.hermes/.env
|
||||
echo 'NTFY_ALLOWED_USERS=hermes-myname-2026' >> ~/.hermes/.env
|
||||
hermes gateway restart
|
||||
```
|
||||
4. From the ntfy app, send a message to the topic. The agent's reply lands as a push notification.
|
||||
|
||||
## Using ntfy with cron jobs
|
||||
|
||||
Once `NTFY_HOME_CHANNEL` is set, cron jobs can deliver to ntfy:
|
||||
|
||||
```python
|
||||
cronjob(
|
||||
action="create",
|
||||
schedule="every 1h",
|
||||
deliver="ntfy", # uses NTFY_HOME_CHANNEL
|
||||
prompt="Check for alerts and summarise."
|
||||
)
|
||||
```
|
||||
|
||||
Or target a specific topic explicitly:
|
||||
|
||||
```python
|
||||
send_message(target="ntfy:alerts-channel", message="Done!")
|
||||
```
|
||||
|
||||
This works even when the cron runs out-of-process from the gateway — the plugin registers a `standalone_sender_fn` that opens its own HTTP connection.
|
||||
|
||||
## Self-hosting ntfy
|
||||
|
||||
If you want full control:
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker run -p 80:80 -it binwiederhier/ntfy serve
|
||||
|
||||
# Native
|
||||
go install heckel.io/ntfy/v2@latest
|
||||
ntfy serve
|
||||
```
|
||||
|
||||
Then point Hermes at it:
|
||||
|
||||
```
|
||||
NTFY_SERVER_URL=https://ntfy.mydomain.com
|
||||
NTFY_TOPIC=hermes
|
||||
NTFY_TOKEN=tk_abc123 # if you've set up access control
|
||||
```
|
||||
|
||||
Self-hosting gives you topic access control, message persistence policies, attachments, and emoji tags. See the [ntfy server docs](https://docs.ntfy.sh/install/).
|
||||
|
||||
## Markdown formatting
|
||||
|
||||
ntfy clients render markdown when the publisher sets the `X-Markdown: true` header. To enable for outgoing Hermes replies:
|
||||
|
||||
```
|
||||
NTFY_MARKDOWN=true
|
||||
```
|
||||
|
||||
Or in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
ntfy:
|
||||
extra:
|
||||
markdown: true
|
||||
```
|
||||
|
||||
The mobile app supports a subset of CommonMark — bold, italic, lists, links, fenced code blocks. See [ntfy's markdown docs](https://docs.ntfy.sh/publish/#markdown-formatting) for the exact set.
|
||||
|
||||
## Outgoing-only setup (notifications without inbound)
|
||||
|
||||
If you only want Hermes to *push* notifications to ntfy (cron summaries, alerts) and never accept messages back, set both `NTFY_TOPIC` and `NTFY_PUBLISH_TOPIC` to the same value and skip `NTFY_ALLOWED_USERS` entirely. With no allowlist, the agent never responds to inbound messages — your phone gets the pushes, but the conversation is one-way.
|
||||
|
||||
## Limits
|
||||
|
||||
- **Message size**: ntfy caps message bodies at 4096 chars. Hermes truncates with a warning when this is exceeded.
|
||||
- **No typing indicators**: the protocol doesn't expose one; `send_typing` is a no-op.
|
||||
- **No threads or attachments**: ntfy is plain push notifications. Long replies stay in the message body, no thread fanout.
|
||||
- **No native user identity**: see the identity-model section above.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Auth failure / 401** — `NTFY_TOKEN` is wrong, or the token doesn't have publish/subscribe rights on this topic. The adapter halts its reconnect loop on 401 and the gateway runtime status will show `fatal: ntfy_unauthorized`. Fix the token and restart the gateway.
|
||||
|
||||
**Topic not found / 404** — `NTFY_TOPIC` doesn't exist on the configured server. For ntfy.sh, topics are auto-created on first publish, so a 404 means you're pointed at a self-hosted server that doesn't have the topic provisioned. The adapter halts its reconnect loop with `fatal: ntfy_topic_not_found`.
|
||||
|
||||
**Connected but no messages** — Check that `NTFY_ALLOWED_USERS` includes the topic name itself. With ntfy's identity model, the topic IS the user; leaving the allowlist empty rejects everything.
|
||||
|
||||
**Reconnects every 60s** — The stream keepalive default is 55s; ntfy may have intermittent network issues. The adapter applies exponential backoff (2 → 5 → 10 → 30 → 60s) and resets to 0 once a stream stays alive ≥60s.
|
||||
@@ -0,0 +1,334 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
title: "Open WebUI"
|
||||
description: "Connect Open WebUI to Hermes Agent via the OpenAI-compatible API server"
|
||||
---
|
||||
|
||||
# Open WebUI Integration
|
||||
|
||||
[Open WebUI](https://github.com/open-webui/open-webui) (126k★) is the most popular self-hosted chat interface for AI. With Hermes Agent's built-in API server, you can use Open WebUI as a polished web frontend for your agent — complete with conversation management, user accounts, and a modern chat interface.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Open WebUI<br/>browser UI<br/>port 3000"]
|
||||
B["hermes-agent<br/>gateway API server<br/>port 8642"]
|
||||
A -->|POST /v1/chat/completions| B
|
||||
B -->|SSE streaming response| A
|
||||
```
|
||||
|
||||
Open WebUI connects to Hermes Agent's API server just like it would connect to OpenAI. Hermes handles the requests with its full toolset — terminal, file operations, web search, memory, skills — and returns the final response.
|
||||
|
||||
:::important Runtime location
|
||||
The API server is a **Hermes agent runtime**, not a pure LLM proxy. For each request, Hermes creates a server-side `AIAgent` on the API-server host. Tool calls run where that API server is running.
|
||||
|
||||
For example, if a laptop points Open WebUI or another OpenAI-compatible client at a Hermes API server on a remote machine, `pwd`, file tools, browser tools, local MCP tools, and other workspace tools run on the remote API-server host, not on the laptop.
|
||||
:::
|
||||
|
||||
Open WebUI talks to Hermes server-to-server, so you do not need `API_SERVER_CORS_ORIGINS` for this integration.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### One-command local bootstrap (macOS/Linux, no Docker)
|
||||
|
||||
If you want Hermes + Open WebUI wired together locally with a reusable launcher, run:
|
||||
|
||||
```bash
|
||||
cd ~/.hermes/hermes-agent
|
||||
bash scripts/setup_open_webui.sh
|
||||
```
|
||||
|
||||
What the script does:
|
||||
|
||||
- ensures `~/.hermes/.env` contains `API_SERVER_ENABLED`, `API_SERVER_HOST`, `API_SERVER_KEY`, `API_SERVER_PORT`, and `API_SERVER_MODEL_NAME`
|
||||
- restarts the Hermes gateway so the API server comes up
|
||||
- installs Open WebUI into `~/.local/open-webui-venv`
|
||||
- writes a launcher at `~/.local/bin/start-open-webui-hermes.sh`
|
||||
- on macOS, installs a `launchd` user service; on Linux with `systemd --user`, installs a user service there
|
||||
|
||||
Defaults:
|
||||
|
||||
- Hermes API: `http://127.0.0.1:8642/v1`
|
||||
- Open WebUI: `http://127.0.0.1:8080`
|
||||
- model name advertised to Open WebUI: `Hermes Agent`
|
||||
|
||||
Useful overrides:
|
||||
|
||||
```bash
|
||||
OPEN_WEBUI_NAME='My Hermes UI' \
|
||||
OPEN_WEBUI_ENABLE_SIGNUP=true \
|
||||
HERMES_API_MODEL_NAME='My Hermes Agent' \
|
||||
bash scripts/setup_open_webui.sh
|
||||
```
|
||||
|
||||
On Linux, automatic background service setup requires a working `systemd --user` session. If you are on a headless SSH box and want to skip service installation, run:
|
||||
|
||||
```bash
|
||||
OPEN_WEBUI_ENABLE_SERVICE=false bash scripts/setup_open_webui.sh
|
||||
```
|
||||
|
||||
### 1. Enable the API server
|
||||
|
||||
```bash
|
||||
hermes config set API_SERVER_ENABLED true
|
||||
hermes config set API_SERVER_KEY your-secret-key
|
||||
```
|
||||
|
||||
`hermes config set` auto-routes the flag to `config.yaml` and the secret to `~/.hermes/.env`. If the gateway is already running, restart it so the change takes effect:
|
||||
|
||||
```bash
|
||||
hermes gateway stop && hermes gateway
|
||||
```
|
||||
|
||||
### 2. Start Hermes Agent gateway
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
You should see:
|
||||
|
||||
```
|
||||
[API Server] API server listening on http://127.0.0.1:8642
|
||||
```
|
||||
|
||||
### 3. Verify the API server is reachable
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8642/health
|
||||
# {"status": "ok", ...}
|
||||
|
||||
curl -s -H "Authorization: Bearer your-secret-key" http://127.0.0.1:8642/v1/models
|
||||
# {"object":"list","data":[{"id":"hermes-agent", ...}]}
|
||||
```
|
||||
|
||||
If `/health` fails, the gateway didn't pick up `API_SERVER_ENABLED=true` — restart it. If `/v1/models` returns `401`, your `Authorization` header doesn't match `API_SERVER_KEY`.
|
||||
|
||||
### 4. Start Open WebUI
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 \
|
||||
-e OPENAI_API_BASE_URL=http://host.docker.internal:8642/v1 \
|
||||
-e OPENAI_API_KEY=your-secret-key \
|
||||
-e ENABLE_OLLAMA_API=false \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
-v open-webui:/app/backend/data \
|
||||
--name open-webui \
|
||||
--restart always \
|
||||
ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
`ENABLE_OLLAMA_API=false` suppresses the default Ollama backend, which would otherwise show up empty and clutter the model picker. Omit it if you actually have Ollama running alongside.
|
||||
|
||||
First launch takes 15–30 seconds: Open WebUI downloads sentence-transformer embedding models (~150MB) the first time it starts. Wait for `docker logs open-webui` to settle before opening the UI.
|
||||
|
||||
### 5. Open the UI
|
||||
|
||||
Go to **http://localhost:3000**. Create your admin account (the first user becomes admin). You should see your agent in the model dropdown (named after your profile, or **hermes-agent** for the default profile). Start chatting!
|
||||
|
||||
## Docker Compose Setup
|
||||
|
||||
For a more permanent setup, create a `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
ports:
|
||||
- "3000:8080"
|
||||
volumes:
|
||||
- open-webui:/app/backend/data
|
||||
environment:
|
||||
- OPENAI_API_BASE_URL=http://host.docker.internal:8642/v1
|
||||
- OPENAI_API_KEY=your-secret-key
|
||||
- ENABLE_OLLAMA_API=false
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
open-webui:
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Configuring via the Admin UI
|
||||
|
||||
If you prefer to configure the connection through the UI instead of environment variables:
|
||||
|
||||
1. Log in to Open WebUI at **http://localhost:3000**
|
||||
2. Click your **profile avatar** → **Admin Settings**
|
||||
3. Go to **Connections**
|
||||
4. Under **OpenAI API**, click the **wrench icon** (Manage)
|
||||
5. Click **+ Add New Connection**
|
||||
6. Enter:
|
||||
- **URL**: `http://host.docker.internal:8642/v1`
|
||||
- **API Key**: the exact same value as `API_SERVER_KEY` in Hermes
|
||||
7. Click the **checkmark** to verify the connection
|
||||
8. **Save**
|
||||
|
||||
Your agent model should now appear in the model dropdown (named after your profile, or **hermes-agent** for the default profile).
|
||||
|
||||
:::warning
|
||||
Environment variables only take effect on Open WebUI's **first launch**. After that, connection settings are stored in its internal database. To change them later, use the Admin UI or delete the Docker volume and start fresh.
|
||||
:::
|
||||
|
||||
## API Type: Chat Completions vs Responses
|
||||
|
||||
Open WebUI supports two API modes when connecting to a backend:
|
||||
|
||||
| Mode | Format | When to use |
|
||||
|------|--------|-------------|
|
||||
| **Chat Completions** (default) | `/v1/chat/completions` | Recommended. Works out of the box. |
|
||||
| **Responses** (experimental) | `/v1/responses` | For server-side conversation state via `previous_response_id`. |
|
||||
|
||||
### Using Chat Completions (recommended)
|
||||
|
||||
This is the default and requires no extra configuration. Open WebUI sends standard OpenAI-format requests and Hermes Agent responds accordingly. Each request includes the full conversation history.
|
||||
|
||||
### Using Responses API
|
||||
|
||||
To use the Responses API mode:
|
||||
|
||||
1. Go to **Admin Settings** → **Connections** → **OpenAI** → **Manage**
|
||||
2. Edit your hermes-agent connection
|
||||
3. Change **API Type** from "Chat Completions" to **"Responses (Experimental)"**
|
||||
4. Save
|
||||
|
||||
With the Responses API, Open WebUI sends requests in the Responses format (`input` array + `instructions`), and Hermes Agent can preserve full tool call history across turns via `previous_response_id`. When `stream: true`, Hermes also streams spec-native `function_call` and `function_call_output` items, which enables custom structured tool-call UI in clients that render Responses events.
|
||||
|
||||
:::note
|
||||
Open WebUI currently manages conversation history client-side even in Responses mode — it sends the full message history in each request rather than using `previous_response_id`. The main advantage of Responses mode today is the structured event stream: text deltas, `function_call`, and `function_call_output` items arrive as OpenAI Responses SSE events instead of Chat Completions chunks.
|
||||
:::
|
||||
|
||||
## How It Works
|
||||
|
||||
When you send a message in Open WebUI:
|
||||
|
||||
1. Open WebUI sends a `POST /v1/chat/completions` request with your message and conversation history
|
||||
2. Hermes Agent creates a server-side `AIAgent` instance using the API server's profile, model/provider config, memory, skills, and configured API-server toolsets
|
||||
3. The agent processes your request — it may call tools (terminal, file operations, web search, etc.) on the API-server host
|
||||
4. As tools execute, **inline progress messages stream to the UI** so you can see what the agent is doing (e.g. `` `💻 ls -la` ``, `` `🔍 Python 3.12 release` ``)
|
||||
5. The agent's final text response streams back to Open WebUI
|
||||
6. Open WebUI displays the response in its chat interface
|
||||
|
||||
Your agent has access to the same tools and capabilities as that API-server Hermes instance. If the API server is remote, those tools are remote too.
|
||||
|
||||
If you need tools to run against your **local** workspace today, run Hermes locally and point it at a pure LLM provider or pure OpenAI-compatible model proxy (for example vLLM, LiteLLM, Ollama, llama.cpp, OpenAI, OpenRouter, etc.). A future split-runtime mode for "remote brain, local hands" is being tracked in [#18715](https://github.com/NousResearch/hermes-agent/issues/18715); it is not the behavior of the current API server.
|
||||
|
||||
:::tip Tool Progress
|
||||
With streaming enabled (the default), you'll see brief inline indicators as tools run — the tool emoji and its key argument. These appear in the response stream before the agent's final answer, giving you visibility into what's happening behind the scenes.
|
||||
:::
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Hermes Agent (API server)
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `API_SERVER_ENABLED` | `false` | Enable the API server |
|
||||
| `API_SERVER_PORT` | `8642` | HTTP server port |
|
||||
| `API_SERVER_HOST` | `127.0.0.1` | Bind address |
|
||||
| `API_SERVER_KEY` | _(required)_ | Bearer token for auth. Match `OPENAI_API_KEY`. |
|
||||
|
||||
### Open WebUI
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OPENAI_API_BASE_URL` | Hermes Agent's API URL (include `/v1`) |
|
||||
| `OPENAI_API_KEY` | Must be non-empty. Match your `API_SERVER_KEY`. |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No models appear in the dropdown
|
||||
|
||||
- **Check the URL has `/v1` suffix**: `http://host.docker.internal:8642/v1` (not just `:8642`)
|
||||
- **Verify the gateway is running**: `curl http://localhost:8642/health` should return `{"status": "ok"}`
|
||||
- **Check model listing**: `curl -H "Authorization: Bearer your-secret-key" http://localhost:8642/v1/models` should return a list with `hermes-agent`
|
||||
- **Docker networking**: From inside Docker, `localhost` means the container, not your host. Use `host.docker.internal` or `--network=host`.
|
||||
- **Empty Ollama backend shadowing the picker**: If you omitted `ENABLE_OLLAMA_API=false`, Open WebUI shows an empty Ollama section above your Hermes models. Restart the container with `-e ENABLE_OLLAMA_API=false` or disable Ollama in **Admin Settings → Connections**.
|
||||
|
||||
### Connection test passes but no models load
|
||||
|
||||
This is almost always the missing `/v1` suffix. Open WebUI's connection test is a basic connectivity check — it doesn't verify model listing works.
|
||||
|
||||
### Response takes a long time
|
||||
|
||||
Hermes Agent may be executing multiple tool calls (reading files, running commands, searching the web) before producing its final response. This is normal for complex queries. The response appears all at once when the agent finishes.
|
||||
|
||||
### "Invalid API key" errors
|
||||
|
||||
Make sure your `OPENAI_API_KEY` in Open WebUI matches the `API_SERVER_KEY` in Hermes Agent.
|
||||
|
||||
:::warning
|
||||
Open WebUI persists OpenAI-compatible connection settings in its own database after first launch. If you accidentally saved a wrong key in the Admin UI, fixing the environment variables alone is not enough — update or delete the saved connection in **Admin Settings → Connections**, or reset the Open WebUI data directory / database.
|
||||
:::
|
||||
|
||||
## Multi-User Setup with Profiles
|
||||
|
||||
To run separate Hermes instances per user — each with their own config, memory, and skills — use [profiles](/user-guide/profiles). Each profile runs its own API server on a different port and automatically advertises the profile name as the model in Open WebUI.
|
||||
|
||||
### 1. Create profiles and configure API servers
|
||||
|
||||
`API_SERVER_*` are env vars, not YAML config keys, so write them to each profile's `.env`. Pick ports outside the default-platform range (`8644` is the webhook adapter, `8645` is wecom-callback, `8646` is msgraph-webhook), e.g. `8650+`:
|
||||
|
||||
```bash
|
||||
hermes profile create alice
|
||||
cat >> ~/.hermes/profiles/alice/.env <<EOF
|
||||
API_SERVER_ENABLED=true
|
||||
API_SERVER_PORT=8650
|
||||
API_SERVER_KEY=alice-secret
|
||||
EOF
|
||||
|
||||
hermes profile create bob
|
||||
cat >> ~/.hermes/profiles/bob/.env <<EOF
|
||||
API_SERVER_ENABLED=true
|
||||
API_SERVER_PORT=8651
|
||||
API_SERVER_KEY=bob-secret
|
||||
EOF
|
||||
```
|
||||
|
||||
### 2. Start each gateway
|
||||
|
||||
```bash
|
||||
hermes -p alice gateway &
|
||||
hermes -p bob gateway &
|
||||
```
|
||||
|
||||
### 3. Add connections in Open WebUI
|
||||
|
||||
In **Admin Settings** → **Connections** → **OpenAI API** → **Manage**, add one connection per profile:
|
||||
|
||||
| Connection | URL | API Key |
|
||||
|-----------|-----|---------|
|
||||
| Alice | `http://host.docker.internal:8650/v1` | `alice-secret` |
|
||||
| Bob | `http://host.docker.internal:8651/v1` | `bob-secret` |
|
||||
|
||||
The model dropdown will show `alice` and `bob` as distinct models. You can assign models to Open WebUI users via the admin panel, giving each user their own isolated Hermes agent.
|
||||
|
||||
:::tip Custom Model Names
|
||||
The model name defaults to the profile name. To override it, set `API_SERVER_MODEL_NAME` in the profile's `.env`:
|
||||
```bash
|
||||
hermes -p alice config set API_SERVER_MODEL_NAME "Alice's Agent"
|
||||
```
|
||||
:::
|
||||
|
||||
## Linux Docker (no Docker Desktop)
|
||||
|
||||
On Linux without Docker Desktop, `host.docker.internal` doesn't resolve by default. Options:
|
||||
|
||||
```bash
|
||||
# Option 1: Add host mapping
|
||||
docker run --add-host=host.docker.internal:host-gateway ...
|
||||
|
||||
# Option 2: Use host networking
|
||||
docker run --network=host -e OPENAI_API_BASE_URL=http://localhost:8642/v1 ...
|
||||
|
||||
# Option 3: Use Docker bridge IP
|
||||
docker run -e OPENAI_API_BASE_URL=http://172.17.0.1:8642/v1 ...
|
||||
```
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
sidebar_position: 18
|
||||
---
|
||||
|
||||
# Photon iMessage
|
||||
|
||||
Connect Hermes to **iMessage** through [Photon][photon], a managed
|
||||
service that handles the Apple line allocation and abuse-prevention
|
||||
layer so you don't have to run your own Mac relay.
|
||||
|
||||
The free tier uses Photon's shared iMessage line pool — different
|
||||
recipients may see different sending numbers, but each conversation
|
||||
stays stable. The paid Business tier gives every user the same
|
||||
dedicated number; the plugin supports both, and the free tier is the
|
||||
recommended starting point.
|
||||
|
||||
:::info Free to start
|
||||
Photon's shared-line pool is free. No subscription is required to send
|
||||
your first iMessage from Hermes — just a phone number we can bind to
|
||||
your account.
|
||||
:::
|
||||
|
||||
## Architecture
|
||||
|
||||
Photon is a **persistent-connection** channel, like Discord or Slack —
|
||||
**no webhook, no public URL, no signing secret to manage.**
|
||||
|
||||
The `spectrum-ts` SDK holds a long-lived **gRPC stream** to Photon for
|
||||
both directions. Because the SDK is TypeScript-only, Hermes runs it in a
|
||||
small supervised **Node sidecar** and talks to it over loopback:
|
||||
|
||||
- **Inbound** — the sidecar consumes the SDK's `app.messages` gRPC
|
||||
stream and forwards each message to the Python adapter over a loopback
|
||||
`GET /inbound` (NDJSON). The adapter dedupes and dispatches it to the
|
||||
agent, reconnecting automatically if the stream drops.
|
||||
- **Outbound** — replies are loopback POSTs to the sidecar, which calls
|
||||
`space.send(...)` on the SDK.
|
||||
|
||||
The Python plugin starts, supervises, and shuts down the sidecar
|
||||
automatically.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Photon account — sign up at [app.photon.codes][app]
|
||||
- **Node.js 18.17 or newer** on PATH (`node --version`)
|
||||
- A phone number that can receive iMessage (used to bind your account)
|
||||
|
||||
That's it — there is no public URL or tunnel to set up.
|
||||
|
||||
## First-time setup
|
||||
|
||||
Either run the unified gateway wizard and pick **Photon iMessage**:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
…or run the Photon setup directly (the wizard calls the same flow):
|
||||
|
||||
```bash
|
||||
# Device-code login + project + user + sidecar deps, all in one
|
||||
hermes photon setup --phone +15551234567
|
||||
```
|
||||
|
||||
The setup, in order:
|
||||
|
||||
1. **Device login** (`client_id=photon-cli`) — opens
|
||||
`https://app.photon.codes/` for approval and stores the bearer token.
|
||||
2. **Finds or creates** the `Hermes Agent` project on your account.
|
||||
3. **Enables Spectrum**, reads the project's Spectrum id, and rotates
|
||||
the project secret.
|
||||
4. **Registers your phone number** as a Spectrum user — skipped if a
|
||||
user with that number already exists, so re-running is safe.
|
||||
5. **Prints your assigned iMessage line** — the number you text to reach
|
||||
your agent.
|
||||
6. **Runs `npm install`** inside the plugin's sidecar directory.
|
||||
|
||||
Runtime credentials are written to `~/.hermes/.env`
|
||||
(`PHOTON_PROJECT_ID` = the Spectrum project id, `PHOTON_PROJECT_SECRET`),
|
||||
the same place every other channel keeps its token. Management metadata
|
||||
(device token, dashboard project id) lives in `~/.hermes/auth.json` under
|
||||
`credential_pool.photon` / `credential_pool.photon_project`.
|
||||
|
||||
## Authorizing users
|
||||
|
||||
Photon uses the same authorization model as every other Hermes
|
||||
channel. Choose one approach:
|
||||
|
||||
**DM pairing (default).** When an unknown number messages your Photon
|
||||
line, Hermes replies with a pairing code. Approve it with:
|
||||
|
||||
```bash
|
||||
hermes pairing approve photon <CODE>
|
||||
```
|
||||
|
||||
Use `hermes pairing list` to see pending codes and approved users.
|
||||
|
||||
**Pre-authorize specific numbers** (in `~/.hermes/.env`):
|
||||
|
||||
```bash
|
||||
PHOTON_ALLOWED_USERS=+15551234567,+15559876543
|
||||
```
|
||||
|
||||
**Open access** (dev only, in `~/.hermes/.env`):
|
||||
|
||||
```bash
|
||||
PHOTON_ALLOW_ALL_USERS=true
|
||||
```
|
||||
|
||||
When `PHOTON_ALLOWED_USERS` is set, unknown senders are silently
|
||||
ignored rather than offered a pairing code (the allowlist signals you
|
||||
deliberately restricted access).
|
||||
|
||||
### Require mentions in group chats
|
||||
|
||||
By default Hermes responds to every authorized DM and group message.
|
||||
To make group chats opt-in, enable mention gating (DMs still always
|
||||
work):
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
photon:
|
||||
enabled: true
|
||||
require_mention: true
|
||||
```
|
||||
|
||||
With `require_mention: true`, group-chat messages are ignored unless
|
||||
they match a wake-word pattern. The defaults match `Hermes` and
|
||||
`@Hermes agent` variants. For a custom agent name, set regex patterns:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
photon:
|
||||
require_mention: true
|
||||
mention_patterns:
|
||||
- '(?<![\w@])@?amos\b[,:\-]?'
|
||||
```
|
||||
|
||||
Both keys also accept env vars (`PHOTON_REQUIRE_MENTION`,
|
||||
`PHOTON_MENTION_PATTERNS`). This is the same mention-gating model the
|
||||
BlueBubbles iMessage channel uses.
|
||||
|
||||
## Start the gateway
|
||||
|
||||
```bash
|
||||
hermes gateway start
|
||||
```
|
||||
|
||||
You'll see something like:
|
||||
|
||||
```
|
||||
[photon] connected — sidecar on 127.0.0.1:8789, streaming inbound over gRPC
|
||||
```
|
||||
|
||||
Send an iMessage to your assigned number and Hermes will reply.
|
||||
|
||||
## Status & troubleshooting
|
||||
|
||||
```bash
|
||||
hermes photon status
|
||||
```
|
||||
|
||||
Prints saved credentials, sidecar health, your registered number, and the
|
||||
assigned iMessage line Hermes uses. When a Photon token and dashboard project
|
||||
are available, `status` refreshes missing number rows from the dashboard
|
||||
without provisioning new lines.
|
||||
|
||||
```
|
||||
Photon iMessage status
|
||||
──────────────────────
|
||||
device token : ✓ stored
|
||||
dashboard project : 3c90c3cc-0d44-4b50-...
|
||||
spectrum project id : sp-...
|
||||
project secret : ✓ stored
|
||||
my number : +15551234567
|
||||
assigned number : +16282679185
|
||||
node binary : /usr/bin/node
|
||||
sidecar deps : ✓ installed
|
||||
```
|
||||
|
||||
Common issues:
|
||||
|
||||
- **`sidecar deps : ✗ run hermes photon install-sidecar`** — Node is
|
||||
installed but `spectrum-ts` isn't. Run the suggested command.
|
||||
- **`device token : ✗ missing`** — run `hermes photon setup` to log in.
|
||||
- **`No iMessage line assigned yet`** — Spectrum is enabled but no line
|
||||
has been provisioned; re-run `hermes photon setup` or check the
|
||||
[dashboard][app].
|
||||
- **Sidecar won't start** — confirm `node --version` is 18.17+ and that
|
||||
`hermes photon install-sidecar` completed without errors.
|
||||
|
||||
## Limits today
|
||||
|
||||
- **Inbound attachments are metadata-only.** Inbound events carry the
|
||||
filename + MIME type; the agent sees a marker but can't yet read the
|
||||
bytes. The SDK exposes attachment bytes via `content.read()`, so this
|
||||
is a sidecar follow-up.
|
||||
- **Outbound attachments are supported.** Hermes sends images, voice
|
||||
notes, video, and documents through spectrum-ts' `attachment()` /
|
||||
`voice()` content builders via the sidecar's `/send-attachment`
|
||||
endpoint. Captions arrive as a separate iMessage bubble after the
|
||||
media.
|
||||
- **Photon's free quotas:** 5,000 messages per server per day,
|
||||
50 new-conversation initiations per shared line per day. Increases
|
||||
available — email `help@photon.codes`.
|
||||
|
||||
## Env vars
|
||||
|
||||
| Variable | Default | Notes |
|
||||
|---------------------------|--------------------|--------------------------------------------|
|
||||
| `PHOTON_PROJECT_ID` | from `.env` | Spectrum project id (the SDK's `projectId`); set by setup |
|
||||
| `PHOTON_PROJECT_SECRET` | from `.env` | Project secret; set by setup |
|
||||
| `PHOTON_SIDECAR_PORT` | `8789` | Loopback port for the sidecar control + inbound channel |
|
||||
| `PHOTON_SIDECAR_AUTOSTART`| `true` | Whether the adapter spawns the sidecar |
|
||||
| `PHOTON_NODE_BIN` | `which node` | Override the Node binary path |
|
||||
| `PHOTON_HOME_CHANNEL` | (unset) | Default space id for cron / notifications |
|
||||
| `PHOTON_HOME_CHANNEL_NAME`| (unset) | Human label for the home channel |
|
||||
| `PHOTON_ALLOWED_USERS` | (unset) | Comma-separated E.164 allowlist |
|
||||
| `PHOTON_ALLOW_ALL_USERS` | `false` | Dev only — accept any sender |
|
||||
| `PHOTON_REQUIRE_MENTION` | `false` | Require a wake word before responding in groups |
|
||||
| `PHOTON_MENTION_PATTERNS` | Hermes wake words | JSON list / comma / newline regex patterns for group mentions |
|
||||
| `PHOTON_DASHBOARD_HOST` | `app.photon.codes` | Override the dashboard / device-login host |
|
||||
| `PHOTON_SPECTRUM_HOST` | `spectrum.photon.codes` | Override the Spectrum API host |
|
||||
|
||||
[photon]: https://photon.codes/
|
||||
[app]: https://app.photon.codes/
|
||||
@@ -0,0 +1,123 @@
|
||||
# QQ Bot
|
||||
|
||||
Connect Hermes to QQ via the **Official QQ Bot API (v2)** — supporting private (C2C), group @-mentions, guild, and direct messages with voice transcription.
|
||||
|
||||
## Overview
|
||||
|
||||
The QQ Bot adapter uses the [Official QQ Bot API](https://bot.q.qq.com/wiki/develop/api-v2/) to:
|
||||
|
||||
- Receive messages via a persistent **WebSocket** connection to the QQ Gateway
|
||||
- Send text and markdown replies via the **REST API**
|
||||
- Download and process images, voice messages, and file attachments
|
||||
- Transcribe voice messages using Tencent's built-in ASR or a configurable STT provider
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **QQ Bot Application** — Register at [q.qq.com](https://q.qq.com):
|
||||
- Create a new application and note your **App ID** and **App Secret**
|
||||
- Enable the required intents: C2C messages, Group @-messages, Guild messages
|
||||
- Configure your bot in sandbox mode for testing, or publish for production
|
||||
|
||||
2. **Dependencies** — The adapter requires `aiohttp` and `httpx`:
|
||||
```bash
|
||||
pip install aiohttp httpx
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Interactive setup
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **QQ Bot** from the platform list and follow the prompts.
|
||||
|
||||
### Manual configuration
|
||||
|
||||
Set the required environment variables in `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
QQ_APP_ID=your-app-id
|
||||
QQ_CLIENT_SECRET=your-app-secret
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `QQ_APP_ID` | QQ Bot App ID (required) | — |
|
||||
| `QQ_CLIENT_SECRET` | QQ Bot App Secret (required) | — |
|
||||
| `QQBOT_HOME_CHANNEL` | OpenID for cron/notification delivery | — |
|
||||
| `QQBOT_HOME_CHANNEL_NAME` | Display name for home channel | `Home` |
|
||||
| `QQ_ALLOWED_USERS` | Comma-separated user OpenIDs for DM access | open (all users) |
|
||||
| `QQ_GROUP_ALLOWED_USERS` | Comma-separated group OpenIDs for group access | — |
|
||||
| `QQ_ALLOW_ALL_USERS` | Set to `true` to allow all DMs | `false` |
|
||||
| `QQ_PORTAL_HOST` | Override the QQ portal host (set to `sandbox.q.qq.com` for sandbox routing) | `q.qq.com` |
|
||||
| `QQ_STT_API_KEY` | API key for voice-to-text provider | — |
|
||||
| `QQ_STT_BASE_URL` | (Not read directly — set `platforms.qqbot.extra.stt.baseUrl` in `config.yaml` instead) | n/a |
|
||||
| `QQ_STT_MODEL` | STT model name | `glm-asr` |
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
For fine-grained control, add platform settings to `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
qqbot:
|
||||
enabled: true
|
||||
extra:
|
||||
app_id: "your-app-id"
|
||||
client_secret: "your-secret"
|
||||
markdown_support: true # enable QQ markdown (msg_type 2). Config-only; no env-var equivalent.
|
||||
dm_policy: "open" # open | allowlist | disabled
|
||||
allow_from:
|
||||
- "user_openid_1"
|
||||
group_policy: "open" # open | allowlist | disabled
|
||||
group_allow_from:
|
||||
- "group_openid_1"
|
||||
stt:
|
||||
provider: "zai" # zai (GLM-ASR), openai (Whisper), etc.
|
||||
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4"
|
||||
apiKey: "your-stt-key"
|
||||
model: "glm-asr"
|
||||
```
|
||||
|
||||
## Voice Messages (STT)
|
||||
|
||||
Voice transcription works in two stages:
|
||||
|
||||
1. **QQ built-in ASR** (free, always tried first) — QQ provides `asr_refer_text` in voice message attachments, which uses Tencent's own speech recognition
|
||||
2. **Configured STT provider** (fallback) — If QQ's ASR doesn't return text, the adapter calls an OpenAI-compatible STT API:
|
||||
|
||||
- **Zhipu/GLM (zai)**: Default provider, uses `glm-asr` model
|
||||
- **OpenAI Whisper**: Set `QQ_STT_BASE_URL` and `QQ_STT_MODEL`
|
||||
- Any OpenAI-compatible STT endpoint
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot disconnects immediately (quick disconnect)
|
||||
|
||||
This usually means:
|
||||
- **Invalid App ID / Secret** — Double-check your credentials at q.qq.com
|
||||
- **Missing permissions** — Ensure the bot has the required intents enabled
|
||||
- **Sandbox-only bot** — If the bot is in sandbox mode, it can only receive messages from QQ's sandbox test channel
|
||||
|
||||
### Voice messages not transcribed
|
||||
|
||||
1. Check if QQ's built-in `asr_refer_text` is present in the attachment data
|
||||
2. If using a custom STT provider, verify `QQ_STT_API_KEY` is set correctly
|
||||
3. Check gateway logs for STT error messages
|
||||
|
||||
### Messages not delivered
|
||||
|
||||
- Verify the bot's **intents** are enabled at q.qq.com
|
||||
- Check `QQ_ALLOWED_USERS` if DM access is restricted
|
||||
- For group messages, ensure the bot is **@mentioned** (group policy may require allowlisting)
|
||||
- Check `QQBOT_HOME_CHANNEL` for cron/notification delivery
|
||||
|
||||
### Connection errors
|
||||
|
||||
- Ensure `aiohttp` and `httpx` are installed: `pip install aiohttp httpx`
|
||||
- Check network connectivity to `api.sgroup.qq.com` and the WebSocket gateway
|
||||
- Review gateway logs for detailed error messages and reconnect behavior
|
||||
@@ -0,0 +1,258 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
title: "Signal"
|
||||
description: "Set up Hermes Agent as a Signal messenger bot via signal-cli daemon"
|
||||
---
|
||||
|
||||
# Signal Setup
|
||||
|
||||
Hermes connects to Signal through the [signal-cli](https://github.com/AsamK/signal-cli) daemon running in HTTP mode. The adapter streams messages in real-time via SSE (Server-Sent Events) and sends responses via JSON-RPC.
|
||||
|
||||
Signal is the most privacy-focused mainstream messenger — end-to-end encrypted by default, open-source protocol, minimal metadata collection. This makes it ideal for security-sensitive agent workflows.
|
||||
|
||||
:::info No New Python Dependencies
|
||||
The Signal adapter uses `httpx` (already a core Hermes dependency) for all communication. No additional Python packages are required. You just need signal-cli installed externally.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **signal-cli** — Java-based Signal client ([GitHub](https://github.com/AsamK/signal-cli))
|
||||
- **Java 17+** runtime — required by signal-cli
|
||||
- **A phone number** with Signal installed (for linking as a secondary device)
|
||||
|
||||
### Installing signal-cli
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install signal-cli
|
||||
|
||||
# Linux (download latest release)
|
||||
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} \
|
||||
https://github.com/AsamK/signal-cli/releases/latest | sed 's/^.*\/v//')
|
||||
curl -L -O "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}.tar.gz"
|
||||
sudo tar xf "signal-cli-${VERSION}.tar.gz" -C /opt
|
||||
sudo ln -sf "/opt/signal-cli-${VERSION}/bin/signal-cli" /usr/local/bin/
|
||||
```
|
||||
|
||||
:::caution
|
||||
signal-cli is **not** in apt or snap repositories. The Linux install above downloads directly from [GitHub releases](https://github.com/AsamK/signal-cli/releases).
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Link Your Signal Account
|
||||
|
||||
Signal-cli works as a **linked device** — like WhatsApp Web, but for Signal. Your phone stays the primary device.
|
||||
|
||||
```bash
|
||||
# Generate a linking URI (displays a QR code or link)
|
||||
signal-cli link -n "HermesAgent"
|
||||
```
|
||||
|
||||
1. Open **Signal** on your phone
|
||||
2. Go to **Settings → Linked Devices**
|
||||
3. Tap **Link New Device**
|
||||
4. Scan the QR code or enter the URI
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Start the signal-cli Daemon
|
||||
|
||||
```bash
|
||||
# Replace +1234567890 with your Signal phone number (E.164 format)
|
||||
signal-cli --account +1234567890 daemon --http 127.0.0.1:8080
|
||||
```
|
||||
|
||||
:::tip
|
||||
Keep this running in the background. You can use `systemd`, `tmux`, `screen`, or run it as a service.
|
||||
:::
|
||||
|
||||
Verify it's running:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8080/api/v1/check
|
||||
# Should return: {"versions":{"signal-cli":...}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Configure Hermes
|
||||
|
||||
The easiest way:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **Signal** from the platform menu. The wizard will:
|
||||
|
||||
1. Check if signal-cli is installed
|
||||
2. Prompt for the HTTP URL (default: `http://127.0.0.1:8080`)
|
||||
3. Test connectivity to the daemon
|
||||
4. Ask for your account phone number
|
||||
5. Configure allowed users and access policies
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Add to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
SIGNAL_HTTP_URL=http://127.0.0.1:8080
|
||||
SIGNAL_ACCOUNT=+1234567890
|
||||
|
||||
# Security (recommended)
|
||||
SIGNAL_ALLOWED_USERS=+1234567890,+0987654321 # Comma-separated E.164 numbers or UUIDs
|
||||
|
||||
# Optional
|
||||
SIGNAL_GROUP_ALLOWED_USERS=groupId1,groupId2 # Enable groups (omit to disable, * for all)
|
||||
SIGNAL_HOME_CHANNEL=+1234567890 # Default delivery target for cron jobs
|
||||
```
|
||||
|
||||
Then start the gateway:
|
||||
|
||||
```bash
|
||||
hermes gateway # Foreground
|
||||
hermes gateway install # Install as a user service
|
||||
sudo hermes gateway install --system # Linux only: boot-time system service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Access Control
|
||||
|
||||
### DM Access
|
||||
|
||||
DM access follows the same pattern as all other Hermes platforms:
|
||||
|
||||
1. **`SIGNAL_ALLOWED_USERS` set** → only those users can message
|
||||
2. **No allowlist set** → unknown users get a DM pairing code (approve via `hermes pairing approve signal CODE`)
|
||||
3. **`SIGNAL_ALLOW_ALL_USERS=true`** → anyone can message (use with caution)
|
||||
|
||||
### Group Access
|
||||
|
||||
Group access is controlled by the `SIGNAL_GROUP_ALLOWED_USERS` env var:
|
||||
|
||||
| Configuration | Behavior |
|
||||
|---------------|----------|
|
||||
| Not set (default) | All group messages are ignored. The bot only responds to DMs. |
|
||||
| Set with group IDs | Only listed groups are monitored (e.g., `groupId1,groupId2`). |
|
||||
| Set to `*` | The bot responds in any group it's a member of. |
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Attachments
|
||||
|
||||
The adapter supports sending and receiving media in both directions.
|
||||
|
||||
**Incoming** (user → agent):
|
||||
|
||||
- **Images** — PNG, JPEG, GIF, WebP (auto-detected via magic bytes)
|
||||
- **Audio** — MP3, OGG, WAV, M4A (voice messages transcribed if Whisper is configured)
|
||||
- **Documents** — PDF, ZIP, and other file types
|
||||
|
||||
**Outgoing** (agent → user):
|
||||
|
||||
The agent can send media files via `MEDIA:` tags in responses. The following delivery methods are supported:
|
||||
|
||||
- **Images** — `send_multiple_images` and `send_image_file` send PNG, JPEG, GIF, WebP as native Signal attachments
|
||||
- **Voice** — `send_voice` sends audio files (OGG, MP3, WAV, M4A, AAC) as attachments
|
||||
- **Video** — `send_video` sends MP4 video files
|
||||
- **Documents** — `send_document` sends any file type (PDF, ZIP, etc.)
|
||||
|
||||
All outgoing media goes through Signal's standard attachment API. Unlike some platforms, Signal does not distinguish between voice messages and file attachments at the protocol level.
|
||||
|
||||
Attachment size limit: **100 MB** (both directions).
|
||||
:::warning
|
||||
**Signal servers will rate-limit attachment uploads**, the adapter uses a scheduler for multiple image sending that batches images in groups of 32 and throttles uploads to match the Signal server policy.
|
||||
:::
|
||||
|
||||
### Native Formatting, Reply Quotes, and Reactions
|
||||
|
||||
Signal messages render with **native formatting** instead of literal markdown characters. The adapter converts markdown (`**bold**`, `*italic*`, `` `code` ``, `~~strike~~`, `||spoiler||`, headings) into Signal `bodyRanges` so the text shows up with real styling on the recipient's client rather than as visible `**` / `` ` `` characters.
|
||||
|
||||
**Reply quotes.** When Hermes replies to a specific message, it now posts a native reply that quotes the original — same UI affordance Signal users see when they use "Reply" themselves. This is automatic for replies generated in response to an inbound message.
|
||||
|
||||
**Reactions.** The agent can react to messages via the standard reaction API; reactions surface in Signal as emoji reactions on the referenced message rather than as extra text.
|
||||
|
||||
None of this requires additional config — it ships on by default in recent signal-cli builds. If your `signal-cli` version is too old, Hermes falls back to plaintext delivery and logs a one-time warning.
|
||||
|
||||
### Typing Indicators
|
||||
|
||||
The bot sends typing indicators while processing messages, refreshing every 8 seconds.
|
||||
|
||||
### Tool Progress Display
|
||||
|
||||
Signal does not support editing already-sent messages. Hermes therefore suppresses gateway tool-progress bubbles on Signal, even when `/verbose` is enabled and saves a non-`off` mode for the platform.
|
||||
|
||||
You can still see tool activity in the CLI, and final Signal replies can include normal assistant output. If you need live per-tool progress in chat, use a messaging platform with message editing support.
|
||||
|
||||
### Phone Number Redaction
|
||||
|
||||
All phone numbers are automatically redacted in logs:
|
||||
- `+15551234567` → `+155****4567`
|
||||
- This applies to both Hermes gateway logs and the global redaction system
|
||||
|
||||
### Note to Self (Single-Number Setup)
|
||||
|
||||
If you run signal-cli as a **linked secondary device** on your own phone number (rather than a separate bot number), you can interact with Hermes through Signal's "Note to Self" feature.
|
||||
|
||||
Just send a message to yourself from your phone — signal-cli picks it up and Hermes responds in the same conversation.
|
||||
|
||||
**How it works:**
|
||||
- "Note to Self" messages arrive as `syncMessage.sentMessage` envelopes
|
||||
- The adapter detects when these are addressed to the bot's own account and processes them as regular inbound messages
|
||||
- Echo-back protection (sent-timestamp tracking) prevents infinite loops — the bot's own replies are filtered out automatically
|
||||
|
||||
**No extra configuration needed.** This works automatically as long as `SIGNAL_ACCOUNT` matches your phone number.
|
||||
|
||||
### Health Monitoring
|
||||
|
||||
The adapter monitors the SSE connection and automatically reconnects if:
|
||||
- The connection drops (with exponential backoff: 2s → 60s)
|
||||
- No activity is detected for 120 seconds (pings signal-cli to verify)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| **"Cannot reach signal-cli"** during setup | Ensure signal-cli daemon is running: `signal-cli --account +YOUR_NUMBER daemon --http 127.0.0.1:8080` |
|
||||
| **Messages not received** | Check that `SIGNAL_ALLOWED_USERS` includes the sender's number in E.164 format (with `+` prefix) |
|
||||
| **"signal-cli not found on PATH"** | Install signal-cli and ensure it's in your PATH, or use Docker |
|
||||
| **Connection keeps dropping** | Check signal-cli logs for errors. Ensure Java 17+ is installed. |
|
||||
| **Group messages ignored** | Configure `SIGNAL_GROUP_ALLOWED_USERS` with specific group IDs, or `*` to allow all groups. |
|
||||
| **Bot responds to no one** | Configure `SIGNAL_ALLOWED_USERS`, use DM pairing, or explicitly allow all users through gateway policy if you want broader access. |
|
||||
| **Duplicate messages** | Ensure only one signal-cli instance is listening on your phone number |
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
**Always configure access controls.** The bot has terminal access by default. Without `SIGNAL_ALLOWED_USERS` or DM pairing, the gateway denies all incoming messages as a safety measure.
|
||||
:::
|
||||
|
||||
- Phone numbers are redacted in all log output
|
||||
- Use DM pairing or explicit allowlists for safe onboarding of new users
|
||||
- Keep groups disabled unless you specifically need group support, or allowlist only the groups you trust
|
||||
- Signal's end-to-end encryption protects message content in transit
|
||||
- The signal-cli session data in `~/.local/share/signal-cli/` contains account credentials — protect it like a password
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `SIGNAL_HTTP_URL` | Yes | — | signal-cli HTTP endpoint |
|
||||
| `SIGNAL_ACCOUNT` | Yes | — | Bot phone number (E.164) |
|
||||
| `SIGNAL_ALLOWED_USERS` | No | — | Comma-separated phone numbers/UUIDs |
|
||||
| `SIGNAL_GROUP_ALLOWED_USERS` | No | — | Group IDs to monitor, or `*` for all (omit to disable groups) |
|
||||
| `SIGNAL_ALLOW_ALL_USERS` | No | `false` | Allow any user to interact (skip allowlist) |
|
||||
| `SIGNAL_HOME_CHANNEL` | No | — | Default delivery target for cron jobs |
|
||||
@@ -0,0 +1,134 @@
|
||||
# SimpleX Chat
|
||||
|
||||
[SimpleX Chat](https://simplex.chat/) is a private, decentralised messaging platform where users own their contacts and groups. Unlike other platforms, SimpleX assigns no persistent user IDs — every contact is identified by an opaque internal ID generated at connection time, which makes it one of the most private messengers available.
|
||||
|
||||
> Run `hermes gateway setup` and pick **SimpleX** for a guided walk-through.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The **simplex-chat** CLI installed and running as a daemon
|
||||
- Python package **websockets** (`pip install websockets`)
|
||||
|
||||
## Install simplex-chat
|
||||
|
||||
Download the latest release from the [simplex-chat GitHub releases](https://github.com/simplex-chat/simplex-chat/releases) page:
|
||||
|
||||
```bash
|
||||
# Linux / macOS binary
|
||||
curl -L https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-22_04-x86_64 -o simplex-chat
|
||||
chmod +x simplex-chat
|
||||
```
|
||||
|
||||
The SimpleX Chat project does not publish a prebuilt Docker image for the chat client; to run it under Docker, build from source from the [simplex-chat repository](https://github.com/simplex-chat/simplex-chat).
|
||||
|
||||
## Start the daemon
|
||||
|
||||
```bash
|
||||
simplex-chat -p 5225
|
||||
```
|
||||
|
||||
The daemon listens on WebSocket at `ws://127.0.0.1:5225` by default.
|
||||
|
||||
## Configure Hermes
|
||||
|
||||
### Via setup wizard
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **SimpleX Chat** and follow the prompts.
|
||||
|
||||
### Via environment variables
|
||||
|
||||
Add these to `~/.hermes/.env`:
|
||||
|
||||
```
|
||||
SIMPLEX_WS_URL=ws://127.0.0.1:5225
|
||||
SIMPLEX_ALLOWED_USERS=<contact-id-1>,<contact-id-2>
|
||||
SIMPLEX_HOME_CHANNEL=<contact-id>
|
||||
```
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `SIMPLEX_WS_URL` | Yes | WebSocket URL of the simplex-chat daemon |
|
||||
| `SIMPLEX_ALLOWED_USERS` | Recommended | Comma-separated allowlist. Each entry can be a numeric `contactId` **or** a display name — both forms work. |
|
||||
| `SIMPLEX_ALLOW_ALL_USERS` | Optional | Set `true` to allow every contact (use carefully) |
|
||||
| `SIMPLEX_AUTO_ACCEPT` | Optional | Auto-accept incoming contact requests (default: `true`) |
|
||||
| `SIMPLEX_GROUP_ALLOWED` | Optional | Comma-separated group IDs the bot participates in, or `*` for any group. Omit to ignore group messages entirely |
|
||||
| `SIMPLEX_HOME_CHANNEL` | Optional | Default contact/group ID for cron job delivery |
|
||||
| `SIMPLEX_HOME_CHANNEL_NAME` | Optional | Human label for the home channel |
|
||||
| `HERMES_SIMPLEX_TEXT_BATCH_DELAY` | Optional | Quiet-period seconds (default: `0.8`) used to concatenate rapid-fire inbound text messages into one event |
|
||||
|
||||
## Find your contact ID or display name
|
||||
|
||||
After starting the daemon, open a conversation with your agent contact. The numeric `contactId` appears in session logs or via `hermes send_message action=list`. If you'd rather use the display name shown in the SimpleX UI, that works too — `SIMPLEX_ALLOWED_USERS` accepts either form.
|
||||
|
||||
## Authorization
|
||||
|
||||
By default **all contacts are denied**. You must either:
|
||||
|
||||
1. Set `SIMPLEX_ALLOWED_USERS` to a comma-separated list of `contactId`s and/or display names (e.g. `SIMPLEX_ALLOWED_USERS=4,alice` matches either contactId 4 or the contact whose display name is "alice"), or
|
||||
2. Use **DM pairing** — send any message to the bot and it will reply with a pairing code. Enter that code via `hermes pairing approve simplex <CODE>`.
|
||||
|
||||
## Group chats
|
||||
|
||||
By default the adapter ignores group messages — a bot in a group otherwise
|
||||
processes every member's traffic. Opt-in explicitly:
|
||||
|
||||
```
|
||||
SIMPLEX_GROUP_ALLOWED=12,34 # specific group IDs
|
||||
# or
|
||||
SIMPLEX_GROUP_ALLOWED=* # any group the bot is in
|
||||
```
|
||||
|
||||
Address groups by prefixing the chat ID with `group:`, e.g.
|
||||
`simplex:group:12` in `send_message` or as a cron `deliver=` target.
|
||||
|
||||
## Attachments
|
||||
|
||||
The adapter supports native SimpleX attachments in both directions:
|
||||
|
||||
- **Inbound** — incoming images, voice notes, and files are accepted via
|
||||
the daemon's XFTP flow (`rcvFileDescrReady` → `/freceive` → wait for
|
||||
`rcvFileComplete`) and surfaced as `MessageEvent.media_urls` with the
|
||||
appropriate `MessageType` (`PHOTO`, `VOICE`, `TEXT` + document).
|
||||
- **Outbound** — `send_image_file`, `send_voice`, `send_document`, and
|
||||
`send_video` all use the structured `/_send` form with `filePath`, so
|
||||
the receiving SimpleX client renders images inline and plays voice
|
||||
notes inline rather than offering them as downloads.
|
||||
|
||||
Agent replies can also embed `MEDIA:/path/to/file` tags in plain text —
|
||||
the adapter strips the tag from the body and sends the file as either a
|
||||
voice note (audio extensions) or a document.
|
||||
|
||||
## Using SimpleX with cron jobs
|
||||
|
||||
```python
|
||||
cronjob(
|
||||
action="create",
|
||||
schedule="every 1h",
|
||||
deliver="simplex", # uses SIMPLEX_HOME_CHANNEL
|
||||
prompt="Check for alerts and summarise."
|
||||
)
|
||||
```
|
||||
|
||||
Or target a specific contact:
|
||||
|
||||
```python
|
||||
send_message(target="simplex:<contact-id>", message="Done!")
|
||||
```
|
||||
|
||||
## Privacy notes
|
||||
|
||||
- SimpleX never reveals phone numbers or email addresses — contacts use opaque IDs
|
||||
- The connection between Hermes and the daemon is local WebSocket (`ws://127.0.0.1:5225`) — no data leaves your machine
|
||||
- Messages are end-to-end encrypted by the SimpleX protocol before reaching the daemon
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Cannot reach daemon"** — Ensure `simplex-chat -p 5225` is running and the port matches `SIMPLEX_WS_URL`.
|
||||
|
||||
**"websockets not installed"** — Run `pip install websockets`.
|
||||
|
||||
**Messages not received** — Check that the contact's ID is in `SIMPLEX_ALLOWED_USERS` or approve them via DM pairing.
|
||||
@@ -0,0 +1,643 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: "Slack"
|
||||
description: "Set up Hermes Agent as a Slack bot using Socket Mode"
|
||||
---
|
||||
|
||||
# Slack Setup
|
||||
|
||||
Connect Hermes Agent to Slack as a bot using Socket Mode. Socket Mode uses WebSockets instead of
|
||||
public HTTP endpoints, so your Hermes instance doesn't need to be publicly accessible — it works
|
||||
behind firewalls, on your laptop, or on a private server.
|
||||
|
||||
:::warning Classic Slack Apps Deprecated
|
||||
Classic Slack apps (using RTM API) were **fully deprecated in March 2025**. Hermes uses the modern
|
||||
Bolt SDK with Socket Mode. If you have an old classic app, you must create a new one following
|
||||
the steps below.
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
| Component | Value |
|
||||
|-----------|-------|
|
||||
| **Library** | `slack-bolt` / `slack_sdk` for Python (Socket Mode) |
|
||||
| **Connection** | WebSocket — no public URL required |
|
||||
| **Auth tokens needed** | Bot Token (`xoxb-`) + App-Level Token (`xapp-`) |
|
||||
| **User identification** | Slack Member IDs (e.g., `U01ABC2DEF3`) |
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create a Slack App
|
||||
|
||||
The fastest path is to paste a manifest Hermes generates for you. It
|
||||
declares every built-in slash command (`/btw`, `/stop`, `/model`, …),
|
||||
every required OAuth scope, every event subscription, and enables Socket
|
||||
Mode — all at once.
|
||||
|
||||
### Option A: From a Hermes-generated manifest (recommended)
|
||||
|
||||
1. Generate the manifest:
|
||||
```bash
|
||||
hermes slack manifest --write
|
||||
```
|
||||
This writes `~/.hermes/slack-manifest.json` and prints paste-in
|
||||
instructions.
|
||||
2. Go to [https://api.slack.com/apps](https://api.slack.com/apps) →
|
||||
**Create New App** → **From an app manifest**
|
||||
3. Pick your workspace, paste the JSON contents, review, click **Next**
|
||||
→ **Create**
|
||||
4. Skip ahead to **Step 6: Install App to Workspace**. The manifest
|
||||
handled scopes, events, and slash commands for you.
|
||||
|
||||
### Option B: From scratch (manual)
|
||||
|
||||
1. Go to [https://api.slack.com/apps](https://api.slack.com/apps)
|
||||
2. Click **Create New App**
|
||||
3. Choose **From scratch**
|
||||
4. Enter an app name (e.g., "Hermes Agent") and select your workspace
|
||||
5. Click **Create App**
|
||||
|
||||
You'll land on the app's **Basic Information** page. Continue with
|
||||
Steps 2–6 below.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Configure Bot Token Scopes
|
||||
|
||||
Navigate to **Features → OAuth & Permissions** in the sidebar. Scroll to **Scopes → Bot Token Scopes** and add the following:
|
||||
|
||||
| Scope | Purpose |
|
||||
|-------|---------|
|
||||
| `chat:write` | Send messages as the bot |
|
||||
| `app_mentions:read` | Detect when @mentioned in channels |
|
||||
| `channels:history` | Read messages in public channels the bot is in |
|
||||
| `channels:read` | List and get info about public channels |
|
||||
| `groups:history` | Read messages in private channels the bot is invited to |
|
||||
| `im:history` | Read direct message history |
|
||||
| `im:read` | View basic DM info |
|
||||
| `im:write` | Open and manage DMs |
|
||||
| `users:read` | Look up user information |
|
||||
| `files:read` | Read and download attached files, including voice notes/audio |
|
||||
| `files:write` | Upload files (images, audio, documents) |
|
||||
|
||||
:::caution Missing scopes = missing features
|
||||
Without `channels:history` and `groups:history`, the bot **will not receive messages in channels** —
|
||||
it will only work in DMs. Without `files:read`, Hermes can chat but **cannot reliably read user-uploaded attachments**.
|
||||
These are the most commonly missed scopes.
|
||||
:::
|
||||
|
||||
**Optional scopes:**
|
||||
|
||||
| Scope | Purpose |
|
||||
|-------|---------|
|
||||
| `groups:read` | List and get info about private channels |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Enable Socket Mode
|
||||
|
||||
Socket Mode lets the bot connect via WebSocket instead of requiring a public URL.
|
||||
|
||||
1. In the sidebar, go to **Settings → Socket Mode**
|
||||
2. Toggle **Enable Socket Mode** to ON
|
||||
3. You'll be prompted to create an **App-Level Token**:
|
||||
- Name it something like `hermes-socket` (the name doesn't matter)
|
||||
- Add the **`connections:write`** scope
|
||||
- Click **Generate**
|
||||
4. **Copy the token** — it starts with `xapp-`. This is your `SLACK_APP_TOKEN`
|
||||
|
||||
:::tip
|
||||
You can always find or regenerate app-level tokens under **Settings → Basic Information → App-Level Tokens**.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Subscribe to Events
|
||||
|
||||
This step is critical — it controls what messages the bot can see.
|
||||
|
||||
|
||||
1. In the sidebar, go to **Features → Event Subscriptions**
|
||||
2. Toggle **Enable Events** to ON
|
||||
3. Expand **Subscribe to bot events** and add:
|
||||
|
||||
| Event | Required? | Purpose |
|
||||
|-------|-----------|---------|
|
||||
| `message.im` | **Yes** | Bot receives direct messages |
|
||||
| `message.channels` | **Yes** | Bot receives messages in **public** channels it's added to |
|
||||
| `message.groups` | **Recommended** | Bot receives messages in **private** channels it's invited to |
|
||||
| `app_mention` | **Yes** | Prevents Bolt SDK errors when bot is @mentioned |
|
||||
|
||||
4. Click **Save Changes** at the bottom of the page
|
||||
|
||||
:::danger Missing event subscriptions is the #1 setup issue
|
||||
If the bot works in DMs but **not in channels**, you almost certainly forgot to add
|
||||
`message.channels` (for public channels) and/or `message.groups` (for private channels).
|
||||
Without these events, Slack simply never delivers channel messages to the bot.
|
||||
:::
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Enable the Messages Tab
|
||||
|
||||
This step enables direct messages to the bot. Without it, users see **"Sending messages to this app has been turned off"** when trying to DM the bot.
|
||||
|
||||
1. In the sidebar, go to **Features → App Home**
|
||||
2. Scroll to **Show Tabs**
|
||||
3. Toggle **Messages Tab** to ON
|
||||
4. Check **"Allow users to send Slash commands and messages from the messages tab"**
|
||||
|
||||
:::danger Without this step, DMs are completely blocked
|
||||
Even with all the correct scopes and event subscriptions, Slack will not allow users to send direct messages to the bot unless the Messages Tab is enabled. This is a Slack platform requirement, not a Hermes configuration issue.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Install App to Workspace
|
||||
|
||||
1. In the sidebar, go to **Settings → Install App**
|
||||
2. Click **Install to Workspace**
|
||||
3. Review the permissions and click **Allow**
|
||||
4. After authorization, you'll see a **Bot User OAuth Token** starting with `xoxb-`
|
||||
5. **Copy this token** — this is your `SLACK_BOT_TOKEN`
|
||||
|
||||
:::tip
|
||||
If you change scopes or event subscriptions later, you **must reinstall the app** for the changes
|
||||
to take effect. The Install App page will show a banner prompting you to do so.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Find User IDs for the Allowlist
|
||||
|
||||
Hermes uses Slack **Member IDs** (not usernames or display names) for the allowlist.
|
||||
|
||||
To find a Member ID:
|
||||
|
||||
1. In Slack, click on the user's name or avatar
|
||||
2. Click **View full profile**
|
||||
3. Click the **⋮** (more) button
|
||||
4. Select **Copy member ID**
|
||||
|
||||
Member IDs look like `U01ABC2DEF3`. You need your own Member ID at minimum.
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Configure Hermes
|
||||
|
||||
Add the following to your `~/.hermes/.env` file:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
|
||||
SLACK_APP_TOKEN=xapp-your-app-token-here
|
||||
SLACK_ALLOWED_USERS=U01ABC2DEF3 # Comma-separated Member IDs
|
||||
|
||||
# Optional
|
||||
SLACK_HOME_CHANNEL=C01234567890 # Default channel for cron/scheduled messages
|
||||
SLACK_HOME_CHANNEL_NAME=general # Human-readable name for the home channel (optional)
|
||||
```
|
||||
|
||||
Or run the interactive setup:
|
||||
|
||||
```bash
|
||||
hermes gateway setup # Select Slack when prompted
|
||||
```
|
||||
|
||||
Then start the gateway:
|
||||
|
||||
```bash
|
||||
hermes gateway # Foreground
|
||||
hermes gateway install # Install as a user service
|
||||
sudo hermes gateway install --system # Linux only: boot-time system service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Invite the Bot to Channels
|
||||
|
||||
After starting the gateway, you need to **invite the bot** to any channel where you want it to respond:
|
||||
|
||||
```
|
||||
/invite @Hermes Agent
|
||||
```
|
||||
|
||||
The bot will **not** automatically join channels. You must invite it to each channel individually.
|
||||
|
||||
---
|
||||
|
||||
## Slash Commands
|
||||
|
||||
Every Hermes command (`/btw`, `/stop`, `/new`, `/model`, `/help`, ...)
|
||||
is a native Slack slash command — exactly the way they work on Telegram
|
||||
and Discord. Type `/` in Slack and the autocomplete picker lists every
|
||||
Hermes command with its description.
|
||||
|
||||
Under the hood: Hermes ships with a generated Slack app manifest (see
|
||||
Step 1, Option A) that declares every command in
|
||||
[`COMMAND_REGISTRY`](https://github.com/NousResearch/hermes-agent/blob/main/hermes_cli/commands.py)
|
||||
as a slash command. In Socket Mode, Slack routes the command event
|
||||
through the WebSocket regardless of the manifest's `url` field.
|
||||
|
||||
### Refreshing slash commands after updates
|
||||
|
||||
When Hermes adds new commands (e.g. after `hermes update`), regenerate
|
||||
the manifest and update your Slack app:
|
||||
|
||||
```bash
|
||||
hermes slack manifest --write
|
||||
```
|
||||
|
||||
Then in Slack:
|
||||
1. Open [https://api.slack.com/apps](https://api.slack.com/apps) →
|
||||
your Hermes app
|
||||
2. **Features → App Manifest → Edit**
|
||||
3. Paste the new contents of `~/.hermes/slack-manifest.json`
|
||||
4. **Save**. Slack will prompt to reinstall the app if scopes or slash
|
||||
commands changed.
|
||||
|
||||
### Legacy `/hermes <subcommand>` still works
|
||||
|
||||
For backward compatibility with older manifests, you can still type
|
||||
`/hermes btw run the tests` — Hermes routes it the same way as `/btw
|
||||
run the tests`. Free-form questions also work: `/hermes what's the
|
||||
weather?` is treated as a regular message.
|
||||
|
||||
### Using commands inside threads (the `!cmd` prefix)
|
||||
|
||||
Slack itself blocks native slash commands inside thread replies — try
|
||||
`/queue` in a thread and Slack responds with *"/queue is not supported
|
||||
in threads. Sorry!"* There is no app-side setting that re-enables them;
|
||||
Slack never delivers them to Hermes.
|
||||
|
||||
As a workaround, Hermes recognises a leading `!` as an alternate
|
||||
command prefix that works in threads (and anywhere else). Type
|
||||
`!queue`, `!stop`, `!model gpt-5.4`, etc. as a regular thread reply —
|
||||
Hermes treats it identically to the slash form and replies in the same
|
||||
thread.
|
||||
|
||||
Only the first token is checked against the known command list, so
|
||||
casual messages like `!nice work` pass through to the agent unchanged.
|
||||
|
||||
Approval prompts (dangerous command / `execute_code` approval) normally
|
||||
render as interactive buttons. When buttons can't be delivered and
|
||||
Hermes falls back to a text prompt, the prompt instructs you to reply
|
||||
with `!approve` / `!deny` — the form that works inside threads.
|
||||
|
||||
### Advanced: emit only the slash-commands array
|
||||
|
||||
If you maintain your Slack manifest by hand and just want the slash
|
||||
command list:
|
||||
|
||||
```bash
|
||||
hermes slack manifest --slashes-only > /tmp/slashes.json
|
||||
```
|
||||
|
||||
Paste that array into the `features.slash_commands` key of your
|
||||
existing manifest.
|
||||
|
||||
---
|
||||
|
||||
## How the Bot Responds
|
||||
|
||||
Understanding how Hermes behaves in different contexts:
|
||||
|
||||
| Context | Behavior |
|
||||
|---------|----------|
|
||||
| **DMs** | Bot responds to every message — no @mention needed |
|
||||
| **Channels** | Bot **only responds when @mentioned** (e.g., `@Hermes Agent what time is it?`). In channels, Hermes replies in a thread attached to that message. |
|
||||
| **Threads** | If you @mention Hermes inside an existing thread, it replies in that same thread. Once the bot has an active session in a thread, **subsequent replies in that thread do not require @mention** — the bot follows the conversation naturally. |
|
||||
|
||||
:::tip
|
||||
In channels, always @mention the bot to start a conversation. Once the bot is active in a thread, you can reply in that thread without mentioning it. Outside of threads, messages without @mention are ignored to prevent noise in busy channels.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Beyond the required environment variables from Step 8, you can customize Slack bot behavior through `~/.hermes/config.yaml`.
|
||||
|
||||
### Thread & Reply Behavior
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
slack:
|
||||
# Controls how multi-part responses are threaded
|
||||
# "off" — never thread replies to the original message
|
||||
# "first" — first chunk threads to user's message (default)
|
||||
# "all" — all chunks thread to user's message
|
||||
reply_to_mode: "first"
|
||||
|
||||
extra:
|
||||
# Whether to reply in a thread (default: true).
|
||||
# When false, channel messages get direct channel replies instead
|
||||
# of threads. Messages inside existing threads still reply in-thread.
|
||||
reply_in_thread: true
|
||||
|
||||
# Also post thread replies to the main channel
|
||||
# (Slack's "Also send to channel" feature).
|
||||
# Only the first chunk of the first reply is broadcast.
|
||||
reply_broadcast: false
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `platforms.slack.reply_to_mode` | `"first"` | Threading mode for multi-part messages: `"off"`, `"first"`, or `"all"` |
|
||||
| `platforms.slack.extra.reply_in_thread` | `true` | When `false`, channel messages get direct replies instead of threads. Messages inside existing threads still reply in-thread. |
|
||||
| `platforms.slack.extra.reply_broadcast` | `false` | When `true`, thread replies are also posted to the main channel. Only the first chunk is broadcast. |
|
||||
|
||||
### Session Isolation
|
||||
|
||||
```yaml
|
||||
# Global setting — applies to Slack and all other platforms
|
||||
group_sessions_per_user: true
|
||||
```
|
||||
|
||||
When `true` (the default), each user in a shared channel gets their own isolated conversation session. Two people talking to Hermes in `#general` will have separate histories and contexts.
|
||||
|
||||
Set to `false` if you want a collaborative mode where the entire channel shares one conversation session. Be aware this means users share context growth and token costs, and one user's `/reset` clears the session for everyone.
|
||||
|
||||
### Mention & Trigger Behavior
|
||||
|
||||
```yaml
|
||||
slack:
|
||||
# Require @mention in channels (this is the default behavior;
|
||||
# the Slack adapter enforces @mention gating in channels regardless,
|
||||
# but you can set this explicitly for consistency with other platforms)
|
||||
require_mention: true
|
||||
|
||||
# Prevent thread auto-engagement: only reply to channel messages that
|
||||
# contain an explicit @mention. With this OFF (default), Slack can
|
||||
# "auto-engage" — remembering past mentions in a thread and following
|
||||
# up on bot-message replies, and resuming active sessions without a
|
||||
# fresh mention. With strict_mention ON, every new channel message
|
||||
# must @mention the bot before Hermes will respond.
|
||||
strict_mention: false
|
||||
|
||||
# Custom mention patterns that trigger the bot
|
||||
# (in addition to the default @mention detection)
|
||||
mention_patterns:
|
||||
- "hey hermes"
|
||||
- "hermes,"
|
||||
|
||||
# Text prepended to every outgoing message
|
||||
reply_prefix: ""
|
||||
```
|
||||
|
||||
:::tip When to use `strict_mention`
|
||||
Set this to `true` in busy workspaces where Slack's default "the bot remembers this thread" behavior surprises users — for example, a long tech-support thread where the bot helped at the start and you'd rather it stay silent unless explicitly pinged again. DMs and active interactive sessions are unaffected.
|
||||
:::
|
||||
|
||||
:::info
|
||||
Slack supports both patterns: `@mention` required to start a conversation by default, but you can opt specific channels out via `SLACK_FREE_RESPONSE_CHANNELS` (comma-separated channel IDs) or `slack.free_response_channels` in `config.yaml`. Once the bot has an active session in a thread, subsequent thread replies do not require a mention. In DMs the bot always responds without needing a mention.
|
||||
:::
|
||||
|
||||
### Channel allowlist (`allowed_channels`)
|
||||
|
||||
Restrict the bot to a fixed set of Slack channels — useful when the bot is invited to many channels but should only respond in a few. When set, messages from channels NOT in this list are **silently ignored**, even if the bot is `@mentioned`.
|
||||
|
||||
**DMs are exempt** from this filter, so authorized users can always reach the bot in a direct message.
|
||||
|
||||
```yaml
|
||||
slack:
|
||||
allowed_channels:
|
||||
- "C0123456789" # #ops
|
||||
- "C0987654321" # #incident-response
|
||||
```
|
||||
|
||||
Or via env var (comma-separated):
|
||||
|
||||
```bash
|
||||
SLACK_ALLOWED_CHANNELS="C0123456789,C0987654321"
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- Empty / unset → no restriction (fully backward compatible).
|
||||
- Non-empty → channel ID must be on the list, or the message is dropped before any other gating (mention requirement, `free_response_channels`, etc.) runs.
|
||||
- Slack channel IDs start with `C` (public), `G` (private), or `D` (DM). Look them up via the Slack UI's "Open channel details" → "About" panel, or via the API.
|
||||
|
||||
See also: [admin/user slash command split](../../reference/slash-commands.md#permissions-and-adminuser-split).
|
||||
|
||||
### Unauthorized User Handling
|
||||
|
||||
```yaml
|
||||
slack:
|
||||
# What happens when an unauthorized user (not in SLACK_ALLOWED_USERS) DMs the bot
|
||||
# "pair" — prompt them for a pairing code (default)
|
||||
# "ignore" — silently drop the message
|
||||
unauthorized_dm_behavior: "pair"
|
||||
```
|
||||
|
||||
You can also set this globally for all platforms:
|
||||
|
||||
```yaml
|
||||
unauthorized_dm_behavior: "pair"
|
||||
```
|
||||
|
||||
The platform-specific setting under `slack:` takes precedence over the global setting.
|
||||
|
||||
### Voice Transcription
|
||||
|
||||
```yaml
|
||||
# Global setting — enable/disable automatic transcription of incoming voice messages
|
||||
stt_enabled: true
|
||||
```
|
||||
|
||||
When `true` (the default), incoming audio messages are automatically transcribed using the configured STT provider before being processed by the agent.
|
||||
|
||||
### Full Example
|
||||
|
||||
```yaml
|
||||
# Global gateway settings
|
||||
group_sessions_per_user: true
|
||||
unauthorized_dm_behavior: "pair"
|
||||
stt_enabled: true
|
||||
|
||||
# Slack-specific settings
|
||||
slack:
|
||||
require_mention: true
|
||||
unauthorized_dm_behavior: "pair"
|
||||
|
||||
# Platform config
|
||||
platforms:
|
||||
slack:
|
||||
reply_to_mode: "first"
|
||||
extra:
|
||||
reply_in_thread: true
|
||||
reply_broadcast: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Home Channel
|
||||
|
||||
Set `SLACK_HOME_CHANNEL` to a channel ID where Hermes will deliver scheduled messages,
|
||||
cron job results, and other proactive notifications. To find a channel ID:
|
||||
|
||||
1. Right-click the channel name in Slack
|
||||
2. Click **View channel details**
|
||||
3. Scroll to the bottom — the Channel ID is shown there
|
||||
|
||||
```bash
|
||||
SLACK_HOME_CHANNEL=C01234567890
|
||||
```
|
||||
|
||||
Make sure the bot has been **invited to the channel** (`/invite @Hermes Agent`).
|
||||
|
||||
---
|
||||
|
||||
## Multi-Workspace Support
|
||||
|
||||
Hermes can connect to **multiple Slack workspaces** simultaneously using a single gateway instance. Each workspace is authenticated independently with its own bot user ID.
|
||||
|
||||
### Configuration
|
||||
|
||||
Provide multiple bot tokens as a **comma-separated list** in `SLACK_BOT_TOKEN`:
|
||||
|
||||
```bash
|
||||
# Multiple bot tokens — one per workspace
|
||||
SLACK_BOT_TOKEN=xoxb-workspace1-token,xoxb-workspace2-token,xoxb-workspace3-token
|
||||
|
||||
# A single app-level token is still used for Socket Mode
|
||||
SLACK_APP_TOKEN=xapp-your-app-token
|
||||
```
|
||||
|
||||
Or in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
slack:
|
||||
token: "xoxb-workspace1-token,xoxb-workspace2-token"
|
||||
```
|
||||
|
||||
### OAuth Token File
|
||||
|
||||
In addition to tokens in the environment or config, Hermes also loads tokens from an **OAuth token file** at:
|
||||
|
||||
```
|
||||
~/.hermes/slack_tokens.json
|
||||
```
|
||||
|
||||
This file is a JSON object mapping team IDs to token entries:
|
||||
|
||||
```json
|
||||
{
|
||||
"T01ABC2DEF3": {
|
||||
"token": "xoxb-workspace-token-here",
|
||||
"team_name": "My Workspace"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Tokens from this file are merged with any tokens specified via `SLACK_BOT_TOKEN`. Duplicate tokens are automatically deduplicated.
|
||||
|
||||
### How it works
|
||||
|
||||
- The **first token** in the list is the primary token, used for the Socket Mode connection (AsyncApp).
|
||||
- Each token is authenticated via `auth.test` on startup. The gateway maps each `team_id` to its own `WebClient` and `bot_user_id`.
|
||||
- When a message arrives, Hermes uses the correct workspace-specific client to respond.
|
||||
- The primary `bot_user_id` (from the first token) is used for backward compatibility with features that expect a single bot identity.
|
||||
|
||||
---
|
||||
|
||||
## Voice Messages
|
||||
|
||||
Hermes supports voice on Slack:
|
||||
|
||||
- **Incoming:** Voice/audio messages are automatically transcribed using the configured STT provider: local `faster-whisper`, Groq Whisper (`GROQ_API_KEY`), or OpenAI Whisper (`VOICE_TOOLS_OPENAI_KEY`)
|
||||
- **Outgoing:** TTS responses are sent as audio file attachments
|
||||
|
||||
---
|
||||
|
||||
## Per-Channel Prompts
|
||||
|
||||
Assign ephemeral system prompts to specific Slack channels. The prompt is injected at runtime on every turn — never persisted to transcript history — so changes take effect immediately.
|
||||
|
||||
```yaml
|
||||
slack:
|
||||
channel_prompts:
|
||||
"C01RESEARCH": |
|
||||
You are a research assistant. Focus on academic sources,
|
||||
citations, and concise synthesis.
|
||||
"C02ENGINEERING": |
|
||||
Code review mode. Be precise about edge cases and
|
||||
performance implications.
|
||||
```
|
||||
|
||||
Keys are Slack channel IDs (find them via channel details → "About" → scroll to bottom). All messages in the matching channel get the prompt injected as an ephemeral system instruction.
|
||||
|
||||
## Per-Channel Skill Bindings
|
||||
|
||||
Auto-load a skill whenever a new session starts in a specific channel or DM. Unlike per-channel prompts (which are injected on every turn), skill bindings inject the skill content as a user message at **session start** — it becomes part of the conversation history and does not need to be reloaded on subsequent turns.
|
||||
|
||||
This is ideal for DMs or channels with a dedicated purpose (flashcards, a domain-specific Q&A bot, a support triage channel, etc.) where you don't want the model's own skill selector to decide whether to load on every short reply.
|
||||
|
||||
```yaml
|
||||
slack:
|
||||
channel_skill_bindings:
|
||||
# DM channel — always runs in "german-flashcards" mode
|
||||
- id: "D0ATH9TQ0G6"
|
||||
skills:
|
||||
- german-flashcards
|
||||
# Research channel — preload multiple skills in order
|
||||
- id: "C01RESEARCH"
|
||||
skills:
|
||||
- arxiv
|
||||
- writing-plans
|
||||
# Short form: single skill as a string
|
||||
- id: "C02SUPPORT"
|
||||
skill: hubspot-on-demand
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The binding matches by channel ID. For threaded messages in a bound channel, the thread inherits the parent channel's binding.
|
||||
- The skill is loaded only at session start (new session or after auto-reset). If you change the binding, run `/new` or wait for the session to auto-reset for it to take effect.
|
||||
- Combine with `channel_prompts` for per-channel tone/constraints on top of the skill's instructions.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Bot doesn't respond to DMs | Verify `message.im` is in your event subscriptions and the app is reinstalled |
|
||||
| Bot works in DMs but not in channels | **Most common issue.** Add `message.channels` and `message.groups` to event subscriptions, reinstall the app, and invite the bot to the channel with `/invite @Hermes Agent` |
|
||||
| Bot doesn't respond to @mentions in channels | 1) Check `message.channels` event is subscribed. 2) Bot must be invited to the channel. 3) Ensure `channels:history` scope is added. 4) Reinstall the app after scope/event changes |
|
||||
| Bot ignores messages in private channels | Add both the `message.groups` event subscription and `groups:history` scope, then reinstall the app and `/invite` the bot |
|
||||
| "Sending messages to this app has been turned off" in DMs | Enable the **Messages Tab** in App Home settings (see Step 5) |
|
||||
| "not_authed" or "invalid_auth" errors | Regenerate your Bot Token and App Token, update `.env` |
|
||||
| Bot responds but can't post in a channel | Invite the bot to the channel with `/invite @Hermes Agent` |
|
||||
| Bot can chat but can't read uploaded images/files | Add `files:read`, then **reinstall** the app. Hermes now surfaces attachment access diagnostics in-chat when Slack returns scope/auth/permission failures. |
|
||||
| `missing_scope` error | Add the required scope in OAuth & Permissions, then **reinstall** the app |
|
||||
| Socket disconnects frequently | Check your network; Bolt auto-reconnects but unstable connections cause lag |
|
||||
| Changed scopes/events but nothing changed | You **must reinstall** the app to your workspace after any scope or event subscription change |
|
||||
|
||||
### Quick Checklist
|
||||
|
||||
If the bot isn't working in channels, verify **all** of the following:
|
||||
|
||||
1. ✅ `message.channels` event is subscribed (for public channels)
|
||||
2. ✅ `message.groups` event is subscribed (for private channels)
|
||||
3. ✅ `app_mention` event is subscribed
|
||||
4. ✅ `channels:history` scope is added (for public channels)
|
||||
5. ✅ `groups:history` scope is added (for private channels)
|
||||
6. ✅ App was **reinstalled** after adding scopes/events
|
||||
7. ✅ Bot was **invited** to the channel (`/invite @Hermes Agent`)
|
||||
8. ✅ You are **@mentioning** the bot in your message
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
**Always set `SLACK_ALLOWED_USERS`** with the Member IDs of authorized users. Without this setting,
|
||||
the gateway will **deny all messages** by default as a safety measure. Never share your bot tokens —
|
||||
treat them like passwords.
|
||||
:::
|
||||
|
||||
- Tokens should be stored in `~/.hermes/.env` (file permissions `600`)
|
||||
- Rotate tokens periodically via the Slack app settings
|
||||
- Audit who has access to your Hermes config directory
|
||||
- Socket Mode means no public endpoint is exposed — one less attack surface
|
||||
@@ -0,0 +1,203 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
sidebar_label: "SMS (Twilio)"
|
||||
title: "SMS (Twilio)"
|
||||
description: "Set up Hermes Agent as an SMS chatbot via Twilio"
|
||||
---
|
||||
|
||||
# SMS Setup (Twilio)
|
||||
|
||||
Hermes connects to SMS through the [Twilio](https://www.twilio.com/) API. People text your Twilio phone number and get AI responses back — same conversational experience as Telegram or Discord, but over standard text messages.
|
||||
|
||||
:::info Shared Credentials
|
||||
The SMS gateway shares credentials with the optional [telephony skill](/reference/skills-catalog). If you've already set up Twilio for voice calls or one-off SMS, the gateway works with the same `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, and `TWILIO_PHONE_NUMBER`.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Twilio account** — [Sign up at twilio.com](https://www.twilio.com/try-twilio) (free trial available)
|
||||
- **A Twilio phone number** with SMS capability
|
||||
- **A publicly accessible server** — Twilio sends webhooks to your server when SMS arrives
|
||||
- **aiohttp** — `pip install 'hermes-agent[sms]'`
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Get Your Twilio Credentials
|
||||
|
||||
1. Go to the [Twilio Console](https://console.twilio.com/)
|
||||
2. Copy your **Account SID** and **Auth Token** from the dashboard
|
||||
3. Go to **Phone Numbers → Manage → Active Numbers** — note your phone number in E.164 format (e.g., `+15551234567`)
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Configure Hermes
|
||||
|
||||
### Interactive setup (recommended)
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **SMS (Twilio)** from the platform list. The wizard will prompt for your credentials.
|
||||
|
||||
### Manual setup
|
||||
|
||||
Add to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
TWILIO_AUTH_TOKEN=your_auth_token_here
|
||||
TWILIO_PHONE_NUMBER=+15551234567
|
||||
|
||||
# Security: restrict to specific phone numbers (recommended)
|
||||
SMS_ALLOWED_USERS=+15559876543,+15551112222
|
||||
|
||||
# Optional: set a home channel for cron job delivery
|
||||
SMS_HOME_CHANNEL=+15559876543
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Configure Twilio Webhook
|
||||
|
||||
Twilio needs to know where to send incoming messages. In the [Twilio Console](https://console.twilio.com/):
|
||||
|
||||
1. Go to **Phone Numbers → Manage → Active Numbers**
|
||||
2. Click your phone number
|
||||
3. Under **Messaging → A MESSAGE COMES IN**, set:
|
||||
- **Webhook**: `https://your-server:8080/webhooks/twilio`
|
||||
- **HTTP Method**: `POST`
|
||||
|
||||
:::tip Exposing Your Webhook
|
||||
If you're running Hermes locally, use a tunnel to expose the webhook:
|
||||
|
||||
```bash
|
||||
# Using cloudflared
|
||||
cloudflared tunnel --url http://localhost:8080
|
||||
|
||||
# Using ngrok
|
||||
ngrok http 8080
|
||||
```
|
||||
|
||||
Set the resulting public URL as your Twilio webhook.
|
||||
:::
|
||||
|
||||
**Set `SMS_WEBHOOK_URL` to the same URL you configured in Twilio.** This is required for Twilio signature validation — the adapter will refuse to start without it:
|
||||
|
||||
```bash
|
||||
# Must match the webhook URL in your Twilio Console
|
||||
SMS_WEBHOOK_URL=https://your-server:8080/webhooks/twilio
|
||||
```
|
||||
|
||||
The webhook port defaults to `8080`. Override with:
|
||||
|
||||
```bash
|
||||
SMS_WEBHOOK_PORT=3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Start the Gateway
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
You should see:
|
||||
|
||||
```
|
||||
[sms] Twilio webhook server listening on 127.0.0.1:8080, from: +1555***4567
|
||||
```
|
||||
|
||||
If you see `Refusing to start: SMS_WEBHOOK_URL is required`, set `SMS_WEBHOOK_URL` to the public URL configured in your Twilio Console (see Step 3).
|
||||
|
||||
Text your Twilio number — Hermes will respond via SMS.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `TWILIO_ACCOUNT_SID` | Yes | Twilio Account SID (starts with `AC`) |
|
||||
| `TWILIO_AUTH_TOKEN` | Yes | Twilio Auth Token (also used for webhook signature validation) |
|
||||
| `TWILIO_PHONE_NUMBER` | Yes | Your Twilio phone number (E.164 format) |
|
||||
| `SMS_WEBHOOK_URL` | Yes | Public URL for Twilio signature validation — must match the webhook URL in your Twilio Console |
|
||||
| `SMS_WEBHOOK_PORT` | No | Webhook listener port (default: `8080`) |
|
||||
| `SMS_WEBHOOK_HOST` | No | Webhook bind address (default: `127.0.0.1`) |
|
||||
| `SMS_INSECURE_NO_SIGNATURE` | No | Set to `true` to disable signature validation (local dev only — **not for production**) |
|
||||
| `SMS_ALLOWED_USERS` | No | Comma-separated E.164 phone numbers allowed to chat |
|
||||
| `SMS_ALLOW_ALL_USERS` | No | Set to `true` to allow anyone (not recommended) |
|
||||
| `SMS_HOME_CHANNEL` | No | Phone number for cron job / notification delivery |
|
||||
| `SMS_HOME_CHANNEL_NAME` | No | Display name for the home channel (default: `Home`) |
|
||||
|
||||
---
|
||||
|
||||
## SMS-Specific Behavior
|
||||
|
||||
- **Plain text only** — Markdown is automatically stripped since SMS renders it as literal characters
|
||||
- **1600 character limit** — Longer responses are split across multiple messages at natural boundaries (newlines, then spaces)
|
||||
- **Echo prevention** — Messages from your own Twilio number are ignored to prevent loops
|
||||
- **Phone number redaction** — Phone numbers are redacted in logs for privacy
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### Webhook signature validation
|
||||
|
||||
Hermes validates that inbound webhooks genuinely originate from Twilio by verifying the `X-Twilio-Signature` header (HMAC-SHA1). This prevents attackers from injecting forged messages.
|
||||
|
||||
**`SMS_WEBHOOK_URL` is required.** Set it to the public URL configured in your Twilio Console. The adapter will refuse to start without it.
|
||||
|
||||
For local development without a public URL, you can disable validation:
|
||||
|
||||
```bash
|
||||
# Local dev only — NOT for production
|
||||
SMS_INSECURE_NO_SIGNATURE=true
|
||||
```
|
||||
|
||||
### User allowlists
|
||||
|
||||
**The gateway denies all users by default.** Configure an allowlist:
|
||||
|
||||
```bash
|
||||
# Recommended: restrict to specific phone numbers
|
||||
SMS_ALLOWED_USERS=+15559876543,+15551112222
|
||||
|
||||
# Or allow all (NOT recommended for bots with terminal access)
|
||||
SMS_ALLOW_ALL_USERS=true
|
||||
```
|
||||
|
||||
:::warning
|
||||
SMS has no built-in encryption. Don't use SMS for sensitive operations unless you understand the security implications. For sensitive use cases, prefer Signal or Telegram.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Messages not arriving
|
||||
|
||||
1. Check your Twilio webhook URL is correct and publicly accessible
|
||||
2. Verify `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` are correct
|
||||
3. Check the Twilio Console → **Monitor → Logs → Messaging** for delivery errors
|
||||
4. Ensure your phone number is in `SMS_ALLOWED_USERS` (or `SMS_ALLOW_ALL_USERS=true`)
|
||||
|
||||
### Replies not sending
|
||||
|
||||
1. Check `TWILIO_PHONE_NUMBER` is set correctly (E.164 format with `+`)
|
||||
2. Verify your Twilio account has SMS-capable numbers
|
||||
3. Check Hermes gateway logs for Twilio API errors
|
||||
|
||||
### Webhook port conflicts
|
||||
|
||||
If port 8080 is already in use, change it:
|
||||
|
||||
```bash
|
||||
SMS_WEBHOOK_PORT=3001
|
||||
```
|
||||
|
||||
Update the webhook URL in Twilio Console to match.
|
||||
@@ -0,0 +1,241 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
title: "Teams Meetings"
|
||||
description: "Set up the Microsoft Teams meeting summary pipeline with Microsoft Graph webhooks"
|
||||
---
|
||||
|
||||
# Microsoft Teams Meetings
|
||||
|
||||
Use the Teams meeting pipeline when you want Hermes to ingest Microsoft Graph meeting events, fetch transcripts first, fall back to recordings plus STT when needed, and deliver a structured summary to downstream sinks.
|
||||
|
||||
Prerequisites: see [Microsoft Teams](./teams.md) for the underlying bot/credential setup.
|
||||
|
||||
> Run `hermes gateway setup` and pick **Teams Meetings** for a guided walk-through.
|
||||
|
||||
This page focuses on setup and enablement:
|
||||
- Graph credentials
|
||||
- webhook listener configuration
|
||||
- Teams delivery modes
|
||||
- pipeline config shape
|
||||
|
||||
For day-2 operations, go-live checks, and the operator worksheet, use the dedicated guide: [Operate the Teams Meeting Pipeline](/guides/operate-teams-meeting-pipeline).
|
||||
|
||||
## What This Feature Does
|
||||
|
||||
The pipeline:
|
||||
1. receives Microsoft Graph webhook events
|
||||
2. resolves the meeting and prefers transcript artifacts first
|
||||
3. falls back to recording download plus STT when no usable transcript is available
|
||||
4. stores durable job state and sink records locally
|
||||
5. can write summaries to Notion, Linear, and Microsoft Teams
|
||||
|
||||
Operator actions stay in the CLI (the `teams-pipeline` subcommand is registered by the `teams_pipeline` plugin — enable it via `hermes plugins enable teams_pipeline` or set `plugins.enabled: [teams_pipeline]` in `config.yaml`):
|
||||
|
||||
```bash
|
||||
hermes teams-pipeline validate
|
||||
hermes teams-pipeline list
|
||||
hermes teams-pipeline maintain-subscriptions
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before enabling the meetings pipeline, make sure you have:
|
||||
|
||||
- a working Hermes install
|
||||
- the existing [Microsoft Teams bot setup](/user-guide/messaging/teams) if you want Teams outbound delivery
|
||||
- Microsoft Graph application credentials with the permissions required for the meeting resources you plan to subscribe to
|
||||
- a public HTTPS URL that Microsoft Graph can call for webhook delivery
|
||||
- `ffmpeg` installed if you want recording-plus-STT fallback
|
||||
|
||||
## Step 1: Add Microsoft Graph Credentials
|
||||
|
||||
Add Graph app-only credentials to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
MSGRAPH_TENANT_ID=<tenant-id>
|
||||
MSGRAPH_CLIENT_ID=<client-id>
|
||||
MSGRAPH_CLIENT_SECRET=<client-secret>
|
||||
```
|
||||
|
||||
These credentials are used by:
|
||||
- the Graph client foundation
|
||||
- subscription maintenance commands
|
||||
- meeting resolution and artifact fetches
|
||||
- Graph-based Teams outbound delivery when you do not provide a dedicated Teams access token
|
||||
|
||||
## Step 2: Enable the Graph Webhook Listener
|
||||
|
||||
The webhook listener is a gateway platform named `msgraph_webhook`. At minimum, enable it and set a client state value:
|
||||
|
||||
```bash
|
||||
MSGRAPH_WEBHOOK_ENABLED=true
|
||||
MSGRAPH_WEBHOOK_HOST=127.0.0.1
|
||||
MSGRAPH_WEBHOOK_PORT=8646
|
||||
MSGRAPH_WEBHOOK_CLIENT_STATE=<random-shared-secret>
|
||||
MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES=communications/onlineMeetings
|
||||
```
|
||||
|
||||
The listener exposes:
|
||||
- `/msgraph/webhook` for Graph notifications
|
||||
- `/health` for a simple health check
|
||||
|
||||
You need to route your public HTTPS endpoint to that listener. For example, if your public domain is `https://ops.example.com`, your Graph notification URL would typically be:
|
||||
|
||||
```text
|
||||
https://ops.example.com/msgraph/webhook
|
||||
```
|
||||
|
||||
## Step 3: Configure Teams Delivery and Pipeline Behavior
|
||||
|
||||
The meeting pipeline reads its runtime config from the existing `teams` platform entry. Pipeline-specific knobs live under `teams.extra.meeting_pipeline`. Teams outbound delivery stays on the normal Teams platform config surface.
|
||||
|
||||
Example `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
msgraph_webhook:
|
||||
enabled: true
|
||||
extra:
|
||||
host: 127.0.0.1
|
||||
port: 8646
|
||||
client_state: "replace-me"
|
||||
accepted_resources:
|
||||
- "communications/onlineMeetings"
|
||||
|
||||
teams:
|
||||
enabled: true
|
||||
extra:
|
||||
client_id: "your-teams-client-id"
|
||||
client_secret: "your-teams-client-secret"
|
||||
tenant_id: "your-teams-tenant-id"
|
||||
|
||||
# outbound summary delivery
|
||||
delivery_mode: "graph" # or incoming_webhook
|
||||
team_id: "team-id"
|
||||
channel_id: "channel-id"
|
||||
# incoming_webhook_url: "https://..."
|
||||
|
||||
meeting_pipeline:
|
||||
transcript_min_chars: 80
|
||||
transcript_required: false
|
||||
transcription_fallback: true
|
||||
ffmpeg_extract_audio: true
|
||||
notion:
|
||||
enabled: false
|
||||
linear:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
If you bind the listener to a non-loopback host such as `0.0.0.0`, you must also set `allowed_source_cidrs` to Microsoft's webhook egress ranges. Loopback binds (`127.0.0.1` / `::1`) are the intended dev-tunnel and local reverse-proxy setup.
|
||||
|
||||
## Teams Delivery Modes
|
||||
|
||||
The pipeline supports two Teams summary-delivery modes inside the existing Teams plugin.
|
||||
|
||||
### `incoming_webhook`
|
||||
|
||||
Use this when you want a simple webhook post into Teams without channel-message creation through Graph.
|
||||
|
||||
Required config:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
teams:
|
||||
enabled: true
|
||||
extra:
|
||||
delivery_mode: "incoming_webhook"
|
||||
incoming_webhook_url: "https://..."
|
||||
```
|
||||
|
||||
### `graph`
|
||||
|
||||
Use this when you want Hermes to post the summary through Microsoft Graph into a Teams chat or channel.
|
||||
|
||||
Supported targets:
|
||||
- `chat_id`
|
||||
- `team_id` + `channel_id`
|
||||
- `team_id` + `home_channel` fallback for the existing Teams platform
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
teams:
|
||||
enabled: true
|
||||
extra:
|
||||
delivery_mode: "graph"
|
||||
team_id: "team-id"
|
||||
channel_id: "channel-id"
|
||||
```
|
||||
|
||||
## Step 4: Start the Gateway
|
||||
|
||||
Start Hermes normally after updating config:
|
||||
|
||||
```bash
|
||||
hermes gateway run
|
||||
```
|
||||
|
||||
Or, if you run Hermes in Docker, start the gateway the same way you already do for your deployment.
|
||||
|
||||
Check the listener:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8646/health
|
||||
```
|
||||
|
||||
## Step 5: Create Graph Subscriptions
|
||||
|
||||
Use the plugin CLI to create and inspect subscriptions.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
hermes teams-pipeline subscribe \
|
||||
--resource communications/onlineMeetings/getAllTranscripts \
|
||||
--notification-url https://ops.example.com/msgraph/webhook \
|
||||
--client-state "$MSGRAPH_WEBHOOK_CLIENT_STATE"
|
||||
|
||||
hermes teams-pipeline subscribe \
|
||||
--resource communications/onlineMeetings/getAllRecordings \
|
||||
--notification-url https://ops.example.com/msgraph/webhook \
|
||||
--client-state "$MSGRAPH_WEBHOOK_CLIENT_STATE"
|
||||
```
|
||||
|
||||
:::warning Graph subscriptions expire in 72 hours
|
||||
|
||||
Microsoft Graph caps webhook subscriptions at 72 hours and will not auto-renew them. You MUST schedule `hermes teams-pipeline maintain-subscriptions` before going live, or notifications will silently stop three days after any manual subscription creation. See [Automating subscription renewal](/guides/operate-teams-meeting-pipeline#automating-subscription-renewal-required-for-production) in the operator runbook — three options (Hermes cron, systemd timer, plain crontab).
|
||||
|
||||
:::
|
||||
|
||||
For subscription maintenance and day-2 operator flows, continue with the guide: [Operate the Teams Meeting Pipeline](/guides/operate-teams-meeting-pipeline).
|
||||
|
||||
## Validation
|
||||
|
||||
Run the built-in validation snapshot:
|
||||
|
||||
```bash
|
||||
hermes teams-pipeline validate
|
||||
```
|
||||
|
||||
Useful companion checks:
|
||||
|
||||
```bash
|
||||
hermes teams-pipeline token-health
|
||||
hermes teams-pipeline subscriptions
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | What to check |
|
||||
|---------|---------------|
|
||||
| Graph webhook validation fails | Confirm the public URL is correct and reachable, and that Graph is calling the exact `/msgraph/webhook` path |
|
||||
| Jobs do not appear in `hermes teams-pipeline list` | Confirm `msgraph_webhook` is enabled and that subscriptions point at the right notification URL |
|
||||
| Transcript-first never succeeds | Check Graph permissions for transcript resources and whether the transcript artifact exists for that meeting |
|
||||
| Recording fallback fails | Confirm `ffmpeg` is installed and the Graph app can access recording artifacts |
|
||||
| Teams summary delivery fails | Re-check `delivery_mode`, target IDs, and Teams auth config |
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Microsoft Teams bot setup](/user-guide/messaging/teams)
|
||||
- [Operate the Teams Meeting Pipeline](/guides/operate-teams-meeting-pipeline)
|
||||
@@ -0,0 +1,254 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
title: "Microsoft Teams"
|
||||
description: "Set up Hermes Agent as a Microsoft Teams bot"
|
||||
---
|
||||
|
||||
# Microsoft Teams Setup
|
||||
|
||||
Connect Hermes Agent to Microsoft Teams as a bot. Unlike Slack's Socket Mode, Teams delivers messages by calling a **public HTTPS webhook**, so your instance needs a publicly reachable endpoint — either a dev tunnel (local dev) or a real domain (production).
|
||||
|
||||
Need meeting summaries from Microsoft Graph events rather than normal bot conversations? Use the dedicated setup page: [Teams Meetings](/user-guide/messaging/teams-meetings).
|
||||
|
||||
> Run `hermes gateway setup` and pick **Microsoft Teams** for a guided walk-through.
|
||||
|
||||
## How the Bot Responds
|
||||
|
||||
| Context | Behavior |
|
||||
|---------|----------|
|
||||
| **Personal chat (DM)** | Bot responds to every message. No @mention needed. |
|
||||
| **Group chat** | Bot only responds when @mentioned. |
|
||||
| **Channel** | Bot only responds when @mentioned. |
|
||||
|
||||
Teams delivers @mentions as regular messages with `<at>BotName</at>` tags, which Hermes strips automatically before processing.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Install the Teams CLI
|
||||
|
||||
The `@microsoft/teams.cli` automates bot registration — no Azure portal needed.
|
||||
|
||||
```bash
|
||||
npm install -g @microsoft/teams.cli@preview
|
||||
teams login
|
||||
```
|
||||
|
||||
To verify your login and find your own AAD object ID (needed for `TEAMS_ALLOWED_USERS`):
|
||||
|
||||
```bash
|
||||
teams status --verbose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Expose the Webhook Port
|
||||
|
||||
Teams cannot deliver messages to `localhost`. For local development, use any tunnel tool to get a public HTTPS URL. The default port is `3978` — change it with `TEAMS_PORT` if needed.
|
||||
|
||||
```bash
|
||||
# devtunnel (Microsoft)
|
||||
devtunnel create hermes-bot --allow-anonymous
|
||||
devtunnel port create hermes-bot -p 3978 --protocol https # replace 3978 with TEAMS_PORT if changed
|
||||
devtunnel host hermes-bot
|
||||
|
||||
# ngrok
|
||||
ngrok http 3978 # replace 3978 with TEAMS_PORT if changed
|
||||
|
||||
# cloudflared
|
||||
cloudflared tunnel --url http://localhost:3978 # replace 3978 with TEAMS_PORT if changed
|
||||
```
|
||||
|
||||
Copy the `https://` URL from the output — you'll use it in the next step. Leave the tunnel running while developing.
|
||||
|
||||
For production, point your bot's endpoint at your server's public domain instead (see [Production Deployment](#production-deployment)).
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create the Bot
|
||||
|
||||
```bash
|
||||
teams app create \
|
||||
--name "Hermes" \
|
||||
--endpoint "https://<your-tunnel-url>/api/messages"
|
||||
```
|
||||
|
||||
The CLI outputs your `CLIENT_ID`, `CLIENT_SECRET`, and `TENANT_ID`, plus an install link for Step 6. Save the client secret — it won't be shown again.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Configure Environment Variables
|
||||
|
||||
Add to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
TEAMS_CLIENT_ID=<your-client-id>
|
||||
TEAMS_CLIENT_SECRET=<your-client-secret>
|
||||
TEAMS_TENANT_ID=<your-tenant-id>
|
||||
|
||||
# Restrict access to specific users (recommended)
|
||||
# Use AAD object IDs from `teams status --verbose`
|
||||
TEAMS_ALLOWED_USERS=<your-aad-object-id>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Start the Gateway
|
||||
|
||||
```bash
|
||||
HERMES_UID=$(id -u) HERMES_GID=$(id -g) docker compose up -d gateway
|
||||
```
|
||||
|
||||
This starts the gateway. The default webhook port is `3978` (override with `TEAMS_PORT`). Check that it's running:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3978/health # should return: ok
|
||||
docker logs -f hermes
|
||||
```
|
||||
|
||||
Look for:
|
||||
```
|
||||
[teams] Webhook server listening on 0.0.0.0:3978/api/messages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Install the App in Teams
|
||||
|
||||
```bash
|
||||
teams app get <teamsAppId> --install-link
|
||||
```
|
||||
|
||||
Open the printed link in your browser — it opens directly in the Teams client. After installing, send a direct message to your bot — it's ready.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `TEAMS_CLIENT_ID` | Azure AD App (client) ID |
|
||||
| `TEAMS_CLIENT_SECRET` | Azure AD client secret |
|
||||
| `TEAMS_TENANT_ID` | Azure AD tenant ID |
|
||||
| `TEAMS_ALLOWED_USERS` | Comma-separated AAD object IDs allowed to use the bot |
|
||||
| `TEAMS_ALLOW_ALL_USERS` | Set `true` to skip the allowlist and allow anyone |
|
||||
| `TEAMS_HOME_CHANNEL` | Conversation ID for cron/proactive message delivery |
|
||||
| `TEAMS_HOME_CHANNEL_NAME` | Display name for the home channel |
|
||||
| `TEAMS_PORT` | Webhook port (default: `3978`) |
|
||||
|
||||
### config.yaml
|
||||
|
||||
Alternatively, configure via `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
teams:
|
||||
enabled: true
|
||||
extra:
|
||||
client_id: "your-client-id"
|
||||
client_secret: "your-secret"
|
||||
tenant_id: "your-tenant-id"
|
||||
port: 3978
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Interactive Approval Cards
|
||||
|
||||
When the agent needs to run a potentially dangerous command, it sends an Adaptive Card with four buttons instead of asking you to type `/approve`:
|
||||
|
||||
- **Allow Once** — approve this specific command
|
||||
- **Allow Session** — approve this pattern for the rest of the session
|
||||
- **Always Allow** — permanently approve this pattern
|
||||
- **Deny** — reject the command
|
||||
|
||||
Clicking a button resolves the approval inline and replaces the card with the decision.
|
||||
|
||||
### Meeting Summary Delivery (Teams Meeting Pipeline)
|
||||
|
||||
When the [Teams meeting pipeline plugin](/user-guide/messaging/msgraph-webhook) is enabled, this adapter also handles outbound delivery of meeting summaries — one Teams integration surface, not two. After a meeting's transcript is summarized, the writer posts the summary into your chosen Teams target.
|
||||
|
||||
Pipeline summary delivery is configured under the `teams` platform entry alongside the bot config:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
teams:
|
||||
enabled: true
|
||||
extra:
|
||||
# existing bot config (client_id, client_secret, tenant_id, port) ...
|
||||
|
||||
# Meeting summary delivery (only used when the teams_pipeline plugin is enabled)
|
||||
delivery_mode: "graph" # or "incoming_webhook"
|
||||
# For delivery_mode: graph — pick ONE of:
|
||||
chat_id: "19:meeting_..." # post into a Teams chat
|
||||
# team_id: "..." # OR post into a channel
|
||||
# channel_id: "..."
|
||||
# access_token: "..." # optional; falls back to MSGRAPH_* app credentials
|
||||
# For delivery_mode: incoming_webhook:
|
||||
# incoming_webhook_url: "https://outlook.office.com/webhook/..."
|
||||
```
|
||||
|
||||
| Mode | Use when | Trade-off |
|
||||
|------|----------|-----------|
|
||||
| `incoming_webhook` | Simple "post a summary into this channel" with a static Teams-generated URL. | No reply threading, no reactions, shows as the webhook's configured identity. |
|
||||
| `graph` | Threaded channel posts or 1:1/group chat posts under the bot's identity via Microsoft Graph. | Requires the [Graph app registration](/guides/microsoft-graph-app-registration) with `ChannelMessage.Send` (channel) or `Chat.ReadWrite.All` (chat) application permissions. |
|
||||
|
||||
If the `teams_pipeline` plugin is **not** enabled, these settings are inert — they only wire up when the pipeline runtime binds to the Graph webhook ingress.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment
|
||||
|
||||
For a permanent server, skip devtunnel and register your bot with your server's public HTTPS endpoint:
|
||||
|
||||
```bash
|
||||
teams app create \
|
||||
--name "Hermes" \
|
||||
--endpoint "https://your-domain.com/api/messages"
|
||||
```
|
||||
|
||||
If you've already created the bot and just need to update the endpoint:
|
||||
|
||||
```bash
|
||||
teams app update --id <teamsAppId> --endpoint "https://your-domain.com/api/messages"
|
||||
```
|
||||
|
||||
Make sure your configured port (`TEAMS_PORT`, default `3978`) is reachable from the internet and that your TLS certificate is valid — Teams rejects self-signed certificates.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| `health` endpoint works but bot doesn't respond | Check that your tunnel is still running and the bot's messaging endpoint matches the tunnel URL |
|
||||
| `KeyError: 'teams'` in logs | Restart the container — this is fixed in the current version |
|
||||
| Bot responds with auth errors | Verify `TEAMS_CLIENT_ID`, `TEAMS_CLIENT_SECRET`, and `TEAMS_TENANT_ID` are all set correctly |
|
||||
| `No inference provider configured` | Check that `ANTHROPIC_API_KEY` (or another provider key) is set in `~/.hermes/.env` |
|
||||
| Bot receives messages but ignores them | Your AAD object ID may not be in `TEAMS_ALLOWED_USERS`. Run `teams status --verbose` to find it |
|
||||
| Tunnel URL changes on restart | devtunnel URLs are persistent if you use a named tunnel (`devtunnel create hermes-bot`). ngrok and cloudflared generate a new URL each run unless you have a paid plan — update the bot endpoint with `teams app update` when it changes |
|
||||
| Teams shows "This bot is not responding" | The webhook returned an error. Check `docker logs hermes` for tracebacks |
|
||||
| `[teams] Failed to connect` in logs | The SDK failed to authenticate. Double-check your credentials and that the tenant ID matches the account you used in `teams login` |
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
**Always set `TEAMS_ALLOWED_USERS`** with the AAD object IDs of authorized users. Without this, anyone who can find or install your bot can interact with it.
|
||||
|
||||
Treat `TEAMS_CLIENT_SECRET` like a password — rotate it periodically via the Azure portal or Teams CLI.
|
||||
:::
|
||||
|
||||
- Store credentials in `~/.hermes/.env` with permissions `600` (`chmod 600 ~/.hermes/.env`)
|
||||
- The bot only accepts messages from users in `TEAMS_ALLOWED_USERS`; unauthorized messages are silently dropped
|
||||
- Your public endpoint (`/api/messages`) is authenticated by the Teams Bot Framework — requests without valid JWTs are rejected
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Teams Meetings](/user-guide/messaging/teams-meetings)
|
||||
- [Operate the Teams Meeting Pipeline](/guides/operate-teams-meeting-pipeline)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,484 @@
|
||||
---
|
||||
sidebar_position: 13
|
||||
title: "Webhooks"
|
||||
description: "Receive events from GitHub, GitLab, and other services to trigger Hermes agent runs"
|
||||
---
|
||||
|
||||
# Webhooks
|
||||
|
||||
Receive events from external services (GitHub, GitLab, JIRA, Stripe, etc.) and trigger Hermes agent runs automatically. The webhook adapter runs an HTTP server that accepts POST requests, validates HMAC signatures, transforms payloads into agent prompts, and routes responses back to the source or to another configured platform.
|
||||
|
||||
The agent processes the event and can respond by posting comments on PRs, sending messages to Telegram/Discord, or logging the result.
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
<div style={{position: 'relative', width: '100%', aspectRatio: '16 / 9', marginBottom: '1.5rem'}}>
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/WNYe5mD4fY8"
|
||||
title="Hermes Agent — Webhooks Tutorial"
|
||||
style={{position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', border: 0}}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Enable via `hermes gateway setup` or environment variables
|
||||
2. Define routes in `config.yaml` **or** create them dynamically with `hermes webhook subscribe`
|
||||
3. Point your service at `http://your-server:8644/webhooks/<route-name>`
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
There are two ways to enable the webhook adapter.
|
||||
|
||||
### Via setup wizard
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Follow the prompts to enable webhooks, set the port, and set a global HMAC secret.
|
||||
|
||||
### Via environment variables
|
||||
|
||||
Add to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
WEBHOOK_ENABLED=true
|
||||
WEBHOOK_PORT=8644 # default
|
||||
WEBHOOK_SECRET=your-global-secret
|
||||
```
|
||||
|
||||
### Verify the server
|
||||
|
||||
Once the gateway is running:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8644/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{"status": "ok", "platform": "webhook"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuring Routes {#configuring-routes}
|
||||
|
||||
Routes define how different webhook sources are handled. Each route is a named entry under `platforms.webhook.extra.routes` in your `config.yaml`.
|
||||
|
||||
### Route properties
|
||||
|
||||
| Property | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `events` | No | List of event types to accept (e.g. `["pull_request"]`). If empty, all events are accepted. Event type is read from `X-GitHub-Event`, `X-GitLab-Event`, or `event_type` in the payload. |
|
||||
| `secret` | **Yes** | HMAC secret for signature validation. Falls back to the global `secret` if not set on the route. Set to `"INSECURE_NO_AUTH"` for testing only (skips validation). |
|
||||
| `prompt` | No | Template string with dot-notation payload access (e.g. `{pull_request.title}`). If omitted, the full JSON payload is dumped into the prompt. |
|
||||
| `skills` | No | List of skill names to load for the agent run. |
|
||||
| `deliver` | No | Where to send the response: `github_comment`, `telegram`, `discord`, `slack`, `signal`, `sms`, `whatsapp`, `matrix`, `mattermost`, `homeassistant`, `email`, `dingtalk`, `feishu`, `wecom`, `weixin`, `bluebubbles`, `qqbot`, or `log` (default). |
|
||||
| `deliver_extra` | No | Additional delivery config — keys depend on `deliver` type (e.g. `repo`, `pr_number`, `chat_id`). Values support the same `{dot.notation}` templates as `prompt`. |
|
||||
| `deliver_only` | No | If `true`, skip the agent entirely — the rendered `prompt` template becomes the literal message that gets delivered. Zero LLM cost, sub-second delivery. See [Direct Delivery Mode](#direct-delivery-mode) for use cases. Requires `deliver` to be a real target (not `log`). |
|
||||
|
||||
### Full example
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
webhook:
|
||||
enabled: true
|
||||
extra:
|
||||
port: 8644
|
||||
secret: "global-fallback-secret"
|
||||
routes:
|
||||
github-pr:
|
||||
events: ["pull_request"]
|
||||
secret: "github-webhook-secret"
|
||||
prompt: |
|
||||
Review this pull request:
|
||||
Repository: {repository.full_name}
|
||||
PR #{number}: {pull_request.title}
|
||||
Author: {pull_request.user.login}
|
||||
URL: {pull_request.html_url}
|
||||
Diff URL: {pull_request.diff_url}
|
||||
Action: {action}
|
||||
skills: ["github-code-review"]
|
||||
deliver: "github_comment"
|
||||
deliver_extra:
|
||||
repo: "{repository.full_name}"
|
||||
pr_number: "{number}"
|
||||
deploy-notify:
|
||||
events: ["push"]
|
||||
secret: "deploy-secret"
|
||||
prompt: "New push to {repository.full_name} branch {ref}: {head_commit.message}"
|
||||
deliver: "telegram"
|
||||
```
|
||||
|
||||
### Prompt Templates
|
||||
|
||||
Prompts use dot-notation to access nested fields in the webhook payload:
|
||||
|
||||
- `{pull_request.title}` resolves to `payload["pull_request"]["title"]`
|
||||
- `{repository.full_name}` resolves to `payload["repository"]["full_name"]`
|
||||
- `{__raw__}` — special token that dumps the **entire payload** as indented JSON (truncated at 4000 characters). Useful for monitoring alerts or generic webhooks where the agent needs the full context.
|
||||
- Missing keys are left as the literal `{key}` string (no error)
|
||||
- Nested dicts and lists are JSON-serialized and truncated at 2000 characters
|
||||
|
||||
You can mix `{__raw__}` with regular template variables:
|
||||
|
||||
```yaml
|
||||
prompt: "PR #{pull_request.number} by {pull_request.user.login}: {__raw__}"
|
||||
```
|
||||
|
||||
If no `prompt` template is configured for a route, the entire payload is dumped as indented JSON (truncated at 4000 characters).
|
||||
|
||||
The same dot-notation templates work in `deliver_extra` values.
|
||||
|
||||
### Forum Topic Delivery
|
||||
|
||||
When delivering webhook responses to Telegram, you can target a specific forum topic by including `message_thread_id` (or `thread_id`) in `deliver_extra`:
|
||||
|
||||
```yaml
|
||||
webhooks:
|
||||
routes:
|
||||
alerts:
|
||||
events: ["alert"]
|
||||
prompt: "Alert: {__raw__}"
|
||||
deliver: "telegram"
|
||||
deliver_extra:
|
||||
chat_id: "-1001234567890"
|
||||
message_thread_id: "42"
|
||||
```
|
||||
|
||||
If `chat_id` is not provided in `deliver_extra`, the delivery falls back to the home channel configured for the target platform.
|
||||
|
||||
---
|
||||
|
||||
## GitHub PR Review (Step by Step) {#github-pr-review}
|
||||
|
||||
This walkthrough sets up automatic code review on every pull request.
|
||||
|
||||
### 1. Create the webhook in GitHub
|
||||
|
||||
1. Go to your repository → **Settings** → **Webhooks** → **Add webhook**
|
||||
2. Set **Payload URL** to `http://your-server:8644/webhooks/github-pr`
|
||||
3. Set **Content type** to `application/json`
|
||||
4. Set **Secret** to match your route config (e.g. `github-webhook-secret`)
|
||||
5. Under **Which events?**, select **Let me select individual events** and check **Pull requests**
|
||||
6. Click **Add webhook**
|
||||
|
||||
### 2. Add the route config
|
||||
|
||||
Add the `github-pr` route to your `~/.hermes/config.yaml` as shown in the example above.
|
||||
|
||||
### 3. Ensure `gh` CLI is authenticated
|
||||
|
||||
The `github_comment` delivery type uses the GitHub CLI to post comments:
|
||||
|
||||
```bash
|
||||
gh auth login
|
||||
```
|
||||
|
||||
### 4. Test it
|
||||
|
||||
Open a pull request on the repository. The webhook fires, Hermes processes the event, and posts a review comment on the PR.
|
||||
|
||||
---
|
||||
|
||||
## GitLab Webhook Setup {#gitlab-webhook-setup}
|
||||
|
||||
GitLab webhooks work similarly but use a different authentication mechanism. GitLab sends the secret as a plain `X-Gitlab-Token` header (exact string match, not HMAC).
|
||||
|
||||
### 1. Create the webhook in GitLab
|
||||
|
||||
1. Go to your project → **Settings** → **Webhooks**
|
||||
2. Set the **URL** to `http://your-server:8644/webhooks/gitlab-mr`
|
||||
3. Enter your **Secret token**
|
||||
4. Select **Merge request events** (and any other events you want)
|
||||
5. Click **Add webhook**
|
||||
|
||||
### 2. Add the route config
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
webhook:
|
||||
enabled: true
|
||||
extra:
|
||||
routes:
|
||||
gitlab-mr:
|
||||
events: ["merge_request"]
|
||||
secret: "your-gitlab-secret-token"
|
||||
prompt: |
|
||||
Review this merge request:
|
||||
Project: {project.path_with_namespace}
|
||||
MR !{object_attributes.iid}: {object_attributes.title}
|
||||
Author: {object_attributes.last_commit.author.name}
|
||||
URL: {object_attributes.url}
|
||||
Action: {object_attributes.action}
|
||||
deliver: "log"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Delivery Options {#delivery-options}
|
||||
|
||||
The `deliver` field controls where the agent's response goes after processing the webhook event.
|
||||
|
||||
| Deliver Type | Description |
|
||||
|-------------|-------------|
|
||||
| `log` | Logs the response to the gateway log output. This is the default and is useful for testing. |
|
||||
| `github_comment` | Posts the response as a PR/issue comment via the `gh` CLI. Requires `deliver_extra.repo` and `deliver_extra.pr_number`. The `gh` CLI must be installed and authenticated on the gateway host (`gh auth login`). |
|
||||
| `telegram` | Routes the response to Telegram. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `discord` | Routes the response to Discord. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `slack` | Routes the response to Slack. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `signal` | Routes the response to Signal. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `sms` | Routes the response to SMS via Twilio. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `whatsapp` | Routes the response to WhatsApp. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `matrix` | Routes the response to Matrix. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `mattermost` | Routes the response to Mattermost. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `homeassistant` | Routes the response to Home Assistant. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `email` | Routes the response to Email. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `dingtalk` | Routes the response to DingTalk. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `feishu` | Routes the response to Feishu/Lark. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `wecom` | Routes the response to WeCom. Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `weixin` | Routes the response to Weixin (WeChat). Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
| `bluebubbles` | Routes the response to BlueBubbles (iMessage). Uses the home channel, or specify `chat_id` in `deliver_extra`. |
|
||||
|
||||
For cross-platform delivery, the target platform must also be enabled and connected in the gateway. If no `chat_id` is provided in `deliver_extra`, the response is sent to that platform's configured home channel.
|
||||
|
||||
---
|
||||
|
||||
## Direct Delivery Mode {#direct-delivery-mode}
|
||||
|
||||
By default, every webhook POST triggers an agent run — the payload becomes a prompt, the agent processes it, and the agent's response is delivered. This costs LLM tokens on every event.
|
||||
|
||||
For use cases where you just want to **push a plain notification** — no reasoning, no agent loop, just deliver the message — set `deliver_only: true` on the route. The rendered `prompt` template becomes the literal message body, and the adapter dispatches it directly to the configured delivery target.
|
||||
|
||||
### When to use direct delivery
|
||||
|
||||
- **External service push** — Supabase/Firebase webhook fires on a database change → notify a user in Telegram instantly
|
||||
- **Monitoring alerts** — Datadog/Grafana alert webhook → push to a Discord channel
|
||||
- **Inter-agent pings** — Agent A notifies Agent B's user that a long-running task finished
|
||||
- **Background job completion** — Cron job finishes → post result to Slack
|
||||
|
||||
Benefits:
|
||||
|
||||
- **Zero LLM tokens** — the agent is never invoked
|
||||
- **Sub-second delivery** — a single adapter call, no reasoning loop
|
||||
- **Same security as agent mode** — HMAC auth, rate limits, idempotency, and body-size limits all still apply
|
||||
- **Synchronous response** — the POST returns `200 OK` once delivery succeeds, or `502` if the target rejects it, so your upstream service can retry intelligently
|
||||
|
||||
### Example: Telegram push from Supabase
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
webhook:
|
||||
enabled: true
|
||||
extra:
|
||||
port: 8644
|
||||
secret: "global-secret"
|
||||
routes:
|
||||
antenna-matches:
|
||||
secret: "antenna-webhook-secret"
|
||||
deliver: "telegram"
|
||||
deliver_only: true
|
||||
prompt: "🎉 New match: {match.user_name} matched with you!"
|
||||
deliver_extra:
|
||||
chat_id: "{match.telegram_chat_id}"
|
||||
```
|
||||
|
||||
Your Supabase edge function signs the payload with HMAC-SHA256 and POSTs to `https://your-server:8644/webhooks/antenna-matches`. The webhook adapter validates the signature, renders the template from the payload, delivers to Telegram, and returns `200 OK`.
|
||||
|
||||
### Example: Dynamic subscription via CLI
|
||||
|
||||
```bash
|
||||
hermes webhook subscribe antenna-matches \
|
||||
--deliver telegram \
|
||||
--deliver-chat-id "123456789" \
|
||||
--deliver-only \
|
||||
--prompt "🎉 New match: {match.user_name} matched with you!" \
|
||||
--description "Antenna match notifications"
|
||||
```
|
||||
|
||||
### Response codes
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| `200 OK` | Delivered successfully. Body: `{"status": "delivered", "route": "...", "target": "...", "delivery_id": "..."}` |
|
||||
| `200 OK` (status=duplicate) | Duplicate `X-GitHub-Delivery` ID within the idempotency TTL (1 hour). Not re-delivered. |
|
||||
| `401 Unauthorized` | HMAC signature invalid or missing. |
|
||||
| `400 Bad Request` | Malformed JSON body. |
|
||||
| `404 Not Found` | Unknown route name. |
|
||||
| `413 Payload Too Large` | Body exceeded `max_body_bytes`. |
|
||||
| `429 Too Many Requests` | Route rate limit exceeded. |
|
||||
| `502 Bad Gateway` | Target adapter rejected the message or raised. The error is logged server-side; the response body is a generic `Delivery failed` to avoid leaking adapter internals. |
|
||||
|
||||
### Configuration gotchas
|
||||
|
||||
- `deliver_only: true` requires `deliver` to be a real target. `deliver: log` (or omitting `deliver`) is rejected at startup — the adapter refuses to start if it finds a misconfigured route.
|
||||
- The `skills` field is ignored in direct delivery mode (no agent runs, so there's nothing to inject skills into).
|
||||
- Template rendering uses the same `{dot.notation}` syntax as agent mode, including the `{__raw__}` token.
|
||||
- Idempotency uses the same `X-GitHub-Delivery` / `X-Request-ID` header — retries with the same ID return `status=duplicate` and do NOT re-deliver.
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Subscriptions (CLI) {#dynamic-subscriptions}
|
||||
|
||||
In addition to static routes in `config.yaml`, you can create webhook subscriptions dynamically using the `hermes webhook` CLI command. This is especially useful when the agent itself needs to set up event-driven triggers.
|
||||
|
||||
### Create a subscription
|
||||
|
||||
```bash
|
||||
hermes webhook subscribe github-issues \
|
||||
--events "issues" \
|
||||
--prompt "New issue #{issue.number}: {issue.title}\nBy: {issue.user.login}\n\n{issue.body}" \
|
||||
--deliver telegram \
|
||||
--deliver-chat-id "-100123456789" \
|
||||
--description "Triage new GitHub issues"
|
||||
```
|
||||
|
||||
This returns the webhook URL and an auto-generated HMAC secret. Configure your service to POST to that URL.
|
||||
|
||||
### List subscriptions
|
||||
|
||||
```bash
|
||||
hermes webhook list
|
||||
```
|
||||
|
||||
### Remove a subscription
|
||||
|
||||
```bash
|
||||
hermes webhook remove github-issues
|
||||
```
|
||||
|
||||
### Test a subscription
|
||||
|
||||
```bash
|
||||
hermes webhook test github-issues
|
||||
hermes webhook test github-issues --payload '{"issue": {"number": 42, "title": "Test"}}'
|
||||
```
|
||||
|
||||
### How dynamic subscriptions work
|
||||
|
||||
- Subscriptions are stored in `~/.hermes/webhook_subscriptions.json`
|
||||
- The webhook adapter hot-reloads this file on each incoming request (mtime-gated, negligible overhead)
|
||||
- Static routes from `config.yaml` always take precedence over dynamic ones with the same name
|
||||
- Dynamic subscriptions use the same route format and capabilities as static routes (events, prompt templates, skills, delivery)
|
||||
- No gateway restart required — subscribe and it's immediately live
|
||||
|
||||
### Agent-driven subscriptions
|
||||
|
||||
The agent can create subscriptions via the terminal tool when guided by the `webhook-subscriptions` skill. Ask the agent to "set up a webhook for GitHub issues" and it will run the appropriate `hermes webhook subscribe` command.
|
||||
|
||||
---
|
||||
|
||||
## Security {#security}
|
||||
|
||||
The webhook adapter includes multiple layers of security:
|
||||
|
||||
### HMAC signature validation
|
||||
|
||||
The adapter validates incoming webhook signatures using the appropriate method for each source:
|
||||
|
||||
- **GitHub**: `X-Hub-Signature-256` header — HMAC-SHA256 hex digest prefixed with `sha256=`
|
||||
- **GitLab**: `X-Gitlab-Token` header — plain secret string match
|
||||
- **Generic**: `X-Webhook-Signature` header — raw HMAC-SHA256 hex digest
|
||||
|
||||
If a secret is configured but no recognized signature header is present, the request is rejected.
|
||||
|
||||
### Secret is required
|
||||
|
||||
Every route must have a secret — either set directly on the route or inherited from the global `secret`. Routes without a secret cause the adapter to fail at startup with an error. For development/testing only, you can set the secret to `"INSECURE_NO_AUTH"` to skip validation entirely.
|
||||
|
||||
`INSECURE_NO_AUTH` is only accepted when the gateway is bound to a loopback host (`127.0.0.1`, `localhost`, `::1`). If it is combined with a non-loopback bind such as `0.0.0.0` or a LAN IP, the adapter refuses to start — this prevents accidentally exposing an unauthenticated endpoint on a public interface.
|
||||
|
||||
### Rate limiting
|
||||
|
||||
Each route is rate-limited to **30 requests per minute** by default (fixed-window). Configure this globally:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
webhook:
|
||||
extra:
|
||||
rate_limit: 60 # requests per minute
|
||||
```
|
||||
|
||||
Requests exceeding the limit receive a `429 Too Many Requests` response.
|
||||
|
||||
### Idempotency
|
||||
|
||||
Delivery IDs (from `X-GitHub-Delivery`, `X-Request-ID`, or a timestamp fallback) are cached for **1 hour**. Duplicate deliveries (e.g. webhook retries) are silently skipped with a `200` response, preventing duplicate agent runs.
|
||||
|
||||
### Body size limits
|
||||
|
||||
Payloads exceeding **1 MB** are rejected before the body is read. Configure this:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
webhook:
|
||||
extra:
|
||||
max_body_bytes: 2097152 # 2 MB
|
||||
```
|
||||
|
||||
### Prompt injection risk
|
||||
|
||||
:::warning
|
||||
Webhook payloads contain attacker-controlled data — PR titles, commit messages, issue descriptions, etc. can all contain malicious instructions. Run the gateway in a sandboxed environment (Docker, VM) when exposed to the internet. Consider using the Docker or SSH terminal backend for isolation.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting {#troubleshooting}
|
||||
|
||||
### Webhook not arriving
|
||||
|
||||
- Verify the port is exposed and accessible from the webhook source
|
||||
- Check firewall rules — port `8644` (or your configured port) must be open
|
||||
- Verify the URL path matches: `http://your-server:8644/webhooks/<route-name>`
|
||||
- Use the `/health` endpoint to confirm the server is running
|
||||
|
||||
### Signature validation failing
|
||||
|
||||
- Ensure the secret in your route config exactly matches the secret configured in the webhook source
|
||||
- For GitHub, the secret is HMAC-based — check `X-Hub-Signature-256`
|
||||
- For GitLab, the secret is a plain token match — check `X-Gitlab-Token`
|
||||
- Check gateway logs for `Invalid signature` warnings
|
||||
|
||||
### Event being ignored
|
||||
|
||||
- Check that the event type is in your route's `events` list
|
||||
- GitHub events use values like `pull_request`, `push`, `issues` (the `X-GitHub-Event` header value)
|
||||
- GitLab events use values like `merge_request`, `push` (the `X-GitLab-Event` header value)
|
||||
- If `events` is empty or not set, all events are accepted
|
||||
|
||||
### Agent not responding
|
||||
|
||||
- Run the gateway in foreground to see logs: `hermes gateway run`
|
||||
- Check that the prompt template is rendering correctly
|
||||
- Verify the delivery target is configured and connected
|
||||
|
||||
### Duplicate responses
|
||||
|
||||
- The idempotency cache should prevent this — check that the webhook source is sending a delivery ID header (`X-GitHub-Delivery` or `X-Request-ID`)
|
||||
- Delivery IDs are cached for 1 hour
|
||||
|
||||
### `gh` CLI errors (GitHub comment delivery)
|
||||
|
||||
- Run `gh auth login` on the gateway host
|
||||
- Ensure the authenticated GitHub user has write access to the repository
|
||||
- Check that `gh` is installed and on the PATH
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables {#environment-variables}
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `WEBHOOK_ENABLED` | Enable the webhook platform adapter | `false` |
|
||||
| `WEBHOOK_PORT` | HTTP server port for receiving webhooks | `8644` |
|
||||
| `WEBHOOK_SECRET` | Global HMAC secret (used as fallback when routes don't specify their own) | _(none)_ |
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
sidebar_position: 15
|
||||
---
|
||||
|
||||
# WeCom Callback (Self-Built App)
|
||||
|
||||
Connect Hermes to WeCom (Enterprise WeChat) as a self-built enterprise application using the callback/webhook model.
|
||||
|
||||
:::info WeCom Bot vs WeCom Callback
|
||||
Hermes supports two WeCom integration modes:
|
||||
- **[WeCom Bot](wecom.md)** — bot-style, connects via WebSocket. Simpler setup, works in group chats.
|
||||
- **WeCom Callback** (this page) — self-built app, receives encrypted XML callbacks. Shows as a first-class app in users' WeCom sidebar. Supports multi-corp routing.
|
||||
:::
|
||||
|
||||
See also: [WeCom Bot](./wecom.md) for the bot-style integration.
|
||||
|
||||
> Run `hermes gateway setup` and pick **WeCom Callback** for a guided walk-through.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. You register a self-built application in the WeCom Admin Console
|
||||
2. WeCom pushes encrypted XML to your HTTP callback endpoint
|
||||
3. Hermes decrypts the message, queues it for the agent
|
||||
4. Immediately acknowledges (silent — nothing displayed to the user)
|
||||
5. The agent processes the request (typically 3–30 minutes)
|
||||
6. The reply is delivered proactively via the WeCom `message/send` API
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A WeCom enterprise account with admin access
|
||||
- `aiohttp` and `httpx` Python packages (included in the default install)
|
||||
- A publicly reachable server for the callback URL (or a tunnel like ngrok)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Create a Self-Built App in WeCom
|
||||
|
||||
1. Go to [WeCom Admin Console](https://work.weixin.qq.com/) → **Applications** → **Create App**
|
||||
2. Note your **Corp ID** (shown at the top of the admin console)
|
||||
3. In the app settings, create a **Corp Secret**
|
||||
4. Note the **Agent ID** from the app's overview page
|
||||
5. Under **Receive Messages**, configure the callback URL:
|
||||
- URL: `http://YOUR_PUBLIC_IP:8645/wecom/callback`
|
||||
- Token: Generate a random token (WeCom provides one)
|
||||
- EncodingAESKey: Generate a key (WeCom provides one)
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
Add to your `.env` file:
|
||||
|
||||
```bash
|
||||
WECOM_CALLBACK_CORP_ID=your-corp-id
|
||||
WECOM_CALLBACK_CORP_SECRET=your-corp-secret
|
||||
WECOM_CALLBACK_AGENT_ID=1000002
|
||||
WECOM_CALLBACK_TOKEN=your-callback-token
|
||||
WECOM_CALLBACK_ENCODING_AES_KEY=your-43-char-aes-key
|
||||
|
||||
# Optional
|
||||
WECOM_CALLBACK_HOST=0.0.0.0
|
||||
WECOM_CALLBACK_PORT=8645
|
||||
WECOM_CALLBACK_ALLOWED_USERS=user1,user2
|
||||
```
|
||||
|
||||
### 3. Start the Gateway
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
(Use `hermes gateway start` only after `hermes gateway install` has registered the systemd/launchd service.)
|
||||
|
||||
The callback adapter starts an HTTP server on the configured port. WeCom will verify the callback URL via a GET request, then begin sending messages via POST.
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
Set these in `config.yaml` under `platforms.wecom_callback.extra`, or use environment variables:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `corp_id` | — | WeCom enterprise Corp ID (required) |
|
||||
| `corp_secret` | — | Corp secret for the self-built app (required) |
|
||||
| `agent_id` | — | Agent ID of the self-built app (required) |
|
||||
| `token` | — | Callback verification token (required) |
|
||||
| `encoding_aes_key` | — | 43-character AES key for callback encryption (required) |
|
||||
| `host` | `0.0.0.0` | Bind address for the HTTP callback server |
|
||||
| `port` | `8645` | Port for the HTTP callback server |
|
||||
| `path` | `/wecom/callback` | URL path for the callback endpoint |
|
||||
|
||||
## Multi-App Routing
|
||||
|
||||
For enterprises running multiple self-built apps (e.g., across different departments or subsidiaries), configure the `apps` list in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
wecom_callback:
|
||||
enabled: true
|
||||
extra:
|
||||
host: "0.0.0.0"
|
||||
port: 8645
|
||||
apps:
|
||||
- name: "dept-a"
|
||||
corp_id: "ww_corp_a"
|
||||
corp_secret: "secret-a"
|
||||
agent_id: "1000002"
|
||||
token: "token-a"
|
||||
encoding_aes_key: "key-a-43-chars..."
|
||||
- name: "dept-b"
|
||||
corp_id: "ww_corp_b"
|
||||
corp_secret: "secret-b"
|
||||
agent_id: "1000003"
|
||||
token: "token-b"
|
||||
encoding_aes_key: "key-b-43-chars..."
|
||||
```
|
||||
|
||||
Users are scoped by `corp_id:user_id` to prevent cross-corp collisions. When a user sends a message, the adapter records which app (corp) they belong to and routes replies through the correct app's access token.
|
||||
|
||||
## Access Control
|
||||
|
||||
Restrict which users can interact with the app:
|
||||
|
||||
```bash
|
||||
# Allowlist specific users
|
||||
WECOM_CALLBACK_ALLOWED_USERS=zhangsan,lisi,wangwu
|
||||
|
||||
# Or allow all users
|
||||
WECOM_CALLBACK_ALLOW_ALL_USERS=true
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
The adapter exposes:
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| GET | `/wecom/callback` | URL verification handshake (WeCom sends this during setup) |
|
||||
| POST | `/wecom/callback` | Encrypted message callback (WeCom sends user messages here) |
|
||||
| GET | `/health` | Health check — returns `{"status": "ok"}` |
|
||||
|
||||
## Encryption
|
||||
|
||||
All callback payloads are encrypted with AES-CBC using the EncodingAESKey. The adapter handles:
|
||||
|
||||
- **Inbound**: Decrypt XML payload, verify SHA1 signature
|
||||
- **Outbound**: Replies sent via proactive API (not encrypted callback response)
|
||||
|
||||
The crypto implementation is compatible with Tencent's official WXBizMsgCrypt SDK.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No streaming** — replies arrive as complete messages after the agent finishes
|
||||
- **No typing indicators** — the callback model doesn't support typing status
|
||||
- **Text only** — currently supports text messages for input; image/file/voice input not yet implemented. The agent is aware of outbound media capabilities via the WeCom platform hint (images, documents, video, voice).
|
||||
- **Response latency** — agent sessions take 3–30 minutes; users see the reply when processing completes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Signature verification failing.**
|
||||
WeCom signs every request with the **Token** you registered in the admin
|
||||
console. A mismatch between the token configured in Hermes and the token the
|
||||
admin console expects is the most common cause. Re-copy both the **Token** and
|
||||
**EncodingAESKey** from the admin console — they're easy to truncate. Whitespace
|
||||
in `~/.hermes/.env` values around `=` will also break signature checks. After
|
||||
fixing, restart `hermes gateway run`.
|
||||
|
||||
**Callback URL not reachable / verification step fails.**
|
||||
WeCom hits the public URL you registered. Confirm:
|
||||
1. Your reverse proxy / tunnel forwards `/wecom/callback` to the gateway's port.
|
||||
2. The URL in the admin console is HTTPS (WeCom rejects plain HTTP).
|
||||
3. From outside your network, `curl -i https://<your-domain>/wecom/callback`
|
||||
returns something other than a timeout (a 4xx without query params is fine —
|
||||
it just means the listener is reachable).
|
||||
|
||||
**Port not reachable / listener not bound.**
|
||||
Check `hermes gateway run` logs for the bound host/port. If the adapter bound to
|
||||
`127.0.0.1` you must front it with a reverse proxy or tunnel — WeCom's servers
|
||||
can't reach loopback. Set `extra.host: 0.0.0.0` in `config.yaml` (plus
|
||||
`allowed_source_cidrs` if exposing directly) or keep loopback and use a tunnel
|
||||
such as Cloudflare Tunnel / nginx.
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
sidebar_position: 14
|
||||
title: "WeCom (Enterprise WeChat)"
|
||||
description: "Connect Hermes Agent to WeCom via the AI Bot WebSocket gateway"
|
||||
---
|
||||
|
||||
# WeCom (Enterprise WeChat)
|
||||
|
||||
Connect Hermes to [WeCom](https://work.weixin.qq.com/) (企业微信), Tencent's enterprise messaging platform. The adapter uses WeCom's AI Bot WebSocket gateway for real-time bidirectional communication — no public endpoint or webhook needed.
|
||||
|
||||
See also: [WeCom Callback](./wecom-callback.md) for inbound webhook setup.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A WeCom organization account
|
||||
- An AI Bot created in the WeCom Admin Console
|
||||
- The Bot ID and Secret from the bot's credentials page
|
||||
- Python packages: `aiohttp` and `httpx`
|
||||
|
||||
## Setup
|
||||
|
||||
### Step 1: Create an AI Bot
|
||||
|
||||
#### Recommended: Scan-to-Create (one command)
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **WeCom** and scan the QR code with your WeCom mobile app. Hermes will automatically create a bot application with the correct permissions and save the credentials.
|
||||
|
||||
The setup wizard will:
|
||||
1. Display a QR code in your terminal
|
||||
2. Wait for you to scan it with the WeCom mobile app
|
||||
3. Automatically retrieve the Bot ID and Secret
|
||||
4. Guide you through access control configuration
|
||||
|
||||
#### Alternative: Manual Setup
|
||||
|
||||
If scan-to-create is not available, the wizard falls back to manual input:
|
||||
|
||||
1. Log in to the [WeCom Admin Console](https://work.weixin.qq.com/wework_admin/frame)
|
||||
2. Navigate to **Applications** → **Create Application** → **AI Bot**
|
||||
3. Configure the bot name and description
|
||||
4. Copy the **Bot ID** and **Secret** from the credentials page
|
||||
5. Run `hermes gateway setup`, select **WeCom**, and enter the credentials when prompted
|
||||
|
||||
:::warning
|
||||
Keep the Bot Secret private. Anyone with it can impersonate your bot.
|
||||
:::
|
||||
|
||||
### Step 2: Configure Hermes
|
||||
|
||||
#### Option A: Interactive Setup (Recommended)
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **WeCom** and follow the prompts. The wizard will guide you through:
|
||||
- Bot credentials (via QR scan or manual entry)
|
||||
- Access control settings (allowlist, pairing mode, or open access)
|
||||
- Home channel for notifications
|
||||
|
||||
#### Option B: Manual Configuration
|
||||
|
||||
Add the following to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
WECOM_BOT_ID=your-bot-id
|
||||
WECOM_SECRET=your-secret
|
||||
|
||||
# Optional: restrict access
|
||||
WECOM_ALLOWED_USERS=user_id_1,user_id_2
|
||||
|
||||
# Optional: home channel for cron/notifications
|
||||
WECOM_HOME_CHANNEL=chat_id
|
||||
```
|
||||
|
||||
### Step 3: Start the gateway
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **WebSocket transport** — persistent connection, no public endpoint needed
|
||||
- **DM and group messaging** — configurable access policies
|
||||
- **Per-group sender allowlists** — fine-grained control over who can interact in each group
|
||||
- **Media support** — images, files, voice, video upload and download
|
||||
- **AES-encrypted media** — automatic decryption for inbound attachments
|
||||
- **Quote context** — preserves reply threading
|
||||
- **Markdown rendering** — rich text responses
|
||||
- **Reply correlation** — responses are correlated to the inbound message context
|
||||
- **Auto-reconnect** — exponential backoff on connection drops
|
||||
|
||||
:::note Streaming and typing indicators
|
||||
The WeCom adapter delivers each response as a single complete message — it does
|
||||
**not** stream responses token-by-token, and it does **not** show a typing
|
||||
indicator. "Reply correlation" (below) only threads a response to its inbound
|
||||
request; it is not live streaming.
|
||||
:::
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Set these in `config.yaml` under `platforms.wecom.extra`:
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `bot_id` | — | WeCom AI Bot ID (required) |
|
||||
| `secret` | — | WeCom AI Bot Secret (required) |
|
||||
| `websocket_url` | `wss://openws.work.weixin.qq.com` | WebSocket gateway URL |
|
||||
| `dm_policy` | `open` | DM access: `open`, `allowlist`, `disabled`, `pairing` |
|
||||
| `group_policy` | `open` | Group access: `open`, `allowlist`, `disabled` |
|
||||
| `allow_from` | `[]` | User IDs allowed for DMs (when dm_policy=allowlist) |
|
||||
| `group_allow_from` | `[]` | Group IDs allowed (when group_policy=allowlist) |
|
||||
| `groups` | `{}` | Per-group configuration (see below) |
|
||||
|
||||
## Access Policies
|
||||
|
||||
### DM Policy
|
||||
|
||||
Controls who can send direct messages to the bot:
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `open` | Anyone can DM the bot (default) |
|
||||
| `allowlist` | Only user IDs in `allow_from` can DM |
|
||||
| `disabled` | All DMs are ignored |
|
||||
| `pairing` | Pairing mode (for initial setup) |
|
||||
|
||||
```bash
|
||||
WECOM_DM_POLICY=allowlist
|
||||
```
|
||||
|
||||
### Group Policy
|
||||
|
||||
Controls which groups the bot responds in:
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `open` | Bot responds in all groups (default) |
|
||||
| `allowlist` | Bot only responds in group IDs listed in `group_allow_from` |
|
||||
| `disabled` | All group messages are ignored |
|
||||
|
||||
```bash
|
||||
WECOM_GROUP_POLICY=allowlist
|
||||
```
|
||||
|
||||
### Per-Group Sender Allowlists
|
||||
|
||||
For fine-grained control, you can restrict which users are allowed to interact with the bot within specific groups. This is configured in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
wecom:
|
||||
enabled: true
|
||||
extra:
|
||||
bot_id: "your-bot-id"
|
||||
secret: "your-secret"
|
||||
group_policy: "allowlist"
|
||||
group_allow_from:
|
||||
- "group_id_1"
|
||||
- "group_id_2"
|
||||
groups:
|
||||
group_id_1:
|
||||
allow_from:
|
||||
- "user_alice"
|
||||
- "user_bob"
|
||||
group_id_2:
|
||||
allow_from:
|
||||
- "user_charlie"
|
||||
"*":
|
||||
allow_from:
|
||||
- "user_admin"
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. The `group_policy` and `group_allow_from` controls determine whether a group is allowed at all.
|
||||
2. If a group passes the top-level check, the `groups.<group_id>.allow_from` list (if present) further restricts which senders within that group can interact with the bot.
|
||||
3. A wildcard `"*"` group entry serves as a default for groups not explicitly listed.
|
||||
4. Allowlist entries support the `*` wildcard to allow all users, and entries are case-insensitive.
|
||||
5. Entries can optionally use the `wecom:user:` or `wecom:group:` prefix format — the prefix is stripped automatically.
|
||||
|
||||
If no `allow_from` is configured for a group, all users in that group are allowed (assuming the group itself passes the top-level policy check).
|
||||
|
||||
## Media Support
|
||||
|
||||
### Inbound (receiving)
|
||||
|
||||
The adapter receives media attachments from users and caches them locally for agent processing:
|
||||
|
||||
| Type | How it's handled |
|
||||
|------|-----------------|
|
||||
| **Images** | Downloaded and cached locally. Supports both URL-based and base64-encoded images. |
|
||||
| **Files** | Downloaded and cached. Filename is preserved from the original message. |
|
||||
| **Voice** | Voice message text transcription is extracted if available. |
|
||||
| **Mixed messages** | WeCom mixed-type messages (text + images) are parsed and all components extracted. |
|
||||
|
||||
**Quoted messages:** Media from quoted (replied-to) messages is also extracted, so the agent has context about what the user is replying to.
|
||||
|
||||
### AES-Encrypted Media Decryption
|
||||
|
||||
WeCom encrypts some inbound media attachments with AES-256-CBC. The adapter handles this automatically:
|
||||
|
||||
- When an inbound media item includes an `aeskey` field, the adapter downloads the encrypted bytes and decrypts them using AES-256-CBC with PKCS#7 padding.
|
||||
- The AES key is the base64-decoded value of the `aeskey` field (must be exactly 32 bytes).
|
||||
- The IV is derived from the first 16 bytes of the key.
|
||||
- This requires the `cryptography` Python package (`pip install cryptography`).
|
||||
|
||||
No configuration is needed — decryption happens transparently when encrypted media is received.
|
||||
|
||||
### Outbound (sending)
|
||||
|
||||
| Method | What it sends | Size limit |
|
||||
|--------|--------------|------------|
|
||||
| `send` | Markdown text messages | 4000 chars |
|
||||
| `send_image` / `send_image_file` | Native image messages | 10 MB |
|
||||
| `send_document` | File attachments | 20 MB |
|
||||
| `send_voice` | Voice messages (AMR format only for native voice) | 2 MB |
|
||||
| `send_video` | Video messages | 10 MB |
|
||||
|
||||
**Chunked upload:** Files are uploaded in 512 KB chunks through a three-step protocol (init → chunks → finish). The adapter handles this automatically.
|
||||
|
||||
**Automatic downgrade:** When media exceeds the native type's size limit but is under the absolute 20 MB file limit, it is automatically sent as a generic file attachment instead:
|
||||
|
||||
- Images > 10 MB → sent as file
|
||||
- Videos > 10 MB → sent as file
|
||||
- Voice > 2 MB → sent as file
|
||||
- Non-AMR audio → sent as file (WeCom only supports AMR for native voice)
|
||||
|
||||
Files exceeding the absolute 20 MB limit are rejected with an informational message sent to the chat.
|
||||
|
||||
## Reply-Mode Responses
|
||||
|
||||
When the bot receives a message via the WeCom callback, the adapter remembers the inbound request ID. If a response is sent while the request context is still active, the adapter uses WeCom's reply-mode (`aibot_respond_msg`) to correlate the response directly to the inbound message. This provides a more natural conversation experience in the WeCom client.
|
||||
|
||||
The full response is delivered as a single message — the adapter does not stream tokens incrementally. If the inbound request context has expired or is unavailable, the adapter falls back to proactive message sending via `aibot_send_msg`.
|
||||
|
||||
Reply-mode also works for media: uploaded media can be sent as a reply to the originating message.
|
||||
|
||||
## Connection and Reconnection
|
||||
|
||||
The adapter maintains a persistent WebSocket connection to WeCom's gateway at `wss://openws.work.weixin.qq.com`.
|
||||
|
||||
### Connection Lifecycle
|
||||
|
||||
1. **Connect:** Opens a WebSocket connection and sends an `aibot_subscribe` authentication frame with the bot_id and secret.
|
||||
2. **Heartbeat:** Sends application-level ping frames every 30 seconds to keep the connection alive.
|
||||
3. **Listen:** Continuously reads inbound frames and dispatches message callbacks.
|
||||
|
||||
### Reconnection Behavior
|
||||
|
||||
On connection loss, the adapter uses exponential backoff to reconnect:
|
||||
|
||||
| Attempt | Delay |
|
||||
|---------|-------|
|
||||
| 1st retry | 2 seconds |
|
||||
| 2nd retry | 5 seconds |
|
||||
| 3rd retry | 10 seconds |
|
||||
| 4th retry | 30 seconds |
|
||||
| 5th+ retry | 60 seconds |
|
||||
|
||||
After each successful reconnection, the backoff counter resets to zero. All pending request futures are failed on disconnect so callers don't hang indefinitely.
|
||||
|
||||
### Deduplication
|
||||
|
||||
Inbound messages are deduplicated using message IDs with a 5-minute window and a maximum cache of 1000 entries. This prevents double-processing of messages during reconnection or network hiccups.
|
||||
|
||||
## All Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `WECOM_BOT_ID` | ✅ | — | WeCom AI Bot ID |
|
||||
| `WECOM_SECRET` | ✅ | — | WeCom AI Bot Secret |
|
||||
| `WECOM_ALLOWED_USERS` | — | _(empty)_ | Comma-separated user IDs for the gateway-level allowlist |
|
||||
| `WECOM_HOME_CHANNEL` | — | — | Chat ID for cron/notification output |
|
||||
| `WECOM_WEBSOCKET_URL` | — | `wss://openws.work.weixin.qq.com` | WebSocket gateway URL |
|
||||
| `WECOM_DM_POLICY` | — | `open` | DM access policy |
|
||||
| `WECOM_GROUP_POLICY` | — | `open` | Group access policy |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| `WECOM_BOT_ID and WECOM_SECRET are required` | Set both env vars or configure in setup wizard |
|
||||
| `WeCom startup failed: aiohttp not installed` | Install aiohttp: `pip install aiohttp` |
|
||||
| `WeCom startup failed: httpx not installed` | Install httpx: `pip install httpx` |
|
||||
| `invalid secret (errcode=40013)` | Verify the secret matches your bot's credentials |
|
||||
| `Timed out waiting for subscribe acknowledgement` | Check network connectivity to `openws.work.weixin.qq.com` |
|
||||
| Bot doesn't respond in groups | Check `group_policy` setting and ensure the group ID is in `group_allow_from` |
|
||||
| Bot ignores certain users in a group | Check per-group `allow_from` lists in the `groups` config section |
|
||||
| Media decryption fails | Install `cryptography`: `pip install cryptography` |
|
||||
| `cryptography is required for WeCom media decryption` | The inbound media is AES-encrypted. Install: `pip install cryptography` |
|
||||
| Voice messages sent as files | WeCom only supports AMR format for native voice. Other formats are auto-downgraded to file. |
|
||||
| `File too large` error | WeCom has a 20 MB absolute limit on all file uploads. Compress or split the file. |
|
||||
| Images sent as files | Images > 10 MB exceed the native image limit and are auto-downgraded to file attachments. |
|
||||
| `Timeout sending message to WeCom` | The WebSocket may have disconnected. Check logs for reconnection messages. |
|
||||
| `WeCom websocket closed during authentication` | Network issue or incorrect credentials. Verify bot_id and secret. |
|
||||
@@ -0,0 +1,333 @@
|
||||
---
|
||||
sidebar_position: 15
|
||||
title: "Weixin (WeChat)"
|
||||
description: "Connect Hermes Agent to personal WeChat accounts via the iLink Bot API"
|
||||
---
|
||||
|
||||
# Weixin (WeChat)
|
||||
|
||||
Connect Hermes to [WeChat](https://weixin.qq.com/) (微信), Tencent's personal messaging platform. The adapter uses Tencent's **iLink Bot API** for personal WeChat accounts — this is distinct from WeCom (Enterprise WeChat). Messages are delivered via long-polling, so no public endpoint or webhook is required.
|
||||
|
||||
:::info
|
||||
This adapter is for **personal WeChat accounts** (微信). If you need enterprise/corporate WeChat, see the [WeCom adapter](./wecom.md) instead.
|
||||
:::
|
||||
|
||||
:::warning iLink bot identity — ordinary WeChat groups may not work
|
||||
QR login connects Hermes to an **iLink bot identity** (e.g. `a5ace6fd482e@im.bot`), **not** a fully scriptable ordinary personal WeChat account. Consequences:
|
||||
|
||||
- The iLink bot identity generally **cannot be invited into ordinary WeChat groups** the way a normal contact can.
|
||||
- iLink typically **does not deliver ordinary WeChat group events** (including `@`-mentions of the personal account used for QR login) to the gateway for most bot-type accounts.
|
||||
- `@`-mentioning the personal WeChat account used to scan the QR code is **not** the same as `@`-mentioning the iLink bot — the bot is a separate identity.
|
||||
- The `WEIXIN_GROUP_POLICY` / `WEIXIN_GROUP_ALLOWED_USERS` settings below only take effect when iLink actually returns group events for your account type. If it doesn't, group messages will never reach Hermes regardless of policy.
|
||||
|
||||
In practice, most deployments only get DMs to the iLink bot working reliably. If group delivery doesn't work after configuration, the limitation is on the iLink side, not in Hermes. The gateway logs a `WARNING` at startup whenever `WEIXIN_GROUP_POLICY` is set to anything other than `disabled`.
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A personal WeChat account
|
||||
- Python packages: `aiohttp` and `cryptography`
|
||||
- Terminal QR rendering is included when Hermes is installed with the `messaging` extra
|
||||
|
||||
Install the required dependencies:
|
||||
|
||||
```bash
|
||||
pip install aiohttp cryptography
|
||||
# Optional: for terminal QR code display
|
||||
pip install hermes-agent[messaging]
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Run the Setup Wizard
|
||||
|
||||
The easiest way to connect your WeChat account is through the interactive setup:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **Weixin** when prompted. The wizard will:
|
||||
|
||||
1. Request a QR code from the iLink Bot API
|
||||
2. Display the QR code in your terminal (or provide a URL)
|
||||
3. Wait for you to scan the QR code with the WeChat mobile app
|
||||
4. Prompt you to confirm the login on your phone
|
||||
5. Save the account credentials automatically to `~/.hermes/weixin/accounts/`
|
||||
|
||||
Once confirmed, you'll see a message like:
|
||||
|
||||
```
|
||||
微信连接成功,account_id=your-account-id
|
||||
```
|
||||
|
||||
The wizard stores the `account_id`, `token`, and `base_url` so you don't need to configure them manually.
|
||||
|
||||
### 2. Configure Environment Variables
|
||||
|
||||
After initial QR login, set at minimum the account ID in `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
WEIXIN_ACCOUNT_ID=your-account-id
|
||||
|
||||
# Optional: override the token (normally auto-saved from QR login)
|
||||
# WEIXIN_TOKEN=your-bot-token
|
||||
|
||||
# Optional: restrict access
|
||||
WEIXIN_DM_POLICY=open
|
||||
WEIXIN_ALLOWED_USERS=user_id_1,user_id_2
|
||||
|
||||
# Optional: restore legacy multiline splitting behavior
|
||||
# WEIXIN_SPLIT_MULTILINE_MESSAGES=true
|
||||
|
||||
# Optional: home channel for cron/notifications
|
||||
WEIXIN_HOME_CHANNEL=chat_id
|
||||
WEIXIN_HOME_CHANNEL_NAME=Home
|
||||
```
|
||||
|
||||
### 3. Start the Gateway
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
The adapter will restore saved credentials, connect to the iLink API, and begin long-polling for messages.
|
||||
|
||||
## Features
|
||||
|
||||
- **Long-poll transport** — no public endpoint, webhook, or WebSocket needed
|
||||
- **QR code login** — scan-to-connect setup via `hermes gateway setup`
|
||||
- **DM messaging** — configurable access policies; group messaging depends on iLink actually delivering group events for the connected identity (often not the case for iLink bot accounts — see the warning above)
|
||||
- **Media support** — images, video, files, and voice messages
|
||||
- **AES-128-ECB encrypted CDN** — automatic encryption/decryption for all media transfers
|
||||
- **Context token persistence** — disk-backed reply continuity across restarts
|
||||
- **Markdown formatting** — preserves Markdown, including headers, tables, and code blocks, so WeChat clients that support Markdown can render it natively
|
||||
- **Smart message chunking** — messages stay as a single bubble when under the limit; only oversized payloads split at logical boundaries
|
||||
- **Typing indicators** — shows "typing…" status in the WeChat client while the agent processes
|
||||
- **SSRF protection** — outbound media URLs are validated before download
|
||||
- **Message deduplication** — 5-minute sliding window prevents double-processing
|
||||
- **Automatic retry with backoff** — recovers from transient API errors
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Set these in `config.yaml` under `platforms.weixin.extra`:
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `account_id` | — | iLink Bot account ID (required) |
|
||||
| `token` | — | iLink Bot token (required, auto-saved from QR login) |
|
||||
| `base_url` | `https://ilinkai.weixin.qq.com` | iLink API base URL |
|
||||
| `cdn_base_url` | `https://novac2c.cdn.weixin.qq.com/c2c` | CDN base URL for media transfer |
|
||||
| `dm_policy` | `open` | DM access: `open`, `allowlist`, `disabled`, `pairing` |
|
||||
| `group_policy` | `disabled` | Group access: `open`, `allowlist`, `disabled` |
|
||||
| `allow_from` | `[]` | User IDs allowed for DMs (when dm_policy=allowlist) |
|
||||
| `group_allow_from` | `[]` | Group IDs allowed (when group_policy=allowlist) |
|
||||
| `split_multiline_messages` | `false` | When `true`, split multi-line replies into multiple chat messages (legacy behavior). When `false`, keep multi-line replies as one message unless they exceed the length limit. |
|
||||
| `text_batch_delay_seconds` | `3.0` | Quiet period (seconds) before a buffered burst of rapid text messages is flushed as one combined request. iLink delivers messages individually, so this debounce avoids one agent invocation per fragment. Set `0` to dispatch each message immediately. |
|
||||
| `text_batch_split_delay_seconds` | `5.0` | Extended flush delay used when the latest fragment is near the split threshold (long messages iLink may have chunked). |
|
||||
|
||||
## Access Policies
|
||||
|
||||
### DM Policy
|
||||
|
||||
Controls who can send direct messages to the bot:
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `open` | Anyone can DM the bot (default) |
|
||||
| `allowlist` | Only user IDs in `allow_from` can DM |
|
||||
| `disabled` | All DMs are ignored |
|
||||
| `pairing` | Pairing mode (for initial setup) |
|
||||
|
||||
```bash
|
||||
WEIXIN_DM_POLICY=allowlist
|
||||
WEIXIN_ALLOWED_USERS=user_id_1,user_id_2
|
||||
```
|
||||
|
||||
`WEIXIN_ALLOWED_USERS` is an **inbound filter**, not an invitation system. QR
|
||||
login connects one iLink bot identity to Hermes. Other people do not scan the
|
||||
Hermes QR code with their own accounts; they must message the connected iLink
|
||||
bot/contact through WeChat, and Hermes will process the DM only if the sender's
|
||||
Weixin user ID is present in `WEIXIN_ALLOWED_USERS`.
|
||||
|
||||
A practical setup flow is:
|
||||
|
||||
1. Pair Hermes once with `hermes gateway setup` and note the connected iLink bot
|
||||
account.
|
||||
2. Have each allowed user send a direct message to that bot/contact.
|
||||
3. Read the sender/user ID from the gateway logs or the inbound event payload.
|
||||
4. Add those IDs to `WEIXIN_ALLOWED_USERS`, then restart the gateway.
|
||||
|
||||
If only the account that scanned the QR code can talk to Hermes, verify that the
|
||||
other users are messaging the iLink bot identity itself, not the personal WeChat
|
||||
account that performed the QR login. The iLink bot is a separate identity, and
|
||||
ordinary WeChat contact/group routing can be limited by Tencent's iLink behavior.
|
||||
|
||||
### Group Policy
|
||||
|
||||
Controls which groups the bot responds in **when iLink delivers group events for the connected identity**. For QR-login iLink bot identities (e.g. `...@im.bot`), group events are typically not delivered at all, so this policy may have no effect — see the iLink bot limitation warning at the top of the page.
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `open` | Bot responds in all groups (if events are delivered) |
|
||||
| `allowlist` | Bot only responds in group IDs listed in `group_allow_from` (if events are delivered) |
|
||||
| `disabled` | All group messages are ignored (default) |
|
||||
|
||||
```bash
|
||||
WEIXIN_GROUP_POLICY=allowlist
|
||||
# NOTE: this is a comma-separated list of group chat IDs, NOT member user IDs,
|
||||
# despite the variable name containing "USERS". Keep this in mind when configuring.
|
||||
WEIXIN_GROUP_ALLOWED_USERS=group_id_1,group_id_2
|
||||
```
|
||||
|
||||
:::note
|
||||
The default group policy is `disabled` for Weixin (unlike WeCom where it defaults to `open`). This is intentional — personal WeChat accounts may be in many groups, and iLink bot identities typically can't receive ordinary WeChat group messages at all. The gateway logs a `WARNING` at startup if you set `WEIXIN_GROUP_POLICY` to anything other than `disabled`.
|
||||
:::
|
||||
|
||||
## Media Support
|
||||
|
||||
### Inbound (receiving)
|
||||
|
||||
The adapter receives media attachments from users, downloads them from the WeChat CDN, decrypts them, and caches them locally for agent processing:
|
||||
|
||||
| Type | How it's handled |
|
||||
|------|-----------------|
|
||||
| **Images** | Downloaded, AES-decrypted, and cached as JPEG. |
|
||||
| **Video** | Downloaded, AES-decrypted, and cached as MP4. |
|
||||
| **Files** | Downloaded, AES-decrypted, and cached. Original filename is preserved. |
|
||||
| **Voice** | If a text transcription is available, it's extracted as text. Otherwise the audio (SILK format) is downloaded and cached. |
|
||||
|
||||
**Quoted messages:** Media from quoted (replied-to) messages is also extracted, so the agent has context about what the user is replying to.
|
||||
|
||||
### AES-128-ECB Encrypted CDN
|
||||
|
||||
WeChat media files are transferred through an encrypted CDN. The adapter handles this transparently:
|
||||
|
||||
- **Inbound:** Encrypted media is downloaded from the CDN using `encrypted_query_param` URLs, then decrypted with AES-128-ECB using the per-file key provided in the message payload.
|
||||
- **Outbound:** Files are encrypted locally with a random AES-128-ECB key, uploaded to the CDN, and the encrypted reference is included in the outbound message.
|
||||
- The AES key is 16 bytes (128-bit). Keys may arrive as raw base64 or hex-encoded — the adapter handles both formats.
|
||||
- This requires the `cryptography` Python package.
|
||||
|
||||
No configuration is needed — encryption and decryption happen automatically.
|
||||
|
||||
### Outbound (sending)
|
||||
|
||||
| Method | What it sends |
|
||||
|--------|--------------|
|
||||
| `send` | Text messages with Markdown formatting |
|
||||
| `send_image` / `send_image_file` | Native image messages (via CDN upload) |
|
||||
| `send_document` | File attachments (via CDN upload) |
|
||||
| `send_video` | Video messages (via CDN upload) |
|
||||
|
||||
All outbound media goes through the encrypted CDN upload flow:
|
||||
|
||||
1. Generate a random AES-128 key
|
||||
2. Encrypt the file with AES-128-ECB + PKCS#7 padding
|
||||
3. Request an upload URL from the iLink API (`getuploadurl`)
|
||||
4. Upload the ciphertext to the CDN
|
||||
5. Send the message with the encrypted media reference
|
||||
|
||||
## Context Token Persistence
|
||||
|
||||
The iLink Bot API requires a `context_token` to be echoed back with each outbound message for a given peer. The adapter maintains a disk-backed context token store:
|
||||
|
||||
- Tokens are saved per account+peer to `~/.hermes/weixin/accounts/<account_id>.context-tokens.json`
|
||||
- On startup, previously saved tokens are restored
|
||||
- Every inbound message updates the stored token for that sender
|
||||
- Outbound messages automatically include the latest context token
|
||||
|
||||
This ensures reply continuity even after gateway restarts.
|
||||
|
||||
## Markdown Formatting
|
||||
|
||||
WeChat clients connected through the iLink Bot API can render Markdown directly, so the adapter preserves Markdown instead of rewriting it:
|
||||
|
||||
- **Headers** stay as Markdown headings (`#`, `##`, ...)
|
||||
- **Tables** stay as Markdown tables
|
||||
- **Code fences** stay as fenced code blocks
|
||||
- **Excessive blank lines** are collapsed to double newlines outside fenced code blocks
|
||||
|
||||
## Message Chunking
|
||||
|
||||
Messages are delivered as a single chat message whenever they fit within the platform limit. Only oversized payloads are split for delivery:
|
||||
|
||||
- Maximum message length: **4000 characters**
|
||||
- Messages under the limit stay intact even when they contain multiple paragraphs or line breaks
|
||||
- Oversized messages split at logical boundaries (paragraphs, blank lines, code fences)
|
||||
- Code fences are kept intact whenever possible (never split mid-block unless the fence itself exceeds the limit)
|
||||
- Oversized individual blocks fall back to the base adapter's truncation logic
|
||||
- A 0.3 s inter-chunk delay prevents WeChat rate-limit drops when multiple chunks are sent
|
||||
|
||||
## Typing Indicators
|
||||
|
||||
The adapter shows typing status in the WeChat client:
|
||||
|
||||
1. When a message arrives, the adapter fetches a `typing_ticket` via the `getconfig` API
|
||||
2. Typing tickets are cached for 10 minutes per user
|
||||
3. `send_typing` sends a typing-start signal; `stop_typing` sends a typing-stop signal
|
||||
4. The gateway automatically triggers typing indicators while the agent processes a message
|
||||
|
||||
## Long-Poll Connection
|
||||
|
||||
The adapter uses HTTP long-polling (not WebSocket) to receive messages:
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Connect:** Validates credentials and starts the poll loop
|
||||
2. **Poll:** Calls `getupdates` with a 35-second timeout; the server holds the request until messages arrive or the timeout expires
|
||||
3. **Dispatch:** Inbound messages are dispatched concurrently via `asyncio.create_task`
|
||||
4. **Sync buffer:** A persistent sync cursor (`get_updates_buf`) is saved to disk so the adapter resumes from the correct position after restarts
|
||||
|
||||
### Retry Behavior
|
||||
|
||||
On API errors, the adapter uses a simple retry strategy:
|
||||
|
||||
| Condition | Behavior |
|
||||
|-----------|----------|
|
||||
| Transient error (1st–2nd) | Retry after 2 seconds |
|
||||
| Repeated errors (3+) | Back off for 30 seconds, then reset counter |
|
||||
| Session expired (`errcode=-14`) | Pause for 10 minutes (re-login may be needed) |
|
||||
| Timeout | Immediately re-poll (normal long-poll behavior) |
|
||||
|
||||
### Deduplication
|
||||
|
||||
Inbound messages are deduplicated using message IDs with a 5-minute window. This prevents double-processing during network hiccups or overlapping poll responses.
|
||||
|
||||
### Token Lock
|
||||
|
||||
Only one Weixin gateway instance can use a given token at a time. The adapter acquires a scoped lock on startup and releases it on shutdown. If another gateway is already using the same token, startup fails with an informative error message.
|
||||
|
||||
## All Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `WEIXIN_ACCOUNT_ID` | ✅ | — | iLink Bot account ID (from QR login) |
|
||||
| `WEIXIN_TOKEN` | ✅ | — | iLink Bot token (auto-saved from QR login) |
|
||||
| `WEIXIN_BASE_URL` | — | `https://ilinkai.weixin.qq.com` | iLink API base URL |
|
||||
| `WEIXIN_CDN_BASE_URL` | — | `https://novac2c.cdn.weixin.qq.com/c2c` | CDN base URL for media transfer |
|
||||
| `WEIXIN_DM_POLICY` | — | `open` | DM access policy: `open`, `allowlist`, `disabled`, `pairing` |
|
||||
| `WEIXIN_GROUP_POLICY` | — | `disabled` | Group access policy: `open`, `allowlist`, `disabled` |
|
||||
| `WEIXIN_ALLOWED_USERS` | — | _(empty)_ | Comma-separated user IDs for DM allowlist |
|
||||
| `WEIXIN_GROUP_ALLOWED_USERS` | — | _(empty)_ | Comma-separated **group chat IDs** (not member user IDs) for group allowlist. The variable name is legacy — it expects group IDs, not user IDs. |
|
||||
| `WEIXIN_HOME_CHANNEL` | — | — | Chat ID for cron/notification output |
|
||||
| `WEIXIN_HOME_CHANNEL_NAME` | — | `Home` | Display name for the home channel |
|
||||
| `WEIXIN_ALLOW_ALL_USERS` | — | — | Gateway-level flag to allow all users (used by setup wizard) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| `Weixin startup failed: aiohttp and cryptography are required` | Install both: `pip install aiohttp cryptography` |
|
||||
| `Weixin startup failed: WEIXIN_TOKEN is required` | Run `hermes gateway setup` to complete QR login, or set `WEIXIN_TOKEN` manually |
|
||||
| `Weixin startup failed: WEIXIN_ACCOUNT_ID is required` | Set `WEIXIN_ACCOUNT_ID` in your `.env` or run `hermes gateway setup` |
|
||||
| `Another local Hermes gateway is already using this Weixin token` | Stop the other gateway instance first — only one poller per token is allowed |
|
||||
| Session expired (`errcode=-14`) | Your login session has expired. Re-run `hermes gateway setup` to scan a new QR code |
|
||||
| QR code expired during setup | The QR auto-refreshes up to 3 times. If it keeps expiring, check your network connection |
|
||||
| Bot doesn't respond to DMs | Check `WEIXIN_DM_POLICY` — if set to `allowlist`, the sender must be in `WEIXIN_ALLOWED_USERS` |
|
||||
| Bot ignores group messages | Group policy defaults to `disabled`. Set `WEIXIN_GROUP_POLICY=open` or `allowlist` — but note that QR-login iLink bot identities (`...@im.bot`) typically cannot receive ordinary WeChat group messages at all. If the gateway logs show no raw inbound events for group messages, the limitation is on the iLink side, not in Hermes. |
|
||||
| Media download/upload fails | Ensure `cryptography` is installed. Check network access to `novac2c.cdn.weixin.qq.com` |
|
||||
| `Blocked unsafe URL (SSRF protection)` | The outbound media URL points to a private/internal address. Only public URLs are allowed |
|
||||
| Voice messages show as text | If WeChat provides a transcription, the adapter uses the text. This is expected behavior |
|
||||
| Messages appear duplicated | The adapter deduplicates by message ID. If you see duplicates, check if multiple gateway instances are running |
|
||||
| `iLink POST ... HTTP 4xx/5xx` | API error from the iLink service. Check your token validity and network connectivity |
|
||||
| Terminal QR code doesn't render | Reinstall with the messaging extra: `pip install hermes-agent[messaging]`. Alternatively, open the URL printed above the QR |
|
||||
@@ -0,0 +1,418 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
title: "WhatsApp Business (Cloud API)"
|
||||
description: "Set up Hermes Agent as a WhatsApp bot via Meta's official Business Cloud API"
|
||||
---
|
||||
|
||||
# WhatsApp Business Cloud API Setup
|
||||
|
||||
Hermes can connect to WhatsApp through Meta's **official** WhatsApp Business Cloud API. This is the production-grade path: no Node.js bridge subprocess, no QR codes, no account-ban risk.
|
||||
|
||||
In exchange:
|
||||
|
||||
- You need a **Meta Business account** (not personal WhatsApp).
|
||||
- The bot operates on a dedicated business phone number, not your personal number.
|
||||
- The Hermes gateway needs a **public HTTPS URL** so Meta can deliver inbound messages via webhook.
|
||||
- Replies more than 24 hours after the user's last message require a pre-approved **template** (this is Meta's "customer service window" rule, not a Hermes limit).
|
||||
|
||||
If those constraints don't work for your use case, the [Baileys bridge integration](./whatsapp.md) is the alternative — personal account, no public URL needed, but unofficial and ban-prone.
|
||||
|
||||
:::tip Which one should I use?
|
||||
- **Cloud API (this guide)** — running a real business bot, want stability, fine with the Meta verification + template paperwork
|
||||
- **[Baileys bridge](./whatsapp.md)** — personal projects, quick demos, single-user setups, willing to risk the bot phone number's account
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
hermes whatsapp-cloud
|
||||
```
|
||||
|
||||
The wizard walks you through every credential, validates each one as you paste it (catches the #1 setup trap — pasting a phone number into the Phone Number ID field), and prints exact follow-up instructions for the parts that need to happen outside the wizard (starting cloudflared, configuring Meta's webhook dashboard).
|
||||
|
||||
The rest of this page is the manual reference.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **A Meta Business account**. Create one at [business.facebook.com](https://business.facebook.com/).
|
||||
2. **A Meta app with WhatsApp enabled**. See "Creating the Meta app" below.
|
||||
3. **A way to expose a local port to the public internet** with HTTPS. Cloudflare Tunnel (`cloudflared`) is recommended — free, no port forwarding, no domain required. ngrok, your own domain with a reverse proxy + TLS, or a VPS with the gateway directly bound to a public IP all work too.
|
||||
4. **Optional but recommended**: ffmpeg on `PATH` so outbound voice messages render as native WhatsApp voice-note bubbles (green waveform) instead of MP3 audio attachments. Hermes degrades gracefully if absent.
|
||||
|
||||
---
|
||||
|
||||
## Creating the Meta app
|
||||
|
||||
1. Go to [developers.facebook.com/apps](https://developers.facebook.com/apps) → **Create App**.
|
||||
2. Choose use case: **"Connect with customers through WhatsApp"** → **Next**.
|
||||
3. Pick or create a business portfolio. Review the publishing requirements. Confirm → **Create app**.
|
||||
4. After creation you'll land on **Customize use case → Connect on WhatsApp → Quickstart**. Click **Start using the API** → you're now on the **API Setup** page.
|
||||
5. Make sure a WhatsApp Business Account (WABA) is linked. If you created a new portfolio in step 3, one was auto-created. Verify in the API Setup page.
|
||||
|
||||
You'll need these values from the dashboard — the wizard prompts for them in this order:
|
||||
|
||||
| Value | Where in dashboard | Field shape | Notes |
|
||||
|---|---|---|---|
|
||||
| **Phone Number ID** | App Dashboard → WhatsApp → API Setup → below the "From" dropdown | Numeric, 15-17 digits | **NOT** the phone number itself. The #1 setup mistake is pasting the actual phone number here. |
|
||||
| **Access Token** | App Dashboard → WhatsApp → API Setup → "Generate access token" | Starts with `EAA`, 100+ chars | Temp tokens last 24h — see "Permanent token" below for production. |
|
||||
| **App Secret** | App Dashboard → Settings → Basic → click "Show" next to App secret | 32-character lowercase hex | Used to verify incoming webhook signatures. Without it, inbound delivery is refused with 503. |
|
||||
| **App ID** (optional) | App Dashboard → Settings → Basic | Numeric, 15-16 digits | Not required for messaging, useful for analytics. |
|
||||
| **WABA ID** (optional) | App Dashboard → WhatsApp → API Setup → near the top | Numeric, 15+ digits | Not required for messaging, useful for analytics. |
|
||||
|
||||
---
|
||||
|
||||
## Permanent token (production)
|
||||
|
||||
Temporary access tokens expire after **24 hours**, which means a token generated today stops working tomorrow. For production deployments use a **System User permanent token**:
|
||||
|
||||
1. Go to [business.facebook.com/latest/settings](https://business.facebook.com/latest/settings) → **System users** (left sidebar).
|
||||
2. **Add** → name (e.g. `hermes-bot`) → role: **Admin**.
|
||||
3. Select the new user → **Assign Assets**:
|
||||
- Select your app → toggle **Manage app** under Full control.
|
||||
- Select your WhatsApp account → toggle **Manage WhatsApp Business Accounts** under Full control.
|
||||
- Click **Assign assets**.
|
||||
4. **Generate token** with these permissions:
|
||||
- `business_management`
|
||||
- `whatsapp_business_messaging`
|
||||
- `whatsapp_business_management`
|
||||
5. Set **token expiration: Never**.
|
||||
6. Copy the token → update `WHATSAPP_CLOUD_ACCESS_TOKEN` in `~/.hermes/.env` → restart the gateway.
|
||||
|
||||
System User tokens don't expire unless you explicitly revoke them.
|
||||
|
||||
---
|
||||
|
||||
## Exposing Hermes to the internet
|
||||
|
||||
The Cloud API delivers inbound messages by HTTPS POST to your webhook URL — that means the Hermes gateway has to be reachable from Meta's servers. Three common ways:
|
||||
|
||||
### Cloudflare Tunnel (recommended)
|
||||
|
||||
Free, no port forwarding, works on Windows / macOS / Linux. Runs as a separate process alongside the gateway.
|
||||
|
||||
**Install:**
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
winget install Cloudflare.cloudflared
|
||||
|
||||
# macOS
|
||||
brew install cloudflared
|
||||
|
||||
# Linux
|
||||
# Download the binary from https://github.com/cloudflare/cloudflared/releases
|
||||
```
|
||||
|
||||
**Run a quick tunnel** (no Cloudflare account needed — gives you a `https://<random>.trycloudflare.com` URL):
|
||||
|
||||
```bash
|
||||
cloudflared tunnel --url http://localhost:8090
|
||||
```
|
||||
|
||||
Note the printed URL — that's what you'll give Meta.
|
||||
|
||||
:::warning Quick tunnels rotate
|
||||
The free quick-tunnel URL changes every time you restart `cloudflared`. For a stable URL, log in with `cloudflared tunnel login` and create a named tunnel. Free Cloudflare accounts get unlimited named tunnels — see [Cloudflare's docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) for the named-tunnel workflow.
|
||||
:::
|
||||
|
||||
### ngrok
|
||||
|
||||
```bash
|
||||
ngrok http 8090
|
||||
```
|
||||
|
||||
Free tier shows a different URL on each restart. Paid tier gives you a stable subdomain.
|
||||
|
||||
### Your own domain + reverse proxy
|
||||
|
||||
If you already have a server with a TLS cert (Caddy, nginx, etc.), point a route at `localhost:8090`. This is the most stable option for production but requires existing infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Configuring the webhook on Meta's side
|
||||
|
||||
Once your tunnel is running:
|
||||
|
||||
1. Note the public URL printed by your tunnel — say `https://abc123.trycloudflare.com`.
|
||||
2. Generate a **Verify Token** — the wizard does this for you with `secrets.token_urlsafe(32)`; if you're configuring manually, run:
|
||||
```bash
|
||||
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
```
|
||||
Save it as `WHATSAPP_CLOUD_VERIFY_TOKEN` in `~/.hermes/.env`.
|
||||
3. Start the Hermes gateway: `hermes gateway`.
|
||||
4. In the Meta App Dashboard → **WhatsApp → Configuration** (or **Use cases → Customize → Configuration** depending on UI version) → click **Edit** on the Webhook section.
|
||||
5. Fill in:
|
||||
- **Callback URL**: `https://abc123.trycloudflare.com/whatsapp/webhook`
|
||||
- **Verify Token**: the string from step 2 (must match exactly)
|
||||
6. Click **Verify and save**. Meta hits your URL with a GET request, the gateway echoes back the challenge, and Meta marks the webhook as verified.
|
||||
7. Under **Webhook fields**, click **Manage** → subscribe to the **messages** field. This is what tells Meta to actually deliver inbound messages to your webhook.
|
||||
|
||||
**To verify the loop manually** (from a third terminal):
|
||||
|
||||
```bash
|
||||
TUNNEL="https://abc123.trycloudflare.com"
|
||||
VERIFY="<your verify token>"
|
||||
|
||||
# Should print HTTP 200 with body "hello"
|
||||
curl -i "$TUNNEL/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=$VERIFY&hub.challenge=hello"
|
||||
|
||||
# Health endpoint — should show verify_token_configured: true and app_secret_configured: true
|
||||
curl "$TUNNEL/health"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recipient whitelist (Meta-side)
|
||||
|
||||
In development mode (before your app goes through App Review), Meta restricts which numbers your bot can message:
|
||||
|
||||
1. App Dashboard → WhatsApp → API Setup → **To** dropdown.
|
||||
2. Click **Manage phone number list**.
|
||||
3. Add the phone numbers you want to message (yours, your team's, friendly testers). Meta sends each one a 6-digit verification code via SMS or WhatsApp.
|
||||
|
||||
Up to 5 numbers in dev mode. Going to App Review removes this limit.
|
||||
|
||||
---
|
||||
|
||||
## Allowlist (Hermes-side)
|
||||
|
||||
In addition to Meta's recipient whitelist, Hermes has its own per-platform allowlist that controls **which incoming messages the agent processes**. Add to `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
# Comma-separated phone numbers, country code, no '+' / spaces / dashes
|
||||
WHATSAPP_CLOUD_ALLOWED_USERS=15551234567,15557654321
|
||||
|
||||
# Or allow everyone (only safe in combination with Meta's recipient whitelist)
|
||||
# WHATSAPP_CLOUD_ALLOW_ALL_USERS=true
|
||||
```
|
||||
|
||||
The wizard sets this in step 6. Without an allowlist, **every inbound message is denied** — this is intentional, so the bot can't be invoked by random numbers if the recipient whitelist is ever loosened.
|
||||
|
||||
---
|
||||
|
||||
## Polishing your bot's WhatsApp profile
|
||||
|
||||
WhatsApp displays a **name and profile picture** for your bot in the chat header and contact list. These can't be set via the Cloud API — they live in Meta's Business Manager.
|
||||
|
||||
Once your bot is working, head to **[business.facebook.com/wa/manage/phone-numbers](https://business.facebook.com/wa/manage/phone-numbers/)**, click your phone number, and you'll find:
|
||||
|
||||
| What | Where | Notes |
|
||||
|---|---|---|
|
||||
| **Display name** | Top of the phone-number page | Changes go through Meta's name-review process (~24–48 hours). |
|
||||
| **Profile picture** | Top of the phone-number page | Square image, ≥640×640px recommended. Updates immediately. |
|
||||
| **About / description / website / email / hours / category** | "Edit profile" button | These appear in the info pane when a user taps the bot's name. Cosmetic. |
|
||||
| **Verified badge** (green checkmark) | Business Manager → Security Center → Start Verification | Requires Meta's separate business verification process. |
|
||||
|
||||
The `hermes whatsapp-cloud` wizard prints these links at the end of setup. None of this is required for the bot to work — it's pure polish for how your bot appears to users.
|
||||
|
||||
---
|
||||
|
||||
## Configuration reference
|
||||
|
||||
All settings live in `~/.hermes/.env`. Required values are in **bold**.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| **`WHATSAPP_CLOUD_PHONE_NUMBER_ID`** | — | The 15-17 digit ID from API Setup. **Not** the phone number. |
|
||||
| **`WHATSAPP_CLOUD_ACCESS_TOKEN`** | — | Meta access token (starts with `EAA`). Temp 24h or System User permanent. |
|
||||
| **`WHATSAPP_CLOUD_APP_SECRET`** | — | 32-char hex from Settings → Basic. Without it, inbound is refused with 503. |
|
||||
| **`WHATSAPP_CLOUD_VERIFY_TOKEN`** | — | Shared secret for the GET handshake. Auto-generated by the wizard. |
|
||||
| **`WHATSAPP_CLOUD_ALLOWED_USERS`** | — | Comma-separated wa_ids allowed to message the bot. |
|
||||
| `WHATSAPP_CLOUD_ALLOW_ALL_USERS` | `false` | Set to `true` to bypass the allowlist. |
|
||||
| `WHATSAPP_CLOUD_APP_ID` | — | Optional, for future analytics integration. |
|
||||
| `WHATSAPP_CLOUD_WABA_ID` | — | Optional, for future analytics integration. |
|
||||
| `WHATSAPP_CLOUD_WEBHOOK_HOST` | `0.0.0.0` | Interface the webhook server binds to. |
|
||||
| `WHATSAPP_CLOUD_WEBHOOK_PORT` | `8090` | Port the webhook server binds to. Must match the port your tunnel forwards. |
|
||||
| `WHATSAPP_CLOUD_WEBHOOK_PATH` | `/whatsapp/webhook` | URL path Meta posts to. |
|
||||
| `WHATSAPP_CLOUD_API_VERSION` | `v20.0` | Meta Graph API version. Only override if a newer version is recommended in Meta's docs. |
|
||||
| `WHATSAPP_CLOUD_HOME_CHANNEL` | — | wa_id to use as the bot's home channel (for cron jobs etc). |
|
||||
|
||||
You can have **both** the Baileys (`whatsapp`) and Cloud (`whatsapp_cloud`) adapters enabled simultaneously, targeting different phone numbers.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Inbound
|
||||
|
||||
- **Text messages** — passed straight to the agent.
|
||||
- **Images** — auto-downloaded and attached to the agent's input. Models with native vision (Claude, GPT-4o, Gemini, etc.) read the image directly; non-vision models receive an auto-generated text description.
|
||||
- **Voice notes** — auto-downloaded as `.ogg`, transcribed via your configured STT provider (local faster-whisper, OpenAI/Nous, Groq, etc.), then handed to the agent as text.
|
||||
- **Documents** — auto-downloaded. Small text-readable files (`.txt`, `.md`, `.json`, `.py`, `.csv`, etc.) up to 100KB get inlined into the agent's input so it can read them without a tool call. Larger files are cached locally for the agent's other tools to access.
|
||||
- **Button taps** — when the user taps a button the bot sent earlier (clarify choice, command approval, slash-command confirm), the tap is routed directly to the right handler. Stale taps fall back to being treated as regular text input.
|
||||
- **Reply context** — when the user replies to a previous bot message, the agent sees the original message as context.
|
||||
|
||||
### Outbound
|
||||
|
||||
- **Text** — markdown is auto-converted to WhatsApp's flavored syntax (`**bold**` → `*bold*`, `~~strike~~` → `~strike~`, headers → bold, `[link](url)` → `link (url)`). Long messages split at 4096 chars per chunk.
|
||||
- **Images** — agent-generated images and local image files both supported, delivered as native photo attachments.
|
||||
- **Voice messages** — text-to-speech output is converted via ffmpeg into the native WhatsApp voice-note bubble (green waveform). Without ffmpeg installed, falls back to an MP3 audio attachment. See "Voice messages" below.
|
||||
- **Video / documents** — both supported, sent as native attachments.
|
||||
|
||||
### Interactive UX
|
||||
|
||||
When the agent invokes any of these flows, Hermes uses WhatsApp's native interactive messages — tap-to-answer buttons instead of "reply with the number" prompts:
|
||||
|
||||
- **`clarify` tool** — multi-choice questions render as quick-reply buttons (1–3 choices) or a tap-to-open list sheet (4+ choices). Picking "✏️ Other" lets the user type a free-form answer that the agent receives as the resolution.
|
||||
- **Dangerous-command approvals** — when the agent's terminal/code execution hits a gated command, the user sees `✅ Approve` / `❌ Deny` buttons instead of needing to type `/approve` or `/deny`.
|
||||
- **Slash-command confirmations** — privileged commands like `/reload-mcp` show `✅ Approve Once` / `🔒 Always` / `❌ Cancel` buttons.
|
||||
|
||||
All interactive prompts gracefully degrade to plain text if the buttons fail to render (e.g. on legacy WhatsApp clients).
|
||||
|
||||
### Read receipts and typing indicator
|
||||
|
||||
Hermes acknowledges inbound messages immediately:
|
||||
|
||||
- Your message shows **blue double-checkmarks** as soon as the gateway receives it.
|
||||
- The bot's name in your WhatsApp chat shows **"typing…"** while the agent is preparing a reply.
|
||||
- The typing indicator auto-dismisses when the bot's first response message arrives.
|
||||
|
||||
This makes it obvious when the bot has seen your message versus when it's still working on a response.
|
||||
|
||||
### Voice messages
|
||||
|
||||
WhatsApp distinguishes between a "voice note" (the green waveform bubble) and a generic audio file attachment. The difference is purely codec: voice notes need to be `audio/ogg` with `opus` encoding.
|
||||
|
||||
Hermes TTS produces MP3. Two paths:
|
||||
|
||||
- **With ffmpeg on PATH** (recommended) — outbound TTS is converted and arrives as a proper voice note. Install:
|
||||
- Windows: `winget install Gyan.FFmpeg`
|
||||
- macOS: `brew install ffmpeg`
|
||||
- Linux: package manager
|
||||
- **Without ffmpeg** — outbound TTS arrives as an MP3 audio attachment. Plays fine, just doesn't look like a voice note. A one-time warning fires in the gateway log so you know.
|
||||
|
||||
You can check whether the gateway found ffmpeg via the health endpoint:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8090/health
|
||||
# look for "ffmpeg_present": true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Known limitations
|
||||
|
||||
### 24-hour conversation window
|
||||
|
||||
Meta only allows **free-form messages** within a 24-hour window after the user's last inbound message. Outside that window, the only thing Meta's API accepts is a pre-approved **message template**.
|
||||
|
||||
**What this means in practice:**
|
||||
|
||||
- Reactive chat (user DMs → bot replies within 24h → user replies → ...) works forever. This covers >95% of normal bot use.
|
||||
- **Cron jobs that deliver to WhatsApp** after a gap > 24h will fail with Graph error code `131047` ("Re-engagement message").
|
||||
- **Long-running `delegate_task` async results** that take longer than 24h fail the same way.
|
||||
- **Webhook subscribers** that route external events to WhatsApp fail when the user hasn't DM'd the bot recently.
|
||||
|
||||
Hermes warns the agent about this window in its system prompt, so the model knows to mention it when scheduling delayed messages.
|
||||
|
||||
Message-template support (the workaround for outside-window sends) is not yet implemented in Hermes. If you need it, please [open an issue](https://github.com/NousResearch/hermes-agent/issues) — it's planned but waiting on a clear demand signal.
|
||||
|
||||
### Group chats
|
||||
|
||||
The Cloud API has limited group support (capability-tier gated by Meta). Hermes's `whatsapp_cloud` adapter currently handles **direct messages only** in v1. If you need group chats, use the Baileys bridge.
|
||||
|
||||
### Outbound rate limit
|
||||
|
||||
Meta's default throughput is **80 messages/second per business phone number**, with upgrades available. Hermes doesn't currently enforce this client-side — extremely high-volume sends could hit Meta's limit.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Setup verification fails ("URL couldn't be validated") in Meta dashboard
|
||||
|
||||
Almost always one of:
|
||||
|
||||
- **Tunnel URL is wrong or stale** — cloudflared quick tunnels rotate. Get a fresh URL and update both `.env` and Meta's dashboard.
|
||||
- **Verify token mismatch** — the token in `~/.hermes/.env`'s `WHATSAPP_CLOUD_VERIFY_TOKEN` must match exactly what you typed into Meta's dashboard. Run the curl probe above to confirm the gateway's verify handshake works locally first.
|
||||
- **Gateway not running** — check `hermes gateway` is up.
|
||||
- **App Secret not set** — without it, Hermes refuses inbound POSTs with 503. Meta interprets that as "can't validate."
|
||||
|
||||
### `graph error 100`: Object with ID '...' does not exist
|
||||
|
||||
You pasted your phone number (10-11 digits) into `WHATSAPP_CLOUD_PHONE_NUMBER_ID` instead of the Phone Number ID (Meta's 15-17 digit internal ID). Re-check the API Setup page — the Phone Number ID is shown *below* the "From" dropdown.
|
||||
|
||||
The wizard catches this with a validator now, but it's worth knowing if you're configuring manually.
|
||||
|
||||
### `graph error 190`: Authentication Error
|
||||
|
||||
Your access token is invalid. Subcodes:
|
||||
|
||||
- `subcode 463` — token expired. Temp tokens last 24h. Regenerate, or switch to a System User permanent token (see above).
|
||||
- `subcode 467` — token invalidated (revoked or password changed).
|
||||
- Other 190 — token didn't have the required permissions when generated. Make sure all three (`business_management`, `whatsapp_business_messaging`, `whatsapp_business_management`) were selected.
|
||||
|
||||
### `graph error 131047`: Re-engagement message
|
||||
|
||||
The 24-hour conversation window expired (see "Known limitations"). Either:
|
||||
|
||||
- Ask the user to DM the bot first to reopen the window.
|
||||
- Wait for template support to land in Hermes.
|
||||
|
||||
### Inbound message: `media metadata fetch failed (status=401)`
|
||||
|
||||
Same 401 root causes as outbound (`graph error 190`) — the access token is invalid or expired. Fix the token.
|
||||
|
||||
### Bot replies appear as raw JSON / tool-call leakage
|
||||
|
||||
Common cause: the toolset configured for `whatsapp_cloud` is missing the tools the agent wants to call. Check `hermes tools list` and verify the platform is using `hermes-whatsapp` (the default Cloud adapter toolset, same as Baileys).
|
||||
|
||||
If the model emits tool-call-shaped text instead of a structured call, it usually means the toolset was effectively empty. See `hermes_cli/platforms.py` for the platform → default toolset mapping.
|
||||
|
||||
### STT (voice note transcription) returns empty / "could not transcribe"
|
||||
|
||||
The default `stt.provider: local` requires `pip install faster-whisper`. If you're a Nous subscriber, you can route STT through Meta's managed audio gateway instead:
|
||||
|
||||
```bash
|
||||
hermes config set stt.provider openai
|
||||
hermes config set stt.use_gateway true
|
||||
hermes gateway restart
|
||||
```
|
||||
|
||||
This uses your Nous Portal access token instead of needing a separate OpenAI key.
|
||||
|
||||
---
|
||||
|
||||
## Security notes
|
||||
|
||||
- **Treat the App Secret like a password** — anyone with it can forge webhook payloads that Hermes will accept as authentic.
|
||||
- **The verify token is a shared secret** — leaks are lower-stakes (worst case someone could re-subscribe Meta's webhook to a different URL of theirs), but still avoid committing it.
|
||||
- **The access token is your bot's identity** — System User tokens are equivalent to long-lived API keys. Rotate immediately if a deployment is compromised.
|
||||
- **The webhook endpoint accepts only signed requests when `WHATSAPP_CLOUD_APP_SECRET` is set** — leave it set even in development. Without it, the gateway refuses inbound delivery with HTTP 503.
|
||||
- **The `/health` endpoint is unauthenticated** — it's safe to expose because it only reports config-presence booleans, not the values themselves. But if you'd rather not surface it, restrict access at the reverse proxy / tunnel layer.
|
||||
|
||||
---
|
||||
|
||||
## Comparison to the Baileys bridge
|
||||
|
||||
| | Baileys (`hermes whatsapp`) | Cloud API (`hermes whatsapp-cloud`) |
|
||||
|---|---|---|
|
||||
| Account type | Personal | Business |
|
||||
| Setup | QR code scan | Meta app + WABA + token |
|
||||
| Dependencies | Node.js + npm | Pure Python (httpx + aiohttp) |
|
||||
| Process | Managed Node subprocess | aiohttp webhook server |
|
||||
| Public URL needed? | No | Yes |
|
||||
| Account ban risk | Yes (unofficial API) | No (officially supported) |
|
||||
| Inbound | Polling Node bridge | Webhook POST from Meta |
|
||||
| Outbound | Local bridge → Baileys | HTTPS to graph.facebook.com |
|
||||
| Groups | Full support | DMs only (v1) |
|
||||
| 24h window | No restriction | Hard rule — templates required after |
|
||||
| Voice notes (out) | Native | Native with ffmpeg, MP3 fallback otherwise |
|
||||
| Read receipts | No | Yes (blue double-checkmarks) |
|
||||
| Typing indicator | No | Yes (auto-dismisses on response) |
|
||||
| Interactive buttons | Text fallback only | Native (clarify, approval, slash-confirm) |
|
||||
| Production use | Risky (Meta can ban) | Designed for it |
|
||||
|
||||
Most users running Hermes for personal projects prefer Baileys. Most users running customer-facing bots prefer Cloud API.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- [Meta's official WhatsApp Business Cloud API docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/) — authoritative reference for the underlying platform, pricing, App Review, and Meta-side rate limits.
|
||||
- [WhatsApp (Baileys bridge) Setup](whatsapp.md) — the alternative integration for personal projects.
|
||||
- [Messaging Platforms overview](index.md) — all messaging integrations at a glance.
|
||||
@@ -0,0 +1,266 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
title: "WhatsApp"
|
||||
description: "Set up Hermes Agent as a WhatsApp bot via the built-in Baileys bridge"
|
||||
---
|
||||
|
||||
# WhatsApp Setup
|
||||
|
||||
Hermes connects to WhatsApp through a built-in bridge based on **Baileys**. This works by emulating a WhatsApp Web session — **not** through the official WhatsApp Business API. No Meta developer account or Business verification is required.
|
||||
|
||||
> Run `hermes gateway setup` and pick **WhatsApp** for a guided walk-through.
|
||||
|
||||
:::tip Two WhatsApp integrations
|
||||
This page is for the **Baileys bridge** — quick to set up, personal accounts, no public URL needed, ban risk.
|
||||
|
||||
If you're running a real business bot and want stability, see the **[WhatsApp Business Cloud API guide](./whatsapp-cloud.md)** instead. It's the official Meta-supported path: no account ban risk, but requires a Meta Business account and a public webhook URL.
|
||||
|
||||
The two adapters can also run in parallel against different phone numbers if you have a reason to.
|
||||
:::
|
||||
|
||||
:::warning Unofficial API — Ban Risk
|
||||
WhatsApp does **not** officially support third-party bots outside the Business API. Using a third-party bridge carries a small risk of account restrictions. To minimize risk:
|
||||
- **Use a dedicated phone number** for the bot (not your personal number)
|
||||
- **Don't send bulk/spam messages** — keep usage conversational
|
||||
- **Don't automate outbound messaging** to people who haven't messaged first
|
||||
:::
|
||||
|
||||
:::warning WhatsApp Web Protocol Updates
|
||||
WhatsApp periodically updates their Web protocol, which can temporarily break compatibility
|
||||
with third-party bridges. When this happens, Hermes will update the bridge dependency. If the
|
||||
bot stops working after a WhatsApp update, pull the latest Hermes version and re-pair.
|
||||
:::
|
||||
|
||||
## Two Modes
|
||||
|
||||
| Mode | How it works | Best for |
|
||||
|------|-------------|----------|
|
||||
| **Separate bot number** (recommended) | Dedicate a phone number to the bot. People message that number directly. | Clean UX, multiple users, lower ban risk |
|
||||
| **Personal self-chat** | Use your own WhatsApp. You message yourself to talk to the agent. | Quick setup, single user, testing |
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js v18+** and **npm** — the WhatsApp bridge runs as a Node.js process
|
||||
- **A phone with WhatsApp** installed (for scanning the QR code)
|
||||
|
||||
Unlike older browser-driven bridges, the current Baileys-based bridge does **not** require a local Chromium or Puppeteer dependency stack.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Run the Setup Wizard
|
||||
|
||||
```bash
|
||||
hermes whatsapp
|
||||
```
|
||||
|
||||
The wizard will:
|
||||
|
||||
1. Ask which mode you want (**bot** or **self-chat**)
|
||||
2. Install bridge dependencies if needed
|
||||
3. Display a **QR code** in your terminal
|
||||
4. Wait for you to scan it
|
||||
|
||||
**To scan the QR code:**
|
||||
|
||||
1. Open WhatsApp on your phone
|
||||
2. Go to **Settings → Linked Devices**
|
||||
3. Tap **Link a Device**
|
||||
4. Point your camera at the terminal QR code
|
||||
|
||||
Once paired, the wizard confirms the connection and exits. Your session is saved automatically.
|
||||
|
||||
:::tip
|
||||
If the QR code looks garbled, make sure your terminal is at least 60 columns wide and supports
|
||||
Unicode. You can also try a different terminal emulator.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Getting a Second Phone Number (Bot Mode)
|
||||
|
||||
For bot mode, you need a phone number that isn't already registered with WhatsApp. Three options:
|
||||
|
||||
| Option | Cost | Notes |
|
||||
|--------|------|-------|
|
||||
| **Google Voice** | Free | US only. Get a number at [voice.google.com](https://voice.google.com). Verify WhatsApp via SMS through the Google Voice app. |
|
||||
| **Prepaid SIM** | $5–15 one-time | Any carrier. Activate, verify WhatsApp, then the SIM can sit in a drawer. Number must stay active (make a call every 90 days). |
|
||||
| **VoIP services** | Free–$5/month | TextNow, TextFree, or similar. Some VoIP numbers are blocked by WhatsApp — try a few if the first doesn't work. |
|
||||
|
||||
After getting the number:
|
||||
|
||||
1. Install WhatsApp on a phone (or use WhatsApp Business app with dual-SIM)
|
||||
2. Register the new number with WhatsApp
|
||||
3. Run `hermes whatsapp` and scan the QR code from that WhatsApp account
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Configure Hermes
|
||||
|
||||
Add the following to your `~/.hermes/.env` file:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
WHATSAPP_ENABLED=true
|
||||
WHATSAPP_MODE=bot # "bot" or "self-chat"
|
||||
|
||||
# Access control — pick ONE of these options:
|
||||
WHATSAPP_ALLOWED_USERS=15551234567 # Comma-separated phone numbers (with country code, no +)
|
||||
# WHATSAPP_ALLOWED_USERS=* # OR use * to allow everyone
|
||||
# WHATSAPP_ALLOW_ALL_USERS=true # OR set this flag instead (same effect as *)
|
||||
```
|
||||
|
||||
:::tip Allow-all shorthand
|
||||
Setting `WHATSAPP_ALLOWED_USERS=*` allows **all** senders (equivalent to `WHATSAPP_ALLOW_ALL_USERS=true`).
|
||||
This is consistent with [Signal group allowlists](/reference/environment-variables).
|
||||
To use the pairing flow instead, remove both variables and rely on the
|
||||
[DM pairing system](/user-guide/security#dm-pairing-system).
|
||||
:::
|
||||
|
||||
Optional behavior settings in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
unauthorized_dm_behavior: pair
|
||||
|
||||
whatsapp:
|
||||
unauthorized_dm_behavior: ignore
|
||||
```
|
||||
|
||||
- `unauthorized_dm_behavior: pair` is the global default. Unknown DM senders get a pairing code.
|
||||
- `whatsapp.unauthorized_dm_behavior: ignore` makes WhatsApp stay silent for unauthorized DMs, which is usually the better choice for a private number.
|
||||
|
||||
Then start the gateway:
|
||||
|
||||
```bash
|
||||
hermes gateway # Foreground
|
||||
hermes gateway install # Install as a user service
|
||||
sudo hermes gateway install --system # Linux only: boot-time system service
|
||||
```
|
||||
|
||||
The gateway starts the WhatsApp bridge automatically using the saved session.
|
||||
|
||||
---
|
||||
|
||||
## Session Persistence
|
||||
|
||||
The Baileys bridge saves its session under `~/.hermes/platforms/whatsapp/session`. This means:
|
||||
|
||||
- **Sessions survive restarts** — you don't need to re-scan the QR code every time
|
||||
- The session data includes encryption keys and device credentials
|
||||
- **Do not share or commit this session directory** — it grants full access to the WhatsApp account
|
||||
|
||||
---
|
||||
|
||||
## Re-pairing
|
||||
|
||||
If the session breaks (phone reset, WhatsApp update, manually unlinked), you'll see connection
|
||||
errors in the gateway logs. To fix it:
|
||||
|
||||
```bash
|
||||
hermes whatsapp
|
||||
```
|
||||
|
||||
This generates a fresh QR code. Scan it again and the session is re-established. The gateway
|
||||
handles **temporary** disconnections (network blips, phone going offline briefly) automatically
|
||||
with reconnection logic.
|
||||
|
||||
---
|
||||
|
||||
## Voice Messages
|
||||
|
||||
Hermes supports voice on WhatsApp:
|
||||
|
||||
- **Incoming:** Voice messages (`.ogg` opus) are automatically transcribed using the configured STT provider: local `faster-whisper`, Groq Whisper (`GROQ_API_KEY`), or OpenAI Whisper (`VOICE_TOOLS_OPENAI_KEY`)
|
||||
- **Outgoing:** TTS responses are sent as MP3 audio file attachments
|
||||
- Agent responses are prefixed with "⚕ **Hermes Agent**" by default. You can customize or disable this in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/config.yaml
|
||||
whatsapp:
|
||||
reply_prefix: "" # Empty string disables the header
|
||||
# reply_prefix: "🤖 *My Bot*\n──────\n" # Custom prefix (supports \n for newlines)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message Formatting & Delivery
|
||||
|
||||
WhatsApp supports **streaming (progressive) responses** — the bot edits its message in real-time as the AI generates text, just like Discord and Telegram. Internally, WhatsApp is classified as a TIER_MEDIUM platform for delivery capabilities.
|
||||
|
||||
### Chunking
|
||||
|
||||
Long responses are automatically split into multiple messages at **4,096 characters** per chunk (WhatsApp's practical display limit). You don't need to configure anything — the gateway handles splitting and sends chunks sequentially.
|
||||
|
||||
### WhatsApp-Compatible Markdown
|
||||
|
||||
Standard Markdown in AI responses is automatically converted to WhatsApp's native formatting:
|
||||
|
||||
| Markdown | WhatsApp | Renders as |
|
||||
|----------|----------|------------|
|
||||
| `**bold**` | `*bold*` | **bold** |
|
||||
| `~~strikethrough~~` | `~strikethrough~` | ~~strikethrough~~ |
|
||||
| `# Heading` | `*Heading*` | Bold text (no native headings) |
|
||||
| `[link text](url)` | `link text (url)` | Inline URL |
|
||||
|
||||
Code blocks and inline code are preserved as-is since WhatsApp supports triple-backtick formatting natively.
|
||||
|
||||
### Tool Progress
|
||||
|
||||
When the agent calls tools (web search, file operations, etc.), WhatsApp displays real-time progress indicators showing which tool is running. This is enabled by default — no configuration needed.
|
||||
|
||||
### Message Batching (Debounce)
|
||||
|
||||
WhatsApp delivers each message individually, so a rapid burst (forwarded batches, paste-splits, multi-line text) would otherwise trigger a separate agent invocation per fragment — wasting tokens and producing several disjointed replies. The adapter buffers successive text messages from the same chat and dispatches them as one combined request after a short quiet period (default **5s**, extended to **10s** for very long fragments). Tune via `config.yaml`:
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/config.yaml
|
||||
gateway:
|
||||
platforms:
|
||||
whatsapp:
|
||||
extra:
|
||||
text_batch_delay_seconds: 5.0 # quiet period before flushing a batch
|
||||
text_batch_split_delay_seconds: 10.0 # extended delay near the split threshold
|
||||
```
|
||||
|
||||
Set `text_batch_delay_seconds: 0` to dispatch each message immediately (disables batching).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| **QR code not scanning** | Ensure terminal is wide enough (60+ columns). Try a different terminal. Make sure you're scanning from the correct WhatsApp account (bot number, not personal). |
|
||||
| **QR code expires** | QR codes refresh every ~20 seconds. If it times out, restart `hermes whatsapp`. |
|
||||
| **Session not persisting** | Check that `~/.hermes/platforms/whatsapp/session` exists and is writable. If containerized, mount it as a persistent volume. |
|
||||
| **Logged out unexpectedly** | WhatsApp unlinks devices after long inactivity. Keep the phone on and connected to the network, then re-pair with `hermes whatsapp` if needed. |
|
||||
| **Bridge crashes or reconnect loops** | Restart the gateway, update Hermes, and re-pair if the session was invalidated by a WhatsApp protocol change. |
|
||||
| **Bot stops working after WhatsApp update** | Update Hermes to get the latest bridge version, then re-pair. |
|
||||
| **macOS: "Node.js not installed" but node works in terminal** | launchd services don't inherit your shell PATH. Run `hermes gateway install` to re-snapshot your current PATH into the plist, then `hermes gateway start`. See the [Gateway Service docs](./index.md#macos-launchd) for details. |
|
||||
| **Messages not being received** | Verify `WHATSAPP_ALLOWED_USERS` includes the sender's number (with country code, no `+` or spaces), or set it to `*` to allow everyone. Set `WHATSAPP_DEBUG=true` in `.env` and restart the gateway to see raw message events in `bridge.log`. |
|
||||
| **Bot replies to strangers with a pairing code** | Set `whatsapp.unauthorized_dm_behavior: ignore` in `~/.hermes/config.yaml` if you want unauthorized DMs to be silently ignored instead. |
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
**Configure access control** before going live. Set `WHATSAPP_ALLOWED_USERS` with specific
|
||||
phone numbers (including country code, without the `+`), use `*` to allow everyone, or set
|
||||
`WHATSAPP_ALLOW_ALL_USERS=true`. Without any of these, the gateway **denies all incoming
|
||||
messages** as a safety measure.
|
||||
:::
|
||||
|
||||
By default, unauthorized DMs still receive a pairing code reply. If you want a private WhatsApp number to stay completely silent to strangers, set:
|
||||
|
||||
```yaml
|
||||
whatsapp:
|
||||
unauthorized_dm_behavior: ignore
|
||||
```
|
||||
|
||||
- The `~/.hermes/platforms/whatsapp/session` directory contains full session credentials — protect it like a password
|
||||
- Set file permissions: `chmod 700 ~/.hermes/platforms/whatsapp/session`
|
||||
- Use a **dedicated phone number** for the bot to isolate risk from your personal account
|
||||
- If you suspect compromise, unlink the device from WhatsApp → Settings → Linked Devices
|
||||
- Phone numbers in logs are partially redacted, but review your log retention policy
|
||||
@@ -0,0 +1,341 @@
|
||||
---
|
||||
sidebar_position: 16
|
||||
title: "Yuanbao"
|
||||
description: "Connect Hermes Agent to the Yuanbao enterprise messaging platform via WebSocket gateway"
|
||||
---
|
||||
|
||||
# Yuanbao
|
||||
|
||||
Connect Hermes to [Yuanbao](https://yuanbao.tencent.com/), Tencent's enterprise messaging platform. The adapter uses a WebSocket gateway for real-time message delivery and supports both direct (C2C) and group conversations.
|
||||
|
||||
:::info
|
||||
Yuanbao is an enterprise messaging platform primarily used within Tencent and enterprise environments. It uses WebSocket for real-time communication, HMAC-based authentication, and supports rich media including images, files, and voice messages.
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Yuanbao account with bot creation permissions
|
||||
- Yuanbao APP_ID and APP_SECRET (from platform admin)
|
||||
- Python packages: `websockets` and `httpx`
|
||||
- For media support: `aiofiles`
|
||||
|
||||
Install the required dependencies:
|
||||
|
||||
```bash
|
||||
pip install websockets httpx aiofiles
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Create a Bot in Yuanbao
|
||||
|
||||
1. Download the Yuanbao app from [https://yuanbao.tencent.com/](https://yuanbao.tencent.com/)
|
||||
2. In the app, go to **PAI → My Bot** and create a new bot
|
||||
3. After the bot is created, copy the **APP_ID** and **APP_SECRET**
|
||||
|
||||
### 2. Run the Setup Wizard
|
||||
|
||||
The easiest way to configure Yuanbao is through the interactive setup:
|
||||
|
||||
```bash
|
||||
hermes gateway setup
|
||||
```
|
||||
|
||||
Select **Yuanbao** when prompted. The wizard will:
|
||||
|
||||
1. Ask for your APP_ID
|
||||
2. Ask for your APP_SECRET
|
||||
3. Save the configuration automatically
|
||||
|
||||
:::tip
|
||||
The WebSocket URL and API Domain have sensible defaults built in. You only need to provide APP_ID and APP_SECRET to get started.
|
||||
:::
|
||||
|
||||
### 3. Configure Environment Variables
|
||||
|
||||
After initial setup, verify these variables in `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
YUANBAO_APP_ID=your-app-id
|
||||
YUANBAO_APP_SECRET=your-app-secret
|
||||
YUANBAO_WS_URL=wss://api.yuanbao.example.com/ws
|
||||
YUANBAO_API_DOMAIN=https://api.yuanbao.example.com
|
||||
|
||||
# Optional: bot account ID (normally obtained automatically from sign-token)
|
||||
# YUANBAO_BOT_ID=your-bot-id
|
||||
|
||||
# Optional: internal routing environment (e.g. test/staging/production)
|
||||
# YUANBAO_ROUTE_ENV=production
|
||||
|
||||
# Optional: home channel for cron/notifications (format: direct:<account> or group:<group_code>)
|
||||
YUANBAO_HOME_CHANNEL=direct:bot_account_id
|
||||
YUANBAO_HOME_CHANNEL_NAME="Bot Notifications"
|
||||
|
||||
# Optional: restrict access (legacy, see Access Control below for fine-grained policies)
|
||||
YUANBAO_ALLOWED_USERS=user_account_1,user_account_2
|
||||
```
|
||||
|
||||
### 4. Start the Gateway
|
||||
|
||||
```bash
|
||||
hermes gateway
|
||||
```
|
||||
|
||||
The adapter will connect to the Yuanbao WebSocket gateway, authenticate using HMAC signatures, and begin processing messages.
|
||||
|
||||
## Features
|
||||
|
||||
- **WebSocket gateway** — real-time bidirectional communication
|
||||
- **HMAC authentication** — secure request signing with APP_ID/APP_SECRET
|
||||
- **C2C messaging** — direct user-to-bot conversations
|
||||
- **Group messaging** — conversations in group chats
|
||||
- **Media support** — images, files, and voice messages via COS (Cloud Object Storage)
|
||||
- **Markdown formatting** — messages are automatically chunked for Yuanbao's size limits
|
||||
- **Message deduplication** — prevents duplicate processing of the same message
|
||||
- **Heartbeat/keep-alive** — maintains WebSocket connection stability
|
||||
- **Typing indicators** — shows "typing…" status while the agent processes
|
||||
- **Automatic reconnection** — handles WebSocket disconnections with exponential backoff
|
||||
- **Group information queries** — retrieve group details and member lists
|
||||
- **Sticker/Emoji support** — send TIMFaceElem stickers and emoji in conversations
|
||||
- **Auto-sethome** — first user to message the bot is automatically set as the home channel owner
|
||||
- **Slow-response notification** — sends a waiting message when the agent takes longer than expected
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Chat ID Formats
|
||||
|
||||
Yuanbao uses prefixed identifiers depending on conversation type:
|
||||
|
||||
| Chat Type | Format | Example |
|
||||
|-----------|--------|---------|
|
||||
| Direct message (C2C) | `direct:<account>` | `direct:user123` |
|
||||
| Group message | `group:<group_code>` | `group:grp456` |
|
||||
|
||||
### Media Uploads
|
||||
|
||||
The Yuanbao adapter automatically handles media uploads via COS (Tencent Cloud Object Storage):
|
||||
|
||||
- **Images**: Supports JPEG, PNG, GIF, WebP
|
||||
- **Files**: Supports all common document types
|
||||
- **Voice**: Supports WAV, MP3, OGG
|
||||
|
||||
Media URLs are automatically validated and downloaded before upload to prevent SSRF attacks.
|
||||
|
||||
## Home Channel
|
||||
|
||||
Use the `/sethome` command in any Yuanbao chat (DM or group) to designate it as the **home channel**. Scheduled tasks (cron jobs) deliver their results to this channel.
|
||||
|
||||
:::tip Auto-sethome
|
||||
If no home channel is configured, the first user to message the bot will be automatically set as the home channel owner. If the current home channel is a group chat, the first DM will upgrade it to a direct channel.
|
||||
:::
|
||||
|
||||
You can also set it manually in `~/.hermes/.env`:
|
||||
|
||||
```bash
|
||||
YUANBAO_HOME_CHANNEL=direct:user_account_id
|
||||
# or for a group:
|
||||
# YUANBAO_HOME_CHANNEL=group:group_code
|
||||
YUANBAO_HOME_CHANNEL_NAME="My Bot Updates"
|
||||
```
|
||||
|
||||
### Example: Set Home Channel
|
||||
|
||||
1. Start a conversation with the bot in Yuanbao
|
||||
2. Send the command: `/sethome`
|
||||
3. The bot responds: "Home channel set to [chat_name] with ID [chat_id]. Cron jobs will deliver to this location."
|
||||
4. Future cron jobs and notifications will be sent to this channel
|
||||
|
||||
### Example: Cron Job Delivery
|
||||
|
||||
Create a cron job:
|
||||
|
||||
```bash
|
||||
/cron "0 9 * * *" Check server status
|
||||
```
|
||||
|
||||
The scheduled output will be delivered to your Yuanbao home channel every day at 9 AM.
|
||||
|
||||
## Usage Tips
|
||||
|
||||
### Starting a Conversation
|
||||
|
||||
Send any message to the bot in Yuanbao:
|
||||
|
||||
```
|
||||
hello
|
||||
```
|
||||
|
||||
The bot responds in the same conversation thread.
|
||||
|
||||
### Available Commands
|
||||
|
||||
All standard Hermes commands work on Yuanbao:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/new` | Start a fresh conversation |
|
||||
| `/model [provider:model]` | Show or change the model |
|
||||
| `/sethome` | Set this chat as the home channel |
|
||||
| `/status` | Show session info |
|
||||
| `/help` | Show available commands |
|
||||
|
||||
### Sending Files
|
||||
|
||||
To send a file to the bot, simply attach it directly in the Yuanbao chat. The bot will automatically download and process the file attachment.
|
||||
|
||||
You can also include a message with the attachment:
|
||||
|
||||
```
|
||||
Please analyze this document
|
||||
```
|
||||
|
||||
### Receiving Files
|
||||
|
||||
When you ask the bot to create or export a file, it sends the file directly to your Yuanbao chat.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot is online but not responding to messages
|
||||
|
||||
**Cause**: Authentication failed during WebSocket handshake.
|
||||
|
||||
**Fix**:
|
||||
1. Verify APP_ID and APP_SECRET are correct
|
||||
2. Check that the WebSocket URL is accessible
|
||||
3. Ensure the bot account has proper permissions
|
||||
4. Review gateway logs: `tail -f ~/.hermes/logs/gateway.log`
|
||||
|
||||
### "Connection refused" error
|
||||
|
||||
**Cause**: WebSocket URL is unreachable or incorrect.
|
||||
|
||||
**Fix**:
|
||||
1. Verify the WebSocket URL format (should start with `wss://`)
|
||||
2. Check network connectivity to the Yuanbao API domain
|
||||
3. Confirm firewall allows WebSocket connections
|
||||
4. Test URL with: `curl -I https://[YUANBAO_API_DOMAIN]`
|
||||
|
||||
### Media uploads fail
|
||||
|
||||
**Cause**: COS credentials are invalid or media server is unreachable.
|
||||
|
||||
**Fix**:
|
||||
1. Verify API_DOMAIN is correct
|
||||
2. Check that media upload permissions are enabled for your bot
|
||||
3. Ensure the media file is accessible and not corrupted
|
||||
4. Check COS bucket configuration with platform admin
|
||||
|
||||
### Messages not delivered to home channel
|
||||
|
||||
**Cause**: Home channel ID format is incorrect or cron job hasn't triggered.
|
||||
|
||||
**Fix**:
|
||||
1. Verify YUANBAO_HOME_CHANNEL is in correct format
|
||||
2. Test with `/sethome` command to auto-detect correct format
|
||||
3. Check cron job schedule with `/status`
|
||||
4. Verify bot has send permissions in the target chat
|
||||
|
||||
### Frequent disconnections
|
||||
|
||||
**Cause**: WebSocket connection is unstable or network is unreliable.
|
||||
|
||||
**Fix**:
|
||||
1. Check gateway logs for error patterns
|
||||
2. Increase heartbeat timeout in connection settings
|
||||
3. Ensure stable network connection to Yuanbao API
|
||||
4. Consider enabling verbose logging: `HERMES_LOG_LEVEL=debug`
|
||||
|
||||
## Access Control
|
||||
|
||||
Yuanbao supports fine-grained access control for both DM and group conversations:
|
||||
|
||||
```bash
|
||||
# DM policy: open (default) | allowlist | disabled
|
||||
YUANBAO_DM_POLICY=open
|
||||
# Comma-separated user IDs allowed to DM the bot (only used when DM_POLICY=allowlist)
|
||||
YUANBAO_DM_ALLOW_FROM=user_id_1,user_id_2
|
||||
|
||||
# Group policy: open (default) | allowlist | disabled
|
||||
YUANBAO_GROUP_POLICY=open
|
||||
# Comma-separated group codes allowed (only used when GROUP_POLICY=allowlist)
|
||||
YUANBAO_GROUP_ALLOW_FROM=group_code_1,group_code_2
|
||||
```
|
||||
|
||||
These can also be set in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
yuanbao:
|
||||
extra:
|
||||
dm_policy: allowlist
|
||||
dm_allow_from: "user1,user2"
|
||||
group_policy: open
|
||||
group_allow_from: ""
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Message Chunking
|
||||
|
||||
Yuanbao has a maximum message size. Hermes automatically chunks large responses with Markdown-aware splitting (respects code fences, tables, and paragraph boundaries).
|
||||
|
||||
### Connection Parameters
|
||||
|
||||
The following connection parameters are built into the adapter with sensible defaults:
|
||||
|
||||
| Parameter | Default Value | Description |
|
||||
|-----------|---------------|-------------|
|
||||
| WebSocket connect timeout | 15 seconds | Time to wait for WS handshake |
|
||||
| Heartbeat interval | 30 seconds | Ping frequency to keep connection alive |
|
||||
| Max reconnect attempts | 100 | Maximum number of reconnection tries |
|
||||
| Reconnect backoff | 1s → 60s (exponential) | Wait time between reconnect attempts |
|
||||
| Reply heartbeat interval | 2 seconds | RUNNING status send frequency |
|
||||
| Send timeout | 30 seconds | Timeout for outbound WS messages |
|
||||
|
||||
:::note
|
||||
These values are currently not configurable via environment variables. They are optimized for typical Yuanbao deployments.
|
||||
:::
|
||||
|
||||
### Verbose Logging
|
||||
|
||||
Enable debug logging to troubleshoot connection issues:
|
||||
|
||||
```bash
|
||||
HERMES_LOG_LEVEL=debug hermes gateway
|
||||
```
|
||||
|
||||
## Integration with Other Features
|
||||
|
||||
### Cron Jobs
|
||||
|
||||
Schedule tasks that run on Yuanbao:
|
||||
|
||||
```
|
||||
/cron "0 */4 * * *" Report system health
|
||||
```
|
||||
|
||||
Results are delivered to your home channel.
|
||||
|
||||
### Background Tasks
|
||||
|
||||
Run long operations without blocking the conversation:
|
||||
|
||||
```
|
||||
/background Analyze all files in the archive
|
||||
```
|
||||
|
||||
### Cross-Platform Messages
|
||||
|
||||
Send a message from CLI to Yuanbao:
|
||||
|
||||
```bash
|
||||
hermes chat -q "Send 'Hello from CLI' to yuanbao:group:group_code"
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Messaging Gateway Overview](./index.md)
|
||||
- [Slash Commands Reference](/reference/slash-commands)
|
||||
- [Cron Jobs](/user-guide/features/cron)
|
||||
- [Background Sessions](/user-guide/cli#background-sessions)
|
||||
Reference in New Issue
Block a user