Connect a messaging gateway to sandboxed.sh while the assistant runtime moves to Hermes. The current built-in compatibility bridge still uses Telegram webhooks, and the dashboard manages it from the top-level Assistant page.
An Assistant Gateway is the operator-facing bridge between chat transports and sandboxed.sh mission control. During the Hermes cutover there are two important pieces:
- Hermes runtime: the target assistant runtime and memory owner. It is an external service and is not shipped by this repository.
- Telegram compatibility bridge: the existing sandboxed.sh webhook path used until Hermes owns the bot webhook.
The current compatibility bridge creates assistant-mode missions and routes
Telegram messages to them. Hermes should replace that runtime by calling
assistant-mcp for sandboxed.sh mission and workspace control. See
Hermes Assistant Migration for the full
handoff contract.
There are two supported compatibility setups:
- Standalone gateway (recommended during cutover) — auto-creates a new mission per Telegram chat. Configured from Assistant in the dashboard.
- Per-mission channel — attaches a bot to an existing mission via the API. All chats share one mission context.
The legacy /settings/telegram dashboard route redirects to the top-level
Assistant page. New operator docs and links should use /assistant.
- Open Assistant in the dashboard.
- Check the MCP and Runtime readiness cards.
- Click Add Gateway.
- Paste your bot token from @BotFather.
- Choose a backend, model, workspace, and trigger mode.
- Click Add Gateway.
If the compatibility bridge remains active, each Telegram chat automatically
gets its own mission. Once Hermes owns the bot webhook, configure the gateway in
Hermes instead and use sandboxed.sh through assistant-mcp.
- Open Telegram and message @BotFather
- Send
/newbotand follow the prompts to choose a name and username - Copy the bot token (e.g.
123456789:ABCdefGHIjklMNOpqrsTUVwxyz)
Create a mission that will serve as the assistant:
POST /api/control/missions
Authorization: Bearer <token>
{
"title": "My Assistant Gateway"
}
Note the returned mission id.
POST /api/control/missions/:mission_id/telegram-channels
Authorization: Bearer <token>
{
"bot_token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"bot_username": "my_bot",
"allowed_chat_ids": [12345678],
"trigger_mode": "mention_or_dm",
"instructions": "Respond in plain text only. Do not use markdown formatting."
}
This will:
- Auto-set the mission to
assistantmode - Register a Telegram webhook so the bot receives messages
- Start routing messages to the mission
POST /api/control/telegram/bots
Authorization: Bearer <token>
{
"bot_token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"bot_username": "my_bot",
"trigger_mode": "mention_or_dm",
"instructions": "You are a helpful assistant.",
"default_backend": "claudecode",
"default_model_override": "claude-sonnet-4-20250514",
"default_workspace_id": "uuid-of-workspace",
"default_config_profile": "default"
}
Each new Telegram chat will automatically create a dedicated mission using the specified defaults.
| Field | Required | Description |
|---|---|---|
bot_token |
Yes | Bot token from BotFather |
bot_username |
No | Bot username (auto-detected if omitted) |
allowed_chat_ids |
No | Restrict to specific chat IDs. Empty = allow all. |
trigger_mode |
No | See Trigger Modes. Default: mention_or_dm |
instructions |
No | System instructions prepended to every message |
| Field | Required | Description |
|---|---|---|
default_backend |
No | Backend for auto-created missions (e.g. claudecode, opencode, codex, grok) |
default_model_override |
No | Model override for auto-created missions |
default_model_effort |
No | Model effort level (low, medium, high) |
default_workspace_id |
No | Workspace for auto-created missions |
default_config_profile |
No | Config profile from the Library |
default_agent |
No | Agent name for auto-created missions |
| Value | Description |
|---|---|
mention_or_dm |
Respond to @mentions in groups, replies to bot, or DMs (default) |
bot_mention |
Only respond when @mentioned in groups |
reply |
Only respond to replies to bot messages |
direct_message |
Only respond in private (1:1) conversations |
always |
Process every message in allowed chats |
The instructions field lets you customize the assistant's behavior
per-channel. Common uses:
"Respond in plain text only. Do not use markdown formatting."— Telegram doesn't render full markdown"You are a helpful coding assistant. Keep answers concise."— Set personality/scope"Always respond in French."— Language preference
Instructions are prepended to every incoming message as [Instructions: ...].
Set allowed_chat_ids to restrict which Telegram chats can interact with the
bot. Leave empty to allow all chats. You can find a chat's ID by forwarding a
message to @userinfobot.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/control/telegram/bots |
List all standalone gateways |
POST |
/api/control/telegram/bots |
Create a standalone gateway |
GET |
/api/control/telegram/bots/:id/chats |
List chats for a gateway |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/control/missions/:id/telegram-channels |
List channels for a mission |
POST |
/api/control/missions/:id/telegram-channels |
Create a channel |
PATCH |
/api/control/telegram-channels/:id |
Update channel settings |
POST |
/api/control/telegram-channels/:id/toggle |
Toggle active/inactive |
DELETE |
/api/control/telegram-channels/:id |
Delete a channel |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/control/assistants |
List all assistant-mode missions |
POST |
/api/control/missions/:id/mode |
Set mission mode (task or assistant) |
| Variable | Description |
|---|---|
SANDBOXED_PUBLIC_URL |
Public URL for webhook registration (e.g. https://agent.example.com). Falls back to http://{HOST}:{PORT}. |
Telegram -> webhook -> /api/telegram/webhook/:channel_id
-> TelegramBridge routes to ChannelContext
-> ControlCommand::UserMessage { target_mission_id }
-> MissionRunner (parallel execution)
-> AgentEvent stream
-> TelegramBridge sends response via editMessageText
Telegram / other chat
-> Hermes gateway + memory
-> assistant-mcp
-> sandboxed.sh mission/workspace/model APIs
Key design decisions:
- Parallel execution: Telegram messages always run in parallel runners, never hijacking the main session.
- Webhook-based: Uses Telegram's
setWebhookAPI (not polling) for lower latency. - Streaming responses: The bot sends a typing indicator, then the first text
chunk as a message, followed by progressive
editMessageTextcalls as the AI generates more text. - Eager boot: On server startup, Telegram webhooks are re-registered automatically.
- Duplicate token rejection: Creating a second channel with the same bot token is rejected (409 Conflict) to prevent webhook conflicts.
- Webhook rollback: If webhook registration fails during channel creation, the channel is deleted from the database to avoid inconsistent state.
- Assistant persistence: Compatibility assistant-mode missions are not auto-completed after replies; they stay active for future messages.
- Single webhook owner: Telegram only supports one active webhook per bot. Do not keep the compatibility bridge and Hermes gateway active for the same bot token at the same time.
- Bot token: Stored in the database. Not returned in API responses (masked
via
#[serde(skip_serializing)]). - Webhook secret: Each channel gets a unique webhook secret validated via
X-Telegram-Bot-Api-Secret-Tokenheader. - Chat ID filtering: Use
allowed_chat_idsto restrict which Telegram users/groups can interact with the bot. - File size limit: Telegram file downloads are capped at 50 MB.
Bot not responding?
- Check the gateway is active:
GET /api/control/telegram/bots - Toggle the gateway off and on to re-register the webhook
- Check server logs for
boot_from_storeor webhook registration errors - Ensure
SANDBOXED_PUBLIC_URLis set and publicly accessible
Messages appearing in wrong mission? Telegram messages use parallel execution, ensuring they stay in their own mission context.
Webhook not registered after restart? The server eagerly boots all active Telegram channels on startup. If this fails, any authenticated API call will trigger lazy boot as a fallback.