Merge remote-tracking branch 'origin/main' into bb/gui
# Conflicts: # apps/dashboard/package-lock.json # apps/dashboard/package.json # apps/dashboard/src/components/BottomPickSheet.tsx # apps/dashboard/src/hooks/useBelowBreakpoint.ts # gateway/platforms/telegram.py # hermes_cli/gateway.py # hermes_cli/web_server.py # nix/web.nix # scripts/install.ps1 # tests/gateway/test_telegram_thread_fallback.py # tui_gateway/server.py
This commit is contained in:
@@ -272,6 +272,10 @@ Put the most common workflow first. Edge cases and advanced usage go at the bott
|
||||
|
||||
For XML/JSON parsing or complex logic, include helper scripts in `scripts/` — don't expect the LLM to write parsers inline every time.
|
||||
|
||||
### Deliver media as documents (`[[as_document]]`)
|
||||
|
||||
If your skill produces a high-resolution screenshot, chart, or any image where lossy preview compression would hurt — emit the literal directive `[[as_document]]` somewhere in the response (commonly the last line). The gateway strips the directive and delivers every extracted media path in that response as a downloadable file attachment instead of an inline image bubble. See [Skill output and media delivery](../user-guide/features/skills.md#skill-output-and-media-delivery) for the full semantics.
|
||||
|
||||
#### Referencing bundled scripts from SKILL.md
|
||||
|
||||
When a skill is loaded, the activation message exposes the absolute skill directory as `[Skill directory: /abs/path]` and also substitutes two template tokens anywhere in the SKILL.md body:
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
---
|
||||
sidebar_position: 12
|
||||
title: "Web Search Provider Plugins"
|
||||
description: "How to build a web-search/extract/crawl backend plugin for Hermes Agent"
|
||||
---
|
||||
|
||||
# Building a Web Search Provider Plugin
|
||||
|
||||
Web-search provider plugins register a backend that services `web_search`, `web_extract`, and (optionally) deep-crawl tool calls. Built-in providers — Firecrawl, SearXNG, Tavily, Exa, Parallel, Brave Search (free tier), and DDGS — all ship as plugins under `plugins/web/<name>/`. You can add a new one, or override a bundled one, by dropping a directory next to them.
|
||||
|
||||
:::tip
|
||||
Web search is one of several **backend plugins** Hermes supports. The others (with their own ABCs) are [Image Generation Provider Plugins](/docs/developer-guide/image-gen-provider-plugin), [Video Generation Provider Plugins](/docs/developer-guide/video-gen-provider-plugin), [Memory Provider Plugins](/docs/developer-guide/memory-provider-plugin), [Context Engine Plugins](/docs/developer-guide/context-engine-plugin), and [Model Provider Plugins](/docs/developer-guide/model-provider-plugin). General tool/hook/CLI plugins live in [Build a Hermes Plugin](/docs/guides/build-a-hermes-plugin).
|
||||
:::
|
||||
|
||||
## How discovery works
|
||||
|
||||
Hermes scans for web-search backends in three places:
|
||||
|
||||
1. **Bundled** — `<repo>/plugins/web/<name>/` (auto-loaded with `kind: backend`, always available)
|
||||
2. **User** — `~/.hermes/plugins/web/<name>/` (opt-in via `plugins.enabled` or `hermes plugins enable <name>`)
|
||||
3. **Pip** — packages declaring a `hermes_agent.plugins` entry point
|
||||
|
||||
Each plugin's `register(ctx)` function calls `ctx.register_web_search_provider(...)` — that puts the instance into the registry in `agent/web_search_registry.py`. The active provider for each capability is picked by config:
|
||||
|
||||
| Capability | Config key | Falls back to |
|
||||
|---|---|---|
|
||||
| `web_search` | `web.search_backend` | `web.backend` |
|
||||
| `web_extract` | `web.extract_backend` | `web.backend` |
|
||||
| Deep crawl modes inside `web_extract` | `web.extract_backend` | `web.backend` |
|
||||
|
||||
When neither key is set, Hermes auto-detects the backend from whichever API key/URL is present in the environment. `hermes tools` walks users through selection.
|
||||
|
||||
## Directory structure
|
||||
|
||||
```
|
||||
plugins/web/my-backend/
|
||||
├── __init__.py # register() entry point
|
||||
├── provider.py # WebSearchProvider subclass
|
||||
└── plugin.yaml # Manifest with kind: backend and provides_web_providers
|
||||
```
|
||||
|
||||
`brave_free/` and `ddgs/` are the smallest in-tree references — `brave_free` for an API-key-gated search-only provider, `ddgs` for a no-key provider that lazy-installs its SDK.
|
||||
|
||||
## The WebSearchProvider ABC
|
||||
|
||||
Subclass `agent.web_search_provider.WebSearchProvider`. The only required members are `name`, `is_available()`, and whichever of `search()` / `extract()` / `crawl()` you implement.
|
||||
|
||||
```python
|
||||
# plugins/web/my-backend/provider.py
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from agent.web_search_provider import WebSearchProvider
|
||||
|
||||
|
||||
class MyBackendWebSearchProvider(WebSearchProvider):
|
||||
"""Minimal search-only provider against the My Backend HTTP API."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
# Stable id used in web.search_backend / web.extract_backend / web.backend
|
||||
# config keys. Lowercase, no spaces; hyphens permitted.
|
||||
return "my-backend"
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
# Human label shown in `hermes tools`. Defaults to `name`.
|
||||
return "My Backend"
|
||||
|
||||
def is_available(self) -> bool:
|
||||
# Cheap check — env var present, optional dep importable, etc.
|
||||
# MUST NOT make network calls (runs on every `hermes tools` paint).
|
||||
return bool(os.getenv("MY_BACKEND_API_KEY", "").strip())
|
||||
|
||||
def supports_search(self) -> bool:
|
||||
return True
|
||||
|
||||
def supports_extract(self) -> bool:
|
||||
return False
|
||||
|
||||
def supports_crawl(self) -> bool:
|
||||
return False
|
||||
|
||||
def search(self, query: str, limit: int = 5) -> Dict[str, Any]:
|
||||
import httpx
|
||||
|
||||
api_key = os.environ["MY_BACKEND_API_KEY"]
|
||||
try:
|
||||
resp = httpx.get(
|
||||
"https://api.example.com/search",
|
||||
params={"q": query, "count": max(1, min(int(limit), 20))},
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except httpx.HTTPError as exc:
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
# Response shape is fixed — see "Response shape" below.
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"web": [
|
||||
{
|
||||
"title": item.get("title", ""),
|
||||
"url": item.get("url", ""),
|
||||
"description": item.get("snippet", ""),
|
||||
"position": idx + 1,
|
||||
}
|
||||
for idx, item in enumerate(data.get("results", []))
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# plugins/web/my-backend/__init__.py
|
||||
from plugins.web.my_backend.provider import MyBackendWebSearchProvider
|
||||
|
||||
|
||||
def register(ctx) -> None:
|
||||
"""Plugin entry point — called once at load time."""
|
||||
ctx.register_web_search_provider(MyBackendWebSearchProvider())
|
||||
```
|
||||
|
||||
## plugin.yaml
|
||||
|
||||
```yaml
|
||||
name: web-my-backend
|
||||
version: 1.0.0
|
||||
description: "My Backend web search — Bearer-auth REST API"
|
||||
author: Your Name
|
||||
kind: backend
|
||||
provides_web_providers:
|
||||
- my-backend
|
||||
requires_env:
|
||||
- MY_BACKEND_API_KEY
|
||||
```
|
||||
|
||||
| Key | Purpose |
|
||||
|---|---|
|
||||
| `kind: backend` | Routes the plugin through the backend-loading path |
|
||||
| `provides_web_providers` | List of provider `name`s this plugin registers — used by the loader to advertise the plugin in `hermes tools` even before `register()` runs |
|
||||
| `requires_env` | Interactive credential prompt during `hermes plugins install` (see [Build a Hermes Plugin](/docs/guides/build-a-hermes-plugin#gate-on-environment-variables) for the rich format) |
|
||||
|
||||
## ABC reference
|
||||
|
||||
Full contract in `agent/web_search_provider.py`. Methods you may override:
|
||||
|
||||
| Member | Required | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `name` | ✅ | — | Stable id used in `web.*_backend` config |
|
||||
| `display_name` | — | `name` | Label shown in `hermes tools` |
|
||||
| `is_available()` | ✅ | — | Cheap availability gate — env vars, optional deps |
|
||||
| `supports_search()` | — | `True` | Capability flag for `web_search` routing |
|
||||
| `supports_extract()` | — | `False` | Capability flag for `web_extract` routing |
|
||||
| `supports_crawl()` | — | `False` | Capability flag for deep-crawl modes |
|
||||
| `search(query, limit)` | conditional | raises | Required when `supports_search()` returns `True` |
|
||||
| `extract(urls, **kwargs)` | conditional | raises | Required when `supports_extract()` returns `True` |
|
||||
| `crawl(url, **kwargs)` | conditional | raises | Required when `supports_crawl()` returns `True` |
|
||||
|
||||
Providers can advertise multiple capabilities from a single class — Firecrawl, Tavily, Exa, and Parallel all implement all three of search/extract/crawl. Brave Search and DDGS are search-only; SearXNG is search-only with a documented "pair me with an extract provider" workflow.
|
||||
|
||||
## Response shape
|
||||
|
||||
The tool wrapper expects a fixed envelope so it doesn't have to translate between backends.
|
||||
|
||||
**Search success:**
|
||||
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"data": {
|
||||
"web": [
|
||||
{"title": str, "url": str, "description": str, "position": int},
|
||||
...
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Extract success:**
|
||||
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"data": [
|
||||
{
|
||||
"url": str,
|
||||
"title": str,
|
||||
"content": str,
|
||||
"raw_content": str,
|
||||
"metadata": dict, # optional
|
||||
"error": str, # optional, only on per-URL failure
|
||||
},
|
||||
...
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**Either capability, on failure:**
|
||||
|
||||
```python
|
||||
{"success": False, "error": "human-readable message"}
|
||||
```
|
||||
|
||||
Both `search()` and `extract()` may be `async def` — the dispatcher detects coroutine functions via `inspect.iscoroutinefunction` and awaits accordingly. Sync implementations that do blocking I/O (HTTP, SDK calls) are fine for small backends; the dispatcher handles threading.
|
||||
|
||||
## Capability flags
|
||||
|
||||
Hermes routes calls to the right provider based on the `supports_*` flags. A common multi-provider setup:
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/config.yaml
|
||||
web:
|
||||
search_backend: "brave-free" # search-only, fast, free 2k/mo
|
||||
extract_backend: "firecrawl" # extract + crawl, paid quota
|
||||
```
|
||||
|
||||
When `web.search_backend` or `web.extract_backend` aren't set, both fall through to `web.backend`. When that's also unset, Hermes picks the first available provider that supports the requested capability based on env-var presence.
|
||||
|
||||
If your provider only supports one capability, leave the other flags at their default (`False`) and the registry will skip it for that tool — users won't see misleading "provider X failed" errors when they're using X only for search and asking the agent to extract.
|
||||
|
||||
## How Hermes wires it into the tools
|
||||
|
||||
The `web_search` and `web_extract` tools live in `tools/web_tools.py`. At call time they:
|
||||
|
||||
1. Read the relevant config key (`web.search_backend` for `web_search`, `web.extract_backend` for `web_extract`)
|
||||
2. Ask the registry for the provider with that `name`
|
||||
3. Check `is_available()` and the matching `supports_*()` flag
|
||||
4. Dispatch to `search()` / `extract()` / `crawl()`, awaiting if the method is a coroutine
|
||||
5. JSON-serialize the response envelope and hand it back to the LLM
|
||||
|
||||
Errors surface as the tool result; the LLM decides how to explain them. If no provider is registered (or every available one fails the capability gate), the tool returns a helpful error pointing at `hermes tools`.
|
||||
|
||||
## Lazy-installing optional dependencies
|
||||
|
||||
If your provider wraps a third-party SDK (like DDGS does with the `ddgs` package), don't `import` it at module top level. Use `tools.lazy_deps.ensure(...)` inside `is_available()` or `search()` — Hermes will install the package on first use, gated by `security.allow_lazy_installs`. See [Build a Hermes Plugin → Lazy-install](/docs/guides/build-a-hermes-plugin#lazy-install-optional-python-dependencies) for the security model.
|
||||
|
||||
## Reference implementations
|
||||
|
||||
- **`plugins/web/brave_free/`** — small, API-key-gated, search-only HTTP provider. Good starting template.
|
||||
- **`plugins/web/ddgs/`** — no-key provider that lazy-installs its SDK. Useful pattern for backends that wrap a Python package.
|
||||
- **`plugins/web/firecrawl/`** — full multi-capability provider (search + extract + crawl) with multiple format modes.
|
||||
- **`plugins/web/searxng/`** — self-hosted, URL-configured backend with no auth.
|
||||
- **`plugins/web/xai/`** — LLM-backed search via Grok's server-side `web_search` tool. Shows how to reuse an existing OAuth/env-var credential surface (`tools/xai_http.py`) without adding new env vars, and how to write a cheap `is_available()` that honors the no-network contract.
|
||||
|
||||
## Distribute via pip
|
||||
|
||||
```toml
|
||||
# pyproject.toml
|
||||
[project.entry-points."hermes_agent.plugins"]
|
||||
my-backend-web = "my_backend_web_package"
|
||||
```
|
||||
|
||||
`my_backend_web_package` must expose a top-level `register` function. See [Distribute via pip](/docs/guides/build-a-hermes-plugin#distribute-via-pip) in the general plugin guide for the full setup.
|
||||
|
||||
## Related pages
|
||||
|
||||
- [Web Search](/docs/user-guide/features/web-search) — user-facing feature documentation and per-backend configuration
|
||||
- [Plugins overview](/docs/user-guide/features/plugins) — all plugin types at a glance
|
||||
- [Build a Hermes Plugin](/docs/guides/build-a-hermes-plugin) — general tools/hooks/slash commands guide
|
||||
@@ -37,7 +37,7 @@ Native Windows support is **early beta**. It installs and works for the common p
|
||||
Open PowerShell and run:
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex
|
||||
iex (irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1)
|
||||
```
|
||||
|
||||
The installer handles **everything**: `uv`, Python 3.11, Node.js 22, `ripgrep`, `ffmpeg`, **and a portable Git Bash** (PortableGit — a self-contained Git-for-Windows distribution that ships `bash.exe` and the full POSIX toolchain Hermes uses for shell commands; on 32-bit Windows the installer falls back to MinGit, which lacks bash and disables terminal-tool / agent-browser features). It clones the repo under `%LOCALAPPDATA%\hermes\hermes-agent`, creates a virtualenv, and adds `hermes` to your **User PATH**. Restart your terminal (or open a new PowerShell window) after the install so PATH picks up.
|
||||
@@ -52,6 +52,8 @@ The installer also sets `HERMES_GIT_BASH_PATH` to the located `bash.exe` so Herm
|
||||
|
||||
If you prefer WSL2, the Linux installer above works inside it; both native and WSL installs can coexist without conflict (native data lives under `%LOCALAPPDATA%\hermes`, WSL data lives under `~/.hermes`).
|
||||
|
||||
**Desktop installer (alternative):** A thin GUI installer is also available — download Hermes Desktop, run the `.exe`, and on first launch it calls `install.ps1` under the hood to provision Python (via `uv`), Node, PortableGit, and the rest of the dependencies. The desktop app and the PowerShell-installed CLI share the same install and data directories, so you can use either or both. See the [Windows (Native) guide](../user-guide/windows-native#desktop-installer-alternative) for details.
|
||||
|
||||
### Android / Termux
|
||||
|
||||
Hermes now ships a Termux-aware installer path too:
|
||||
@@ -180,3 +182,7 @@ The same pattern works on Arch (the installer uses pacman with the same sudo-det
|
||||
| Missing config after update | Run `hermes config check` then `hermes config migrate` |
|
||||
|
||||
For more diagnostics, run `hermes doctor` — it will tell you exactly what's missing and how to fix it.
|
||||
|
||||
## Install method auto-detection
|
||||
|
||||
Hermes auto-detects whether it was installed via `pip`, the git installer, Homebrew, or NixOS, and `hermes update` prints the matching update command for that path. There's no env var to set — the detection is based on the install layout (Python site-packages, `~/.hermes/hermes-agent/`, Homebrew prefix, or Nix store path). `hermes doctor` also surfaces the detected method under its environment summary.
|
||||
|
||||
@@ -69,6 +69,26 @@ updates:
|
||||
|
||||
`--backup` was the always-on behavior in earlier builds, but it was adding minutes to every update on large homes, so it's now opt-in. The lightweight pairing-data snapshot above still runs unconditionally.
|
||||
|
||||
### Windows: another `hermes.exe` is running
|
||||
|
||||
On Windows, `hermes update` will refuse to run if it detects another `hermes.exe` process holding the venv's entry-point executable open — most commonly the Hermes Desktop app's spawned backend, an open `hermes` REPL in another terminal, or a running gateway:
|
||||
|
||||
```
|
||||
$ hermes update
|
||||
✗ Another hermes.exe is running:
|
||||
PID 12345 hermes.exe
|
||||
|
||||
Updating now would fail to overwrite ...\venv\Scripts\hermes.exe because
|
||||
Windows blocks REPLACE on a running executable.
|
||||
|
||||
Close Hermes Desktop, exit any open `hermes` REPLs, and
|
||||
stop the gateway (`hermes gateway stop`) before retrying.
|
||||
Override with `hermes update --force` if you've already
|
||||
confirmed those processes will not write to the venv.
|
||||
```
|
||||
|
||||
Close the listed processes and re-run. If you're sure the concurrent process won't interfere (rare — usually only useful when an antivirus shim is mis-attributed), pass `--force` to skip the check. In that case the updater will still retry the `.exe` rename with exponential backoff and, on stubborn locks, schedule the replacement for next reboot via `MoveFileEx(MOVEFILE_DELAY_UNTIL_REBOOT)` so the update can complete.
|
||||
|
||||
Expected output looks like:
|
||||
|
||||
```
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
---
|
||||
sidebar_position: 15
|
||||
title: "Azure AI Foundry"
|
||||
description: "Use Hermes Agent with Azure AI Foundry — OpenAI-style and Anthropic-style endpoints, auto-detection of transport and deployed models"
|
||||
title: "Microsoft Foundry"
|
||||
description: "Use Hermes Agent with Microsoft Foundry — OpenAI-style and Anthropic-style endpoints, auto-detection of transport and deployed models"
|
||||
---
|
||||
|
||||
# Azure AI Foundry
|
||||
# Microsoft Foundry
|
||||
|
||||
Hermes Agent supports Azure AI Foundry (and Azure OpenAI) as a first-class provider. A single Azure resource can host models with two different wire formats:
|
||||
Hermes Agent's `azure-foundry` provider supports Microsoft Foundry (formerly Azure AI Foundry) and Azure OpenAI. A single Foundry resource can host models with two different wire formats:
|
||||
|
||||
- **OpenAI-style** — `POST /v1/chat/completions` on endpoints like `https://<resource>.openai.azure.com/openai/v1`. Used for GPT-4.x, GPT-5.x, Llama, Mistral, and most open-weight models.
|
||||
- **Anthropic-style** — `POST /v1/messages` on endpoints like `https://<resource>.services.ai.azure.com/anthropic`. Used when Azure Foundry serves Claude models via the Anthropic Messages API format.
|
||||
- **Anthropic-style** — `POST /v1/messages` on endpoints like `https://<resource>.services.ai.azure.com/anthropic`. Used when Microsoft Foundry serves Claude models via the Anthropic Messages API format.
|
||||
|
||||
The setup wizard probes your endpoint and auto-detects which transport it uses, which deployments are available, and each model's context length.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An Azure AI Foundry or Azure OpenAI resource with at least one deployment
|
||||
- An API key for that resource (available in the Azure Portal under "Keys and Endpoint")
|
||||
- A Microsoft Foundry or Azure OpenAI resource with at least one deployment
|
||||
- The deployment's endpoint URL
|
||||
- **Either** an API key (from the Azure Portal under "Keys and Endpoint") **or** the **Azure AI User** RBAC role on the Foundry resource if you plan to use Microsoft Entra ID (the keyless path Microsoft recommends). Some tenants may show the role as **Foundry User** during Microsoft's rename rollout.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -25,20 +25,173 @@ The setup wizard probes your endpoint and auto-detects which transport it uses,
|
||||
hermes model
|
||||
# → Select "Azure Foundry"
|
||||
# → Enter your endpoint URL
|
||||
# → Enter your API key
|
||||
# → Choose Authentication:
|
||||
# 1. API key
|
||||
# 2. Microsoft Entra ID (managed identity / workload identity / az login)
|
||||
# → (Entra) Hermes probes DefaultAzureCredential; on success it never asks for a key
|
||||
# → (API key) Enter your API key
|
||||
# Hermes probes the endpoint and auto-detects transport + models
|
||||
# → Pick a model from the list (or type a deployment name manually)
|
||||
```
|
||||
|
||||
The wizard will:
|
||||
|
||||
1. **Sniff the URL path** — URLs ending in `/anthropic` are recognised as Azure Foundry Claude routes.
|
||||
1. **Sniff the URL path** — URLs ending in `/anthropic` are recognised as Microsoft Foundry Claude routes.
|
||||
2. **Probe `GET <base>/models`** — if the endpoint returns an OpenAI-shaped model list, Hermes switches to `chat_completions` and prefills a picker with the returned deployment IDs.
|
||||
3. **Probe Anthropic Messages shape** — fallback for endpoints that do not expose `/models` but do accept the Anthropic Messages format.
|
||||
4. **Fall back to manual entry** — private/gated endpoints that reject every probe still work; you pick the API mode and type a deployment name by hand.
|
||||
|
||||
Context length for the chosen model is resolved via Hermes' standard metadata chain (`models.dev`, provider metadata, and hardcoded family fallbacks) and stored in `config.yaml` so the model can size its own context window correctly.
|
||||
|
||||
## Microsoft Entra ID (keyless, RBAC) — recommended
|
||||
|
||||
Microsoft recommends [keyless authentication with Microsoft Entra ID](https://learn.microsoft.com/azure/ai-foundry/foundry-models/how-to/configure-entra-id) for production Foundry workloads. Hermes supports Entra ID for **both** API surfaces:
|
||||
|
||||
- **OpenAI-style** (`api_mode: chat_completions` / `codex_responses`) — GPT-4/5, Llama, Mistral, DeepSeek, etc.
|
||||
- **Anthropic-style** (`api_mode: anthropic_messages`) — Claude models on Microsoft Foundry.
|
||||
|
||||
Foundry's RBAC is per-resource (`Azure AI User` grants both surfaces; some tenants may display `Foundry User`) and Microsoft documents the same inference scope (`https://ai.azure.com/.default`) for both. Under the hood:
|
||||
|
||||
- OpenAI-style uses the OpenAI Python SDK's native callable `api_key=` contract — the SDK mints a fresh JWT per request automatically.
|
||||
- Anthropic-style uses an `httpx.Client` with a request event hook installed by `agent.azure_identity_adapter.build_bearer_http_client`, because the Anthropic SDK does not accept callable `auth_token` natively. The hook rewrites `Authorization: Bearer <fresh-jwt>` per outbound request. Same Microsoft RBAC, same Foundry scope — the SDK contract is the only difference.
|
||||
|
||||
### Why use Entra ID?
|
||||
|
||||
- No long-lived API keys to rotate or revoke.
|
||||
- RBAC-driven access — grant or remove `Azure AI User` on the Foundry resource, no config rewrite needed.
|
||||
- Access and audit logs are segmented by assignee instead of all callers sharing one static key.
|
||||
- Single auth surface for Azure VMs, AKS pods, App Service, Functions, Container Apps, and Foundry Agent Service via managed identity.
|
||||
- Workload identity and service-principal flows for CI/CD pipelines.
|
||||
|
||||
### One-time setup (Azure side)
|
||||
|
||||
1. In the Azure Portal, open your Foundry resource → **Access control (IAM)** → **Add → Add role assignment**.
|
||||
2. Pick the **Azure AI User** role (or **Foundry User** if your tenant has the renamed role).
|
||||
3. Assign it to:
|
||||
- **Your user account** for local development with `az login`.
|
||||
- **A managed identity or workload identity** for Azure-hosted compute (recommended for production).
|
||||
- **A Foundry Agent Service hosted agent's agent identity** when Hermes runs inside a hosted agent.
|
||||
- **A service principal** for CI/CD pipelines when workload identity is not available.
|
||||
4. Wait ~5 minutes for the role to propagate.
|
||||
|
||||
Azure CLI equivalent:
|
||||
|
||||
```bash
|
||||
az role assignment create \
|
||||
--assignee <principal-or-agent-identity-client-id> \
|
||||
--role "Azure AI User" \
|
||||
--scope <foundry-resource-id>
|
||||
```
|
||||
|
||||
### One-time setup (Hermes side)
|
||||
|
||||
```bash
|
||||
hermes model
|
||||
# → Select "Azure Foundry"
|
||||
# → Enter your endpoint URL
|
||||
# → Authentication: 2 (Microsoft Entra ID)
|
||||
# → (optional) user-assigned managed identity client ID
|
||||
# → (optional) Azure tenant ID
|
||||
# → Hermes probes DefaultAzureCredential() and reports which inner
|
||||
# credential succeeded (e.g. AzureCliCredential, ManagedIdentityCredential)
|
||||
```
|
||||
|
||||
The wizard runs a bounded preflight probe (10 s timeout). On failure it offers to "save anyway, validate later" — useful when configuring on a machine that doesn't yet have credentials but will at runtime (e.g. preparing config for a managed-identity deployment).
|
||||
|
||||
`azure-identity` is installed automatically on first use via Hermes' lazy-install path. To pre-install:
|
||||
|
||||
```bash
|
||||
pip install azure-identity
|
||||
```
|
||||
|
||||
### Configuration written to `config.yaml`
|
||||
|
||||
```yaml
|
||||
model:
|
||||
provider: azure-foundry
|
||||
base_url: https://my-resource.openai.azure.com/openai/v1
|
||||
api_mode: chat_completions
|
||||
auth_mode: entra_id
|
||||
default: gpt-4o
|
||||
context_length: 128000
|
||||
entra:
|
||||
scope: https://ai.azure.com/.default # only when overriding the default
|
||||
```
|
||||
|
||||
Hermes only manages one Entra-specific knob in `config.yaml`:
|
||||
|
||||
- **`scope`** — the OAuth resource scope. Defaults to Microsoft's documented inference scope (`https://ai.azure.com/.default`). Override only if your resource was provisioned against a non-standard audience.
|
||||
|
||||
Everything else (tenant, service principal secret, federated token file, sovereign cloud authority, broker preferences) is read by `azure-identity` directly from the standard `AZURE_*` environment variables — see the [credential resolution order](#credential-resolution-order) below. Set those in `~/.hermes/.env` or your deployment environment, exactly as Microsoft's SDK reference describes.
|
||||
|
||||
No secrets land in `~/.hermes/.env` for Entra mode — `azure-identity` caches tokens in-process (and where available, in your OS keychain / `~/.IdentityService`).
|
||||
|
||||
### Credential resolution order
|
||||
|
||||
`azure-identity`'s `DefaultAzureCredential` walks this chain on each token request, stopping at the first credential that returns a token:
|
||||
|
||||
1. **Environment credential** — `AZURE_TENANT_ID` + `AZURE_CLIENT_ID` + `AZURE_CLIENT_SECRET` (or `AZURE_CLIENT_CERTIFICATE_PATH` / `AZURE_FEDERATED_TOKEN_FILE`).
|
||||
2. **Workload Identity** — `AZURE_FEDERATED_TOKEN_FILE` (AKS federated tokens / OIDC).
|
||||
3. **Managed Identity** — IMDS endpoint (`169.254.169.254`) for virtual machines; `IDENTITY_ENDPOINT` for App Service / Functions / Container Apps. Foundry Agent Service hosted agents use the hosted agent's agent identity.
|
||||
4. **Visual Studio Code** — Azure account extension.
|
||||
5. **Azure CLI** — `az login` session.
|
||||
6. **Azure Developer CLI** — `azd auth login`.
|
||||
7. **Azure PowerShell** — `Connect-AzAccount`.
|
||||
8. **Broker** (Windows / WSL only) — Web Account Manager.
|
||||
|
||||
Interactive browser credential is excluded by default for unattended Hermes runs; use Azure CLI, Azure Developer CLI, managed identity, workload identity, or service principal credentials instead.
|
||||
|
||||
### Deployment patterns
|
||||
|
||||
**Local development:**
|
||||
```bash
|
||||
az login
|
||||
hermes model # pick Azure Foundry → Entra ID
|
||||
hermes # uses your az login token
|
||||
```
|
||||
|
||||
**Azure VM / Functions / App Service / Container Apps (system-assigned managed identity):**
|
||||
1. Enable system-assigned identity on the compute resource.
|
||||
2. Grant the identity `Azure AI User` (or `Foundry User`) on the Foundry resource.
|
||||
3. Set `model.auth_mode: entra_id` in config.yaml — no env vars needed.
|
||||
|
||||
**Azure VM / Functions / App Service / Container Apps (user-assigned managed identity):**
|
||||
- Set `AZURE_CLIENT_ID` to the user-assigned identity's client ID so `DefaultAzureCredential` picks the right one.
|
||||
|
||||
**Foundry Agent Service hosted agent:**
|
||||
- Create the hosted agent and grant that agent's identity `Azure AI User` (or `Foundry User`) on the Foundry resource. Hermes uses `ManagedIdentityCredential` from inside the hosted agent; role assignment belongs on the agent identity, not just the parent project or your user.
|
||||
|
||||
**AKS Workload Identity (replaces AAD Pod Identity):**
|
||||
- Annotate the pod's service account with the workload identity client ID.
|
||||
- The pod's federated token file is auto-detected via `AZURE_FEDERATED_TOKEN_FILE`.
|
||||
- `model.auth_mode: entra_id` works without further config changes.
|
||||
|
||||
**Service principal in CI:**
|
||||
- Set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` in the runner env.
|
||||
|
||||
#### Sovereign clouds (Government, China)
|
||||
|
||||
Export `AZURE_AUTHORITY_HOST` (e.g. `https://login.microsoftonline.us` for Azure Government, `https://login.partner.microsoftonline.cn` for Azure China). `azure-identity` reads it directly.
|
||||
|
||||
### Health checks
|
||||
|
||||
`hermes doctor` runs a 10 s probe against `DefaultAzureCredential` when `model.auth_mode: entra_id`, reporting which inner credential won (env vars present, managed identity endpoint reachable, etc.).
|
||||
|
||||
`hermes auth` shows a structured status block:
|
||||
|
||||
```
|
||||
azure-foundry (Microsoft Entra ID):
|
||||
Endpoint: https://my-resource.openai.azure.com/openai/v1
|
||||
Scope: https://ai.azure.com/.default
|
||||
Status: configured; live token probe is skipped here
|
||||
```
|
||||
|
||||
### Limitations
|
||||
|
||||
- **Anthropic-style endpoints use an httpx event hook.** The Anthropic Python SDK does not accept a callable `auth_token` natively (≤ 0.86.0). Hermes installs a request event hook on a custom `httpx.Client` that mints a fresh JWT per outbound request and rewrites `Authorization: Bearer <jwt>`. This is functionally equivalent to the OpenAI SDK's native `Callable[[], str]` contract but adds one indirection layer. If the Anthropic SDK adds first-class callable-auth support in a future release, Hermes will switch to it transparently.
|
||||
- **Batch jobs and `multiprocessing.Pool`.** The Entra token provider is a closure that cannot be pickled across process boundaries. `batch_runner.py` automatically drops the callable from the worker config and lets each worker process rebuild its own provider from `config.yaml` — no user action required, but each worker pays one chain walk at startup.
|
||||
- **No bearer JWT persistence in `auth.json`.** Hermes does not duplicate `azure-identity`'s internal token cache; cold starts walk the credential chain on first inference.
|
||||
|
||||
## Configuration (written to `config.yaml`)
|
||||
|
||||
After running the wizard you'll see something like this:
|
||||
@@ -72,11 +225,11 @@ model:
|
||||
|
||||
Important behaviour:
|
||||
|
||||
- **GPT-5.x, codex, and o-series auto-route to the Responses API.** Azure Foundry deploys GPT-5 / codex / o1 / o3 / o4 models as Responses-API-only — calling `/chat/completions` against them returns `400 "The requested operation is unsupported."`. Hermes detects these model families by name and upgrades `api_mode` to `codex_responses` transparently, even when `config.yaml` still reads `api_mode: chat_completions`. GPT-4, GPT-4o, Llama, Mistral, and other deployments stay on `/chat/completions`.
|
||||
- **GPT-5.x, codex, and o-series auto-route to the Responses API.** Microsoft Foundry deploys GPT-5 / codex / o1 / o3 / o4 models as Responses-API-only — calling `/chat/completions` against them returns `400 "The requested operation is unsupported."`. Hermes detects these model families by name and upgrades `api_mode` to `codex_responses` transparently, even when `config.yaml` still reads `api_mode: chat_completions`. GPT-4, GPT-4o, Llama, Mistral, and other deployments stay on `/chat/completions`.
|
||||
- **`max_completion_tokens` is used automatically.** Azure OpenAI (like direct OpenAI) requires `max_completion_tokens` for gpt-4o, o-series, and gpt-5.x models. Hermes sends the right parameter based on the endpoint.
|
||||
- **Pre-v1 endpoints that require `api-version`.** If you have a legacy base URL like `https://<resource>.openai.azure.com/openai?api-version=2025-04-01-preview`, Hermes extracts the query string and forwards it via `default_query` on every request (the OpenAI SDK otherwise drops it when joining paths).
|
||||
|
||||
## Anthropic-style endpoints (Claude via Azure Foundry)
|
||||
## Anthropic-style endpoints (Claude via Microsoft Foundry)
|
||||
|
||||
For Claude deployments, use the Anthropic-style route:
|
||||
|
||||
@@ -92,11 +245,13 @@ Important behaviour:
|
||||
|
||||
- **`/v1` is stripped from the base URL.** The Anthropic SDK appends `/v1/messages` to every request URL — Hermes removes any trailing `/v1` before handing the URL to the SDK to avoid double-`/v1` paths.
|
||||
- **`api-version` is sent via `default_query`, not appended to the URL.** Azure Anthropic requires an `api-version` query string. Baking it into the base URL produces malformed paths like `/anthropic?api-version=.../v1/messages` and returns 404. Hermes passes `api-version=2025-04-15` via the Anthropic SDK's `default_query` instead.
|
||||
- **Bearer auth is used instead of `x-api-key`.** Azure's Anthropic-compatible route requires `Authorization: Bearer <key>` rather than Anthropic's native `x-api-key` header. Hermes detects `azure.com` in the base URL and routes the API key through the SDK's `auth_token` field so the right header reaches the upstream.
|
||||
- **1M context window beta header is kept.** Azure still gates the 1M-token Claude context (Opus 4.6/4.7, Sonnet 4.6) behind the `anthropic-beta: context-1m-2025-08-07` header. Hermes keeps that beta header on Azure paths (it's stripped from native Anthropic OAuth requests because some subscriptions reject it, but Azure requires it).
|
||||
- **OAuth token refresh is disabled.** Azure deployments use static API keys. The `~/.claude/.credentials.json` OAuth token refresh loop that applies to Anthropic Console is explicitly skipped for Azure endpoints to prevent the Claude Code OAuth token from overwriting your Azure key mid-session.
|
||||
|
||||
## Alternative: `provider: anthropic` + Azure base URL
|
||||
|
||||
If you already have `provider: anthropic` configured and just want to point it at Azure AI Foundry for Claude, you can skip the `azure-foundry` provider entirely:
|
||||
If you already have `provider: anthropic` configured and just want to point it at Microsoft Foundry for Claude, you can skip the `azure-foundry` provider entirely:
|
||||
|
||||
```yaml
|
||||
model:
|
||||
@@ -117,7 +272,7 @@ Azure does **not** expose a pure-API-key endpoint to list your *deployed* model
|
||||
What Hermes can do:
|
||||
|
||||
- Azure OpenAI v1 endpoints (`<resource>.openai.azure.com/openai/v1`) expose `GET /models` with the resource's **available** model catalog. Hermes uses this list to prefill the model picker.
|
||||
- Azure Foundry `/anthropic` routes: detected via URL path, model name entered manually.
|
||||
- Microsoft Foundry `/anthropic` routes: detected via URL path, model name entered manually.
|
||||
- Private / firewalled endpoints: manual entry with a friendly "couldn't probe" message.
|
||||
|
||||
You can always type a deployment name directly — Hermes does not validate against the returned list.
|
||||
@@ -126,9 +281,18 @@ You can always type a deployment name directly — Hermes does not validate agai
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `AZURE_FOUNDRY_API_KEY` | Primary API key for Azure AI Foundry / Azure OpenAI |
|
||||
| `AZURE_FOUNDRY_API_KEY` | Primary API key for Microsoft Foundry / Azure OpenAI (api_key mode) |
|
||||
| `AZURE_FOUNDRY_BASE_URL` | Endpoint URL (set via `hermes model`; env var is used as a fallback) |
|
||||
| `AZURE_ANTHROPIC_KEY` | Used by `provider: anthropic` + Azure base URL (alternative to `ANTHROPIC_API_KEY`) |
|
||||
| `AZURE_TENANT_ID` | Entra ID tenant for service-principal flows |
|
||||
| `AZURE_CLIENT_ID` | Entra ID client ID (service principal, workload identity, or user-assigned managed identity) |
|
||||
| `AZURE_CLIENT_SECRET` | Service principal secret |
|
||||
| `AZURE_CLIENT_CERTIFICATE_PATH` | Service principal cert (alternative to secret) |
|
||||
| `AZURE_FEDERATED_TOKEN_FILE` | Workload Identity federated token path (AKS) |
|
||||
| `AZURE_AUTHORITY_HOST` | Sovereign cloud authority host override |
|
||||
| `IDENTITY_ENDPOINT` / `MSI_ENDPOINT` | Managed Identity endpoint for App Service, Functions, and Container Apps; VMs usually use IMDS instead |
|
||||
|
||||
The Azure SDK reads the `AZURE_*` env vars directly. Hermes never inspects them other than to report which sources are present in `hermes doctor` output.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -150,8 +314,21 @@ model:
|
||||
api_mode: anthropic_messages # or chat_completions
|
||||
```
|
||||
|
||||
**Entra ID: "credential chain exhausted" or 401 Unauthorized after switching to `auth_mode: entra_id`.**
|
||||
- Run `az login` to refresh your developer session (the cached token may have expired).
|
||||
- Verify the `Azure AI User` (or `Foundry User`) role assignment took effect: `az role assignment list --assignee <user-or-identity-id>` should list it on your Foundry resource. Role propagation can take up to 5 minutes.
|
||||
- For user-assigned managed identities, double-check `AZURE_CLIENT_ID` matches the identity attached to the compute resource.
|
||||
- Run `hermes doctor` — the Azure Entra probe reports whether token acquisition succeeded and includes a remediation hint.
|
||||
|
||||
**Entra ID: wizard preflight hangs or times out.**
|
||||
The 10 s preflight is a soft check. Choose "Save anyway and validate later" and run `hermes doctor` after deploying to the target environment. Common causes include an unreachable token service or stale local login state — prefer workload identity in CI, set `AZURE_TENANT_ID`+`AZURE_CLIENT_ID`+`AZURE_CLIENT_SECRET` when using a service principal, or run `az login` for local development.
|
||||
|
||||
**401 on Anthropic-style endpoint with Entra ID.**
|
||||
Verify the same `Azure AI User` (or `Foundry User`) role is assigned on the Foundry resource (it covers both `/openai/v1` and `/anthropic` paths). If the OpenAI-style probe works during the wizard but `claude-*` requests fail at runtime, the most common cause is a stale `model.entra.scope` left over from an earlier wizard run — delete the `entra.scope` line from `config.yaml` so the runtime falls back to the default `https://ai.azure.com/.default` scope.
|
||||
|
||||
## Related
|
||||
|
||||
- [Environment variables](/docs/reference/environment-variables)
|
||||
- [Configuration](/docs/user-guide/configuration)
|
||||
- [AWS Bedrock](/docs/guides/aws-bedrock) — the other major cloud provider integration
|
||||
- [Microsoft: Configure Entra ID for Foundry](https://learn.microsoft.com/azure/ai-foundry/foundry-models/how-to/configure-entra-id) — upstream documentation for the keyless path
|
||||
|
||||
@@ -452,6 +452,37 @@ requires_env:
|
||||
|
||||
Both formats can be mixed in the same list. Already-set variables are skipped silently.
|
||||
|
||||
### Lazy-install optional Python dependencies
|
||||
|
||||
If your plugin wraps an SDK that not every user will have installed (a vendor SDK, a heavy ML lib, a platform-specific package), don't `import` it at the top of the module. Use the `tools.lazy_deps.ensure(...)` helper inside the tool handler — Hermes will install the package on first use, gated by the user's `security.allow_lazy_installs` config.
|
||||
|
||||
```python
|
||||
# tools.py
|
||||
from tools.lazy_deps import ensure, FeatureUnavailable
|
||||
|
||||
def my_tool_handler(args, **kwargs):
|
||||
try:
|
||||
ensure("my-plugin.my-backend") # key must be in LAZY_DEPS
|
||||
except FeatureUnavailable as exc:
|
||||
return {"error": str(exc)}
|
||||
|
||||
import my_backend_sdk # safe now
|
||||
...
|
||||
```
|
||||
|
||||
Two rules from the security model in `tools/lazy_deps.py`:
|
||||
|
||||
| Rule | Why |
|
||||
|---|---|
|
||||
| Your feature key must appear in the in-tree `LAZY_DEPS` allowlist | Prevents a malicious config from coaxing Hermes into installing arbitrary packages — only specs Hermes itself ships are eligible |
|
||||
| Specs are PyPI-by-name only | No `--index-url`, `git+https://`, or file: paths. Pin versions with PEP 440 (`"my-sdk>=1.2,<2"`) inside the allowlist entry |
|
||||
|
||||
For third-party plugins distributed via pip, declare the optional deps as `[project.optional-dependencies]` extras in your own `pyproject.toml` and tell users to `pip install your-plugin[backend]` — that path doesn't go through `lazy_deps`. The lazy-install dance is most useful for **bundled** plugins where shipping a hard dependency on every install would bloat the base Hermes footprint.
|
||||
|
||||
When `security.allow_lazy_installs: false` is set globally, `ensure()` raises `FeatureUnavailable` immediately with a remediation hint — your plugin should catch it and degrade gracefully (return an error result, not crash the tool loop).
|
||||
|
||||
|
||||
|
||||
### Conditional tool availability
|
||||
|
||||
For tools that depend on optional libraries:
|
||||
|
||||
@@ -10,6 +10,7 @@ Sometimes you already know exactly what message you want to send. You don't need
|
||||
|
||||
Hermes calls this **no-agent mode**. It's the cron system minus the LLM.
|
||||
|
||||
<!-- ascii-guard-ignore -->
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ scheduler tick │ every │ run script │
|
||||
@@ -23,6 +24,7 @@ Hermes calls this **no-agent mode**. It's the cron system minus the LLM.
|
||||
│ (telegram/disc…) │
|
||||
└──────────────────┘
|
||||
```
|
||||
<!-- ascii-guard-ignore-end -->
|
||||
|
||||
- **No LLM call.** Zero tokens, zero agent loop, zero model spend.
|
||||
- **Script is the job.** The script decides whether to alert. Emit output → message gets sent. Emit nothing → silent tick.
|
||||
|
||||
@@ -180,7 +180,9 @@ Both models support up to 200,000 tokens of context.
|
||||
|
||||
Hermes refreshes the token on every session start if it is within 60 seconds of expiry. If the access token is already expired (for example, after a long offline period), the refresh happens automatically on the next request. If refresh fails with `refresh_token_reused` or `invalid_grant`, Hermes marks the session as requiring re-login.
|
||||
|
||||
**Fix:** run `hermes auth add minimax-oauth` again to start a fresh login.
|
||||
When the refresh failure is terminal (HTTP 4xx, `invalid_grant`, revoked grant, etc.), Hermes marks the refresh token as dead and quarantines it locally so it doesn't keep replaying the doomed exchange. The agent surfaces a single "re-authentication required" message and stays out of the way until you log in again.
|
||||
|
||||
**Fix:** run `hermes auth add minimax-oauth` again to start a fresh login. The quarantine clears on the next successful exchange.
|
||||
|
||||
### Authorization timed out
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Some Hermes providers — currently **xAI Grok OAuth** and **Spotify** — use a
|
||||
|
||||
This works perfectly when Hermes and your browser are on the same machine. It breaks the moment they aren't: your laptop's browser tries to reach `127.0.0.1` on **your laptop**, but the listener is bound to `127.0.0.1` on **the remote server**.
|
||||
|
||||
The fix is a one-line SSH local-forward.
|
||||
The fix is a one-line SSH local-forward — **or**, when you don't have a real SSH client (GCP Cloud Shell, GitHub Codespaces, EC2 Instance Connect, Gitpod, browser-based web IDEs), the new `--manual-paste` flag introduced in [#26923](https://github.com/NousResearch/hermes-agent/issues/26923).
|
||||
|
||||
## TL;DR
|
||||
|
||||
@@ -27,6 +27,23 @@ hermes auth add xai-oauth --no-browser
|
||||
|
||||
Port `56121` is what xAI OAuth uses. For Spotify, replace it with `43827`. Hermes prints the exact port it bound to on the `Waiting for callback on ...` line — copy it from there.
|
||||
|
||||
## Browser-only remote (Cloud Shell / Codespaces / EC2 Instance Connect)
|
||||
|
||||
If you don't have a regular SSH client — for example because you're running Hermes inside GCP Cloud Shell, GitHub Codespaces, AWS EC2 Instance Connect, Gitpod, or another browser-based console — the SSH tunnel above isn't available. Use `--manual-paste` instead:
|
||||
|
||||
```bash
|
||||
hermes auth add xai-oauth --manual-paste
|
||||
# → Hermes prints an authorize URL. Open it in a browser on your laptop.
|
||||
# → Approve in the browser. The redirect to 127.0.0.1:56121/callback fails
|
||||
# to load — that's expected.
|
||||
# → Copy the FULL URL from the failed page's address bar.
|
||||
# → Paste it back into the terminal at the "Callback URL:" prompt.
|
||||
```
|
||||
|
||||
The same flag works on `hermes model --manual-paste` for the integrated model picker. A bare `?code=...&state=...` query fragment is accepted too if you don't want to paste the whole URL.
|
||||
|
||||
Hermes uses the **same PKCE verifier, state and nonce** for both paths, so the upstream OAuth flow is byte-identical — `--manual-paste` is purely a transport change for the callback hop and is not a security downgrade.
|
||||
|
||||
## Which Providers Need This
|
||||
|
||||
| Provider | Loopback port | Tunnel needed? |
|
||||
|
||||
@@ -24,7 +24,7 @@ The same OAuth bearer token is also reused by every direct-to-xAI surface in Her
|
||||
| Endpoint | `https://api.x.ai/v1` |
|
||||
| Auth server | `https://accounts.x.ai` |
|
||||
| Requires env var | No (`XAI_API_KEY` is **not** used for this provider) |
|
||||
| Subscription | [SuperGrok](https://x.ai/grok) (any active tier) |
|
||||
| Subscription | [SuperGrok](https://x.ai/grok) — see note below |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -33,6 +33,10 @@ The same OAuth bearer token is also reused by every direct-to-xAI surface in Her
|
||||
- An active SuperGrok subscription on your xAI account
|
||||
- A browser available on the local machine (or use `--no-browser` for remote sessions)
|
||||
|
||||
:::warning xAI may restrict OAuth API access by tier
|
||||
xAI's backend enforces its own allowlist on the OAuth API surface and has been seen to reject standard SuperGrok subscribers with `HTTP 403` (see issue [#26847](https://github.com/NousResearch/hermes-agent/issues/26847)) even though the in-app subscription is active. If OAuth login succeeds in the browser but inference returns 403, set `XAI_API_KEY` and switch to the API-key path (`provider: xai`) — that surface is not subject to the same gating today.
|
||||
:::
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
@@ -76,6 +80,18 @@ Through a jump box / bastion: add `-J jump-user@jump-host`.
|
||||
|
||||
See [OAuth over SSH / Remote Hosts](./oauth-over-ssh.md) for the full step-by-step, including ProxyJump chains, mosh/tmux, and ControlMaster gotchas.
|
||||
|
||||
### Browser-only remotes (Cloud Shell, Codespaces, EC2 Instance Connect)
|
||||
|
||||
If you don't have a regular SSH client (e.g. you're running Hermes inside GCP Cloud Shell, GitHub Codespaces, AWS EC2 Instance Connect, Gitpod, or another browser-based console), the `ssh -L` recipe above isn't available. Use `--manual-paste` instead — Hermes skips the loopback listener and lets you paste the failed callback URL straight from your browser:
|
||||
|
||||
```bash
|
||||
hermes auth add xai-oauth --manual-paste
|
||||
# Or via the model picker:
|
||||
hermes model --manual-paste
|
||||
```
|
||||
|
||||
See [OAuth over SSH / Remote Hosts](./oauth-over-ssh.md#browser-only-remote-cloud-shell--codespaces--ec2-instance-connect) for the full walkthrough. Regression fix for [#26923](https://github.com/NousResearch/hermes-agent/issues/26923).
|
||||
|
||||
## How the Login Works
|
||||
|
||||
1. Hermes opens your browser to `accounts.x.ai`.
|
||||
@@ -148,8 +164,8 @@ If OAuth tokens are already stored, the picker confirms it and skips the credent
|
||||
The `video_gen` toolset is disabled by default. Enable it in `hermes tools` → `🎬 Video Generation` (press space) before the agent can call `video_generate`. Otherwise the agent may fall back to the bundled ComfyUI skill, which is also tagged for video generation.
|
||||
:::
|
||||
|
||||
:::note X search is off by default
|
||||
The `x_search` toolset is disabled by default. Enable it in `hermes tools` → `🐦 X (Twitter) Search` (press space) before the agent can call `x_search`. The tool routes through xAI's built-in `x_search` Responses API — it works with **either** your SuperGrok OAuth login or a paid `XAI_API_KEY`, and prefers OAuth when both are configured (uses your subscription quota instead of API spend). The tool schema is hidden from the model when no xAI credentials are configured, regardless of whether the toolset is enabled.
|
||||
:::note X search auto-enables when xAI credentials are present
|
||||
The `x_search` toolset auto-enables whenever xAI credentials (a SuperGrok OAuth token or `XAI_API_KEY`) are configured. Disable explicitly via `hermes tools` → `🐦 X (Twitter) Search` (press space) if you don't want this. The tool routes through xAI's built-in `x_search` Responses API — it works with **either** your SuperGrok OAuth login or a paid `XAI_API_KEY`, and prefers OAuth when both are configured (uses your subscription quota instead of API spend). The tool schema is hidden from the model when no xAI credentials are configured, regardless of whether the toolset is enabled.
|
||||
:::
|
||||
|
||||
### Models
|
||||
@@ -180,7 +196,9 @@ The chat catalog is derived live from the on-disk `models.dev` cache; new xAI re
|
||||
|
||||
Hermes refreshes the token before each session and again reactively on a 401. If refresh fails with `invalid_grant` (the refresh token was revoked, or the account was rotated), Hermes surfaces a typed re-auth message instead of crashing.
|
||||
|
||||
**Fix:** run `hermes auth add xai-oauth` again to start a fresh login.
|
||||
When the refresh failure is terminal (HTTP 4xx, `invalid_grant`, revoked grant, etc.), Hermes marks the refresh token as dead and quarantines it locally — subsequent calls skip the doomed refresh attempt instead of replaying the same 401 over and over. The agent surfaces a single "re-authentication required" message and stays out of the way until you log in again.
|
||||
|
||||
**Fix:** run `hermes auth add xai-oauth` again to start a fresh login. The quarantine clears on the next successful exchange.
|
||||
|
||||
### Authorization timed out
|
||||
|
||||
@@ -208,6 +226,21 @@ hermes auth add xai-oauth --no-browser
|
||||
|
||||
Full walkthrough (jump boxes, mosh/tmux, port conflicts): [OAuth over SSH / Remote Hosts](./oauth-over-ssh.md).
|
||||
|
||||
### HTTP 403 after a successful login (tier / entitlement)
|
||||
|
||||
OAuth completed in the browser, tokens are saved, but inference or token refresh returns `HTTP 403` with a message similar to *"The caller does not have permission to execute the specified operation"*.
|
||||
|
||||
This is **not** a stale-token problem — re-running `hermes model` won't change it. xAI's backend has been seen to restrict OAuth API access to specific SuperGrok tiers despite the in-app subscription being active (issue [#26847](https://github.com/NousResearch/hermes-agent/issues/26847)).
|
||||
|
||||
**Fix:** set `XAI_API_KEY` and switch to the API-key path:
|
||||
|
||||
```bash
|
||||
export XAI_API_KEY=xai-...
|
||||
hermes config set model.provider xai
|
||||
```
|
||||
|
||||
Or upgrade your subscription at [x.ai/grok](https://x.ai/grok) if the OAuth route is required.
|
||||
|
||||
### "No xAI credentials found" error at runtime
|
||||
|
||||
The auth store has no `xai-oauth` entry and no `XAI_API_KEY` is set. You haven't logged in yet, or the credential file was deleted.
|
||||
|
||||
@@ -28,7 +28,7 @@ curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scri
|
||||
**Windows (native, PowerShell)** — *early beta, [details →](/docs/user-guide/windows-native)*
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex
|
||||
iex (irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1)
|
||||
```
|
||||
|
||||
**Android (Termux)** — same curl one-liner as Linux; the installer auto-detects Termux.
|
||||
|
||||
@@ -29,8 +29,10 @@ You need at least one way to connect to an LLM. Use `hermes model` to switch pro
|
||||
| **GMI Cloud** | `GMI_API_KEY` in `~/.hermes/.env` (provider: `gmi`; aliases: `gmi-cloud`, `gmicloud`) |
|
||||
| **MiniMax** | `MINIMAX_API_KEY` in `~/.hermes/.env` (provider: `minimax`) |
|
||||
| **MiniMax China** | `MINIMAX_CN_API_KEY` in `~/.hermes/.env` (provider: `minimax-cn`) |
|
||||
| **Alibaba Cloud** | `DASHSCOPE_API_KEY` in `~/.hermes/.env` (provider: `alibaba`) |
|
||||
| **Alibaba Coding Plan** | `DASHSCOPE_API_KEY` (provider: `alibaba-coding-plan`, alias: `alibaba_coding`) — separate billing SKU, different endpoint |
|
||||
| **xAI (Grok) — Responses API** | `XAI_API_KEY` in `~/.hermes/.env` (provider: `xai`) |
|
||||
| **xAI Grok OAuth (SuperGrok)** | `hermes model` → "xAI Grok OAuth (SuperGrok Subscription)" — browser login, no API key. See [guide](../guides/xai-grok-oauth.md) |
|
||||
| **Qwen Cloud (Alibaba DashScope)** | `DASHSCOPE_API_KEY` in `~/.hermes/.env` (provider: `alibaba`) |
|
||||
| **Alibaba Cloud (Coding Plan)** | `DASHSCOPE_API_KEY` (provider: `alibaba-coding-plan`, alias: `alibaba_coding`) — separate billing SKU, different endpoint |
|
||||
| **Kilo Code** | `KILOCODE_API_KEY` in `~/.hermes/.env` (provider: `kilocode`) |
|
||||
| **Xiaomi MiMo** | `XIAOMI_API_KEY` in `~/.hermes/.env` (provider: `xiaomi`, aliases: `mimo`, `xiaomi-mimo`) |
|
||||
| **Tencent TokenHub** | `TOKENHUB_API_KEY` in `~/.hermes/.env` (provider: `tencent-tokenhub`, aliases: `tencent`, `tokenhub`, `tencentmaas`) |
|
||||
@@ -137,6 +139,8 @@ with the Generative Language API enabled.
|
||||
|
||||
:::info Codex Note
|
||||
The OpenAI Codex provider authenticates via device code (open a URL, enter a code). Hermes stores the resulting credentials in its own auth store under `~/.hermes/auth.json` and can import existing Codex CLI credentials from `~/.codex/auth.json` when present. No Codex CLI installation is required.
|
||||
|
||||
If a token refresh fails with a terminal error (HTTP 4xx, `invalid_grant`, revoked grant, etc.), Hermes marks the refresh token as dead and stops replaying it so you don't see a flood of identical auth failures. The next request surfaces a typed re-auth message instead. Run `hermes auth add codex-oauth` (or `hermes model` → OpenAI Codex) to start a fresh device-code login; the quarantine clears on the next successful exchange.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
@@ -158,6 +162,18 @@ Hermes has **two** model commands that serve different purposes:
|
||||
|
||||
If you're trying to switch to a provider you haven't set up yet (e.g. you only have OpenRouter configured and want to use Anthropic), you need `hermes model`, not `/model`. Exit your session first (`Ctrl+C` or `/quit`), run `hermes model`, complete the provider setup, then start a new session.
|
||||
|
||||
### Nous Portal
|
||||
|
||||
Subscription-based access to Hermes-4 models (`Hermes-4-70B`, `Hermes-4.3-36B`, `Hermes-4-405B`) via Nous Research's portal. Run `hermes model`, pick **Nous Portal**, sign in through the browser — Hermes stores a long-lived refresh token at `~/.hermes/auth.json`.
|
||||
|
||||
The refresh token is also shared across profiles via a shared token store, so logging in on one profile carries over to the others.
|
||||
|
||||
#### Token handling
|
||||
|
||||
Hermes mints a short-lived JWT from your stored Nous refresh token on each inference call rather than reusing a long-lived API key. The token lifecycle is fully automatic — refresh, mint, retry on transient 401 — and you never see it.
|
||||
|
||||
If the portal invalidates the refresh token (password change, manual revoke, session expiry), the invalid refresh token is quarantined locally so Hermes stops replaying it and you don't see a stream of identical 401s. The next call surfaces a clear "re-authentication required" message. Run `hermes auth add nous` to log in again; the quarantine clears on the next successful login.
|
||||
|
||||
### Anthropic (Native)
|
||||
|
||||
Use Claude models directly through the Anthropic API — no OpenRouter proxy needed. Supports three auth methods:
|
||||
@@ -292,7 +308,7 @@ hermes chat --provider minimax --model MiniMax-M2.7
|
||||
hermes chat --provider minimax-cn --model MiniMax-M2.7
|
||||
# Requires: MINIMAX_CN_API_KEY in ~/.hermes/.env
|
||||
|
||||
# Alibaba Cloud / DashScope (Qwen models)
|
||||
# Qwen Cloud / DashScope (Qwen models)
|
||||
hermes chat --provider alibaba --model qwen3.5-plus
|
||||
# Requires: DASHSCOPE_API_KEY in ~/.hermes/.env
|
||||
|
||||
@@ -440,11 +456,11 @@ model:
|
||||
|
||||
Set `HERMES_QWEN_BASE_URL` only if the portal endpoint relocates (default: `https://portal.qwen.ai/v1`).
|
||||
|
||||
:::tip Qwen OAuth vs DashScope (Alibaba)
|
||||
`qwen-oauth` uses the consumer-facing Qwen Portal with OAuth login — ideal for individual users. The `alibaba` provider uses DashScope's enterprise API with a `DASHSCOPE_API_KEY` — ideal for programmatic / production workloads. Both route to Qwen-family models but live at different endpoints.
|
||||
:::tip Qwen OAuth vs Qwen Cloud (Alibaba DashScope)
|
||||
`qwen-oauth` uses the consumer-facing Qwen Portal with OAuth login — ideal for individual users. The `alibaba` provider uses Qwen Cloud (Alibaba DashScope) with a `DASHSCOPE_API_KEY` — ideal for programmatic / production workloads. Both route to Qwen-family models but live at different endpoints.
|
||||
:::
|
||||
|
||||
### Alibaba Coding Plan
|
||||
### Alibaba Cloud (Coding Plan)
|
||||
|
||||
If you're subscribed to Alibaba's **Coding Plan** (a pricing SKU separate from standard DashScope API access), Hermes exposes it as its own first-class provider: `alibaba-coding-plan`. Endpoint: `https://coding-intl.dashscope.aliyuncs.com/v1`. It's OpenAI-compatible like the regular `alibaba` provider but with a different base URL and billing surface.
|
||||
|
||||
@@ -512,6 +528,8 @@ model:
|
||||
For on-prem deployments (DGX Spark, local GPU), set `NVIDIA_BASE_URL=http://localhost:8000/v1`. NIM exposes the same OpenAI-compatible chat completions API as build.nvidia.com, so switching between cloud and local is a one-line env-var change.
|
||||
:::
|
||||
|
||||
Hermes automatically attaches the NIM billing-origin header on every request to `build.nvidia.com` — no configuration needed. This routes consumption against the correct origin in NVIDIA's billing dashboard.
|
||||
|
||||
### GMI Cloud
|
||||
|
||||
Open and reasoning models via [GMI Cloud](https://www.gmicloud.ai/) — OpenAI-compatible API, API key authentication.
|
||||
@@ -1203,13 +1221,15 @@ custom_providers:
|
||||
- name: work
|
||||
base_url: https://gpu-server.internal.corp/v1
|
||||
key_env: CORP_API_KEY
|
||||
api_mode: chat_completions # optional, auto-detected from URL
|
||||
api_mode: chat_completions # set explicitly by `hermes model` → Custom Endpoint wizard; auto-detection still happens as a fallback
|
||||
- name: anthropic-proxy
|
||||
base_url: https://proxy.example.com/anthropic
|
||||
key_env: ANTHROPIC_PROXY_KEY
|
||||
api_mode: anthropic_messages # for Anthropic-compatible proxies
|
||||
```
|
||||
|
||||
The `hermes model` → Custom Endpoint wizard now prompts for `api_mode` explicitly and persists your answer to `config.yaml`. URL-based auto-detection (e.g. `/anthropic` paths → `anthropic_messages`) still happens as a fallback when the field is left blank.
|
||||
|
||||
Switch between them mid-session with the triple syntax:
|
||||
|
||||
```
|
||||
|
||||
@@ -62,6 +62,7 @@ hermes [global-options] <command> [subcommand/options]
|
||||
| `hermes config` | Show, edit, migrate, and query configuration files. |
|
||||
| `hermes pairing` | Approve or revoke messaging pairing codes. |
|
||||
| `hermes skills` | Browse, install, publish, audit, and configure skills. |
|
||||
| `hermes bundles` | Group several skills under a single `/<name>` slash command. See [Skill Bundles](../user-guide/features/skills.md#skill-bundles). |
|
||||
| `hermes curator` | Background skill maintenance — status, run, pause, pin. See [Curator](../user-guide/features/curator.md). |
|
||||
| `hermes memory` | Configure external memory provider. Plugin-specific subcommands (e.g. `hermes honcho`) register automatically when their provider is active. |
|
||||
| `hermes acp` | Run Hermes as an ACP server for editor integration. |
|
||||
@@ -211,6 +212,7 @@ Subcommands:
|
||||
| `stop` | Stop the service (or foreground process). |
|
||||
| `restart` | Restart the service. |
|
||||
| `status` | Show service status. |
|
||||
| `list` | List **all profiles** and whether each profile's gateway is currently running (with PID where available). Handy when you run multiple profiles side-by-side and want a single overview. |
|
||||
| `install` | Install as a systemd (Linux) or launchd (macOS) background service. |
|
||||
| `uninstall` | Remove the installed service. |
|
||||
| `setup` | Interactive messaging-platform setup. |
|
||||
@@ -384,7 +386,7 @@ Multi-profile, multi-project collaboration board. Each install can host many boa
|
||||
|------|---------|
|
||||
| `--board <slug>` | Operate on a specific board. Defaults to the current board (set via `hermes kanban boards switch`, the `HERMES_KANBAN_BOARD` env var, or `default`). |
|
||||
|
||||
**This is the human / scripting surface.** Agent workers spawned by the dispatcher drive the board through a dedicated `kanban_*` [toolset](/docs/user-guide/features/kanban#how-workers-interact-with-the-board) (`kanban_show`, `kanban_complete`, `kanban_block`, `kanban_create`, `kanban_link`, `kanban_comment`, `kanban_heartbeat`) instead of shelling to `hermes kanban`. Workers have `HERMES_KANBAN_BOARD` pinned in their env so they physically cannot see other boards.
|
||||
**This is the human / scripting surface.** Agent workers spawned by the dispatcher drive the board through a dedicated `kanban_*` [toolset](/docs/user-guide/features/kanban#how-workers-interact-with-the-board) (`kanban_show`, `kanban_complete`, `kanban_block`, `kanban_create`, `kanban_link`, `kanban_comment`, `kanban_heartbeat`; orchestrator profiles also get `kanban_list` and `kanban_unblock`) instead of shelling to `hermes kanban`. Workers have `HERMES_KANBAN_BOARD` pinned in their env so they physically cannot see other boards.
|
||||
|
||||
| Action | Purpose |
|
||||
|--------|---------|
|
||||
@@ -395,7 +397,7 @@ Multi-profile, multi-project collaboration board. Each install can host many boa
|
||||
| `boards show` / `boards current` | Print the currently-active board's name, DB path, and task counts. |
|
||||
| `boards rename <slug> "<name>"` | Change a board's display name. Slug is immutable. |
|
||||
| `boards rm <slug>` | Archive (default) or hard-delete a board. `--delete` skips the archive step. Archived boards move to `boards/_archived/<slug>-<ts>/`. Refused for `default`. |
|
||||
| `create "<title>"` | Create a new task on the active board. Flags: `--body`, `--assignee`, `--parent` (repeatable), `--workspace scratch\|worktree\|dir:<path>`, `--tenant`, `--priority`, `--triage`, `--idempotency-key`, `--max-runtime`, `--skill` (repeatable). |
|
||||
| `create "<title>"` | Create a new task on the active board. Flags: `--body`, `--assignee`, `--parent` (repeatable), `--workspace scratch\|worktree\|dir:<path>`, `--tenant`, `--priority`, `--triage`, `--idempotency-key`, `--max-runtime`, `--max-retries`, `--skill` (repeatable). |
|
||||
| `list` / `ls` | List tasks on the active board. Filter with `--mine`, `--assignee`, `--status`, `--tenant`, `--archived`, `--json`. |
|
||||
| `show <id>` | Show a task with comments and events. `--json` for machine output. |
|
||||
| `assign <id> <profile>` | Assign or reassign. Use `none` to unassign. Refused while task is running. |
|
||||
@@ -404,11 +406,12 @@ Multi-profile, multi-project collaboration board. Each install can host many boa
|
||||
| `claim <id>` | Atomically claim a ready task. Prints resolved workspace path. |
|
||||
| `comment <id> "<text>"` | Append a comment. The next worker that claims the task reads it as part of its `kanban_show()` response. |
|
||||
| `complete <id>` | Mark task done. Flags: `--result`, `--summary`, `--metadata`. |
|
||||
| `block <id> "<reason>"` | Mark task blocked. Also appends the reason as a comment. |
|
||||
| `unblock <id>` | Return a blocked task to ready. |
|
||||
| `block <id> "<reason>"` | Mark task blocked for human input. Also appends the reason as a comment. |
|
||||
| `schedule <id> "<reason>"` | Park time-delay/follow-up work in `scheduled` so it is not shown as a human blocker. |
|
||||
| `unblock <id>` | Return a blocked or scheduled task to ready (or `todo` if dependencies are still open). |
|
||||
| `archive <id>` | Hide from default list. `gc` will remove scratch workspaces. |
|
||||
| `tail <id>` | Follow a task's event stream. |
|
||||
| `dispatch` | One dispatcher pass on the active board. Flags: `--dry-run`, `--max N`, `--json`. |
|
||||
| `dispatch` | One dispatcher pass on the active board. Flags: `--dry-run`, `--max N`, `--failure-limit N`, `--json`. |
|
||||
| `context <id>` | Print the full context a worker would see (title + body + parent results + comments). |
|
||||
| `specify <id>` / `specify --all` | Flesh out a triage-column task into a concrete spec (title + body with goal, approach, acceptance criteria) via the auxiliary LLM, then promote it to `todo`. Flags: `--tenant` (scope `--all` to one tenant), `--author`, `--json`. Configure the model under `auxiliary.triage_specifier` in `config.yaml`. |
|
||||
| `decompose <id>` / `decompose --all` | Fan a triage-column task out into a graph of child tasks routed to specialist profiles by description (the orchestrator-driven path). Falls back to specify-style single-task promotion when the LLM decides the task doesn't benefit from fan-out. Same flags as `specify`. Configure the model under `auxiliary.kanban_decomposer` in `config.yaml`. Also runs automatically every dispatcher tick when `kanban.auto_decompose: true` (the default). See [Auto vs Manual orchestration](/docs/user-guide/features/kanban#auto-vs-manual-orchestration). |
|
||||
@@ -823,8 +826,43 @@ Notes:
|
||||
- `--force` does not override a `dangerous` scan verdict.
|
||||
- `--source skills-sh` searches the public `skills.sh` directory.
|
||||
- `--source well-known` lets you point Hermes at a site exposing `/.well-known/skills/index.json`.
|
||||
- `--source browse-sh` searches [browse.sh](https://browse.sh)'s catalog of 200+ site-specific browser-automation skills. Identifiers look like `browse-sh/airbnb.com/search-listings-ddgioa`.
|
||||
- Passing an `http(s)://…/*.md` URL installs a single-file SKILL.md directly. When frontmatter has no `name:` and the URL slug isn't a valid identifier, an interactive terminal prompts for a name; non-interactive surfaces (`/skills install` inside the TUI, gateway platforms) require `--name <x>` instead.
|
||||
|
||||
## `hermes bundles`
|
||||
|
||||
```bash
|
||||
hermes bundles <subcommand>
|
||||
```
|
||||
|
||||
Skill bundles group several skills under one `/<bundle-name>` slash command. Invoking the bundle loads every referenced skill into a single combined user message. Storage: `~/.hermes/skill-bundles/<slug>.yaml`. See [Skill Bundles](../user-guide/features/skills.md#skill-bundles) for the YAML schema and behavior.
|
||||
|
||||
Subcommands:
|
||||
|
||||
| Subcommand | Description |
|
||||
|------------|-------------|
|
||||
| `list` | List installed bundles (default when no subcommand given) |
|
||||
| `show <name>` | Show one bundle's name, description, skills, and file path |
|
||||
| `create <name>` | Create a new bundle. Pass `--skill <id>` (repeat) or omit for interactive entry. `--description`, `--instruction`, `--force` available. |
|
||||
| `delete <name>` | Remove a bundle file |
|
||||
| `reload` | Re-scan `~/.hermes/skill-bundles/` and report added/removed bundles |
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
hermes bundles create backend-dev \
|
||||
--skill github-code-review \
|
||||
--skill test-driven-development \
|
||||
--skill github-pr-workflow \
|
||||
-d "Backend feature work"
|
||||
|
||||
hermes bundles list
|
||||
hermes bundles show backend-dev
|
||||
hermes bundles delete backend-dev
|
||||
```
|
||||
|
||||
In a chat session, `/bundles` lists installed bundles and `/<bundle-name>` loads one.
|
||||
|
||||
## `hermes curator`
|
||||
|
||||
```bash
|
||||
|
||||
@@ -50,9 +50,16 @@ All variables go in `~/.hermes/.env`. You can also set them with `hermes config
|
||||
| `XIAOMI_BASE_URL` | Override Xiaomi MiMo base URL (default: `https://api.xiaomimimo.com/v1`) |
|
||||
| `TOKENHUB_API_KEY` | Tencent TokenHub API key ([tokenhub.tencentmaas.com](https://tokenhub.tencentmaas.com)) |
|
||||
| `TOKENHUB_BASE_URL` | Override Tencent TokenHub base URL (default: `https://tokenhub.tencentmaas.com/v1`) |
|
||||
| `AZURE_FOUNDRY_API_KEY` | Azure AI Foundry / Azure OpenAI API key ([ai.azure.com](https://ai.azure.com/)) |
|
||||
| `AZURE_FOUNDRY_BASE_URL` | Azure AI Foundry endpoint URL (e.g. `https://<resource>.openai.azure.com/openai/v1` for OpenAI-style, or `https://<resource>.services.ai.azure.com/anthropic` for Anthropic-style) |
|
||||
| `AZURE_ANTHROPIC_KEY` | Azure Anthropic API key for `provider: anthropic` + `base_url` pointing at an Azure Foundry Claude deployment (alternative to `ANTHROPIC_API_KEY` when both Anthropic and Azure Anthropic are configured) |
|
||||
| `AZURE_FOUNDRY_API_KEY` | Microsoft Foundry / Azure OpenAI API key ([ai.azure.com](https://ai.azure.com/)). Not needed when `model.auth_mode: entra_id` |
|
||||
| `AZURE_FOUNDRY_BASE_URL` | Microsoft Foundry endpoint URL (e.g. `https://<resource>.openai.azure.com/openai/v1` for OpenAI-style, or `https://<resource>.services.ai.azure.com/anthropic` for Anthropic-style) |
|
||||
| `AZURE_ANTHROPIC_KEY` | Azure Anthropic API key for `provider: anthropic` + `base_url` pointing at a Microsoft Foundry Claude deployment (alternative to `ANTHROPIC_API_KEY` when both Anthropic and Azure Anthropic are configured) |
|
||||
| `AZURE_TENANT_ID` | Entra ID tenant ID (service-principal flows; honored by `azure-identity` when `model.auth_mode: entra_id`) |
|
||||
| `AZURE_CLIENT_ID` | Entra ID client ID (service principal, workload identity, or user-assigned managed identity) |
|
||||
| `AZURE_CLIENT_SECRET` | Service principal secret used by `EnvironmentCredential` |
|
||||
| `AZURE_CLIENT_CERTIFICATE_PATH` | Service principal certificate (alternative to `AZURE_CLIENT_SECRET`) |
|
||||
| `AZURE_FEDERATED_TOKEN_FILE` | Federated token file path for AKS Workload Identity / OIDC flows |
|
||||
| `AZURE_AUTHORITY_HOST` | Sovereign-cloud authority override (e.g. `https://login.microsoftonline.us` for Azure Government). See [Azure Foundry guide](/docs/guides/azure-foundry#sovereign-clouds-government-china) |
|
||||
| `IDENTITY_ENDPOINT` / `MSI_ENDPOINT` | Managed Identity endpoint for App Service, Functions, and Container Apps; VMs usually use IMDS instead and do not set these |
|
||||
| `HF_TOKEN` | Hugging Face token for Inference Providers ([huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)) |
|
||||
| `HF_BASE_URL` | Override Hugging Face base URL (default: `https://router.huggingface.co/v1`) |
|
||||
| `GOOGLE_API_KEY` | Google AI Studio API key ([aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)) |
|
||||
@@ -63,7 +70,7 @@ All variables go in `~/.hermes/.env`. You can also set them with `hermes config
|
||||
| `HERMES_GEMINI_PROJECT_ID` | GCP project ID for paid Gemini tiers (free tier auto-provisions) |
|
||||
| `ANTHROPIC_API_KEY` | Anthropic Console API key ([console.anthropic.com](https://console.anthropic.com/)) |
|
||||
| `ANTHROPIC_TOKEN` | Manual or legacy Anthropic OAuth/setup-token override |
|
||||
| `DASHSCOPE_API_KEY` | Alibaba Cloud DashScope API key for Qwen models ([modelstudio.console.alibabacloud.com](https://modelstudio.console.alibabacloud.com/)) |
|
||||
| `DASHSCOPE_API_KEY` | Qwen Cloud (Alibaba DashScope) API key for Qwen models ([modelstudio.console.alibabacloud.com](https://modelstudio.console.alibabacloud.com/)) |
|
||||
| `DASHSCOPE_BASE_URL` | Custom DashScope base URL (default: `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`; use `https://dashscope.aliyuncs.com/compatible-mode/v1` for mainland-China region) |
|
||||
| `DEEPSEEK_API_KEY` | DeepSeek API key for direct DeepSeek access ([platform.deepseek.com](https://platform.deepseek.com/api_keys)) |
|
||||
| `DEEPSEEK_BASE_URL` | Custom DeepSeek API base URL |
|
||||
@@ -75,7 +82,7 @@ All variables go in `~/.hermes/.env`. You can also set them with `hermes config
|
||||
| `STEPFUN_BASE_URL` | Override StepFun base URL (default: `https://api.stepfun.com/v1`) |
|
||||
| `OLLAMA_API_KEY` | Ollama Cloud API key — managed Ollama catalog without local GPU ([ollama.com/settings/keys](https://ollama.com/settings/keys)) |
|
||||
| `OLLAMA_BASE_URL` | Override Ollama Cloud base URL (default: `https://ollama.com/v1`) |
|
||||
| `XAI_API_KEY` | xAI (Grok) API key for chat + TTS ([console.x.ai](https://console.x.ai/)) |
|
||||
| `XAI_API_KEY` | xAI (Grok) API key for chat + TTS + web search ([console.x.ai](https://console.x.ai/)) |
|
||||
| `XAI_BASE_URL` | Override xAI base URL (default: `https://api.x.ai/v1`) |
|
||||
| `MISTRAL_API_KEY` | Mistral API key for Voxtral TTS and Voxtral STT ([console.mistral.ai](https://console.mistral.ai)) |
|
||||
| `AWS_REGION` | AWS region for Bedrock inference (e.g. `us-east-1`, `eu-central-1`). Read by boto3. |
|
||||
@@ -98,6 +105,7 @@ All variables go in `~/.hermes/.env`. You can also set them with `hermes config
|
||||
| `HERMES_KANBAN_BOARD` | Pin the active kanban board for this process. Takes precedence over `~/.hermes/kanban/current`; the dispatcher injects this into worker subprocess env so workers physically cannot see tasks on other boards. Defaults to `default`. Slug validation: lowercase alphanumerics + hyphens + underscores, 1-64 chars |
|
||||
| `HERMES_KANBAN_DB` | Pin the kanban database file path directly (highest precedence; beats `HERMES_KANBAN_BOARD` and `HERMES_KANBAN_HOME`). The dispatcher injects this into worker subprocess env so profile workers converge on the dispatcher's board |
|
||||
| `HERMES_KANBAN_WORKSPACES_ROOT` | Pin the kanban workspaces root directly (highest precedence for workspaces; beats `HERMES_KANBAN_HOME`). The dispatcher injects this into worker subprocess env |
|
||||
| `HERMES_KANBAN_DISPATCH_IN_GATEWAY` | Runtime override for `kanban.dispatch_in_gateway`. Set to `0`, `false`, `no`, or `off` to keep the gateway from starting the embedded Kanban dispatcher; any other non-empty value enables it. Useful when a separate dispatcher process owns the board. |
|
||||
|
||||
## Provider Auth (OAuth)
|
||||
|
||||
@@ -240,10 +248,14 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
|
||||
| `TELEGRAM_GROUP_ALLOWED_CHATS` | Comma-separated group/forum chat IDs; any member is authorized |
|
||||
| `TELEGRAM_HOME_CHANNEL` | Default Telegram chat/channel for cron delivery |
|
||||
| `TELEGRAM_HOME_CHANNEL_NAME` | Display name for the Telegram home channel |
|
||||
| `TELEGRAM_CRON_THREAD_ID` | Forum topic ID to receive cron deliveries; overrides `TELEGRAM_HOME_CHANNEL_THREAD_ID` for cron only. Use in topic mode so replies to cron messages open a new session instead of hitting the system lobby (#24409). |
|
||||
| `TELEGRAM_WEBHOOK_URL` | Public HTTPS URL for webhook mode (enables webhook instead of polling) |
|
||||
| `TELEGRAM_WEBHOOK_PORT` | Local listen port for webhook server (default: `8443`) |
|
||||
| `TELEGRAM_WEBHOOK_SECRET` | Secret token Telegram echoes back in each update for verification. **Required whenever `TELEGRAM_WEBHOOK_URL` is set** — the gateway refuses to start without it (GHSA-3vpc-7q5r-276h). Generate with `openssl rand -hex 32`. |
|
||||
| `TELEGRAM_REACTIONS` | Enable emoji reactions on messages during processing (default: `false`) |
|
||||
| `TELEGRAM_REQUIRE_MENTION` | Require an explicit trigger before responding in Telegram groups. Equivalent to `telegram.require_mention` in `config.yaml`. |
|
||||
| `TELEGRAM_MENTION_PATTERNS` | JSON array, newline-separated list, or comma-separated list of regex wake-word patterns accepted when Telegram group mention gating is enabled. Equivalent to `telegram.mention_patterns`. |
|
||||
| `TELEGRAM_EXCLUSIVE_BOT_MENTIONS` | When enabled, explicit `@...bot` mentions in Telegram groups route only to the mentioned bot usernames before reply or wake-word fallbacks run. Default: `true`. Equivalent to `telegram.exclusive_bot_mentions`. |
|
||||
| `TELEGRAM_REPLY_TO_MODE` | Reply-reference behavior: `off`, `first` (default), or `all`. Matches the Discord pattern. |
|
||||
| `TELEGRAM_IGNORED_THREADS` | Comma-separated Telegram forum topic/thread IDs where the bot never responds |
|
||||
| `TELEGRAM_PROXY` | Proxy URL for Telegram connections — overrides `HTTPS_PROXY`. Supports `http://`, `https://`, `socks5://` |
|
||||
@@ -567,6 +579,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us
|
||||
|----------|-------------|
|
||||
| `SESSION_IDLE_MINUTES` | Reset sessions after N minutes of inactivity (default: 1440) |
|
||||
| `SESSION_RESET_HOUR` | Daily reset hour in 24h format (default: 4 = 4am) |
|
||||
| `HERMES_SESSION_ID` | **Exported automatically into every tool subprocess** Hermes spawns (`terminal`, `execute_code`, persistent shell, Docker/Singularity backends, delegated subagent runs). Set by the agent to the current session ID; user scripts called from tools can read it to correlate their output, telemetry, or side effects with the originating Hermes session. **You should not set this manually** — overriding it from a parent shell only takes effect outside an agent run, and is overwritten the moment the agent starts a session. |
|
||||
|
||||
## Context Compression (config.yaml only)
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ Creates a new profile.
|
||||
| `--clone-from <profile>` | Clone from a specific profile instead of the current one. Used with `--clone` or `--clone-all`. |
|
||||
| `--no-alias` | Skip wrapper script creation. |
|
||||
| `--description "<text>"` | One- or two-sentence description of what this profile is good at. Used by the kanban orchestrator to route tasks based on role instead of profile name alone. Skip and add later via `hermes profile describe`. Persisted in `<profile_dir>/profile.yaml`. |
|
||||
| `--no-skills` | Create an **empty** profile with zero bundled skills enabled. Writes a `.no-skills` marker into the profile so future `hermes update` runs won't re-seed the bundled set, and refuses to combine with `--clone` / `--clone-all` (which would copy skills in anyway). Useful for narrow orchestrator profiles or sandbox profiles that should not inherit the full skill catalog. |
|
||||
|
||||
Creating a profile does **not** make that profile directory the default project/workspace directory for terminal commands. If you want a profile to start in a specific project, set `terminal.cwd` in that profile's `config.yaml`.
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ If a skill is missing from this list but present in the repo, the catalog is reg
|
||||
| [`architecture-diagram`](/docs/user-guide/skills/bundled/creative/creative-architecture-diagram) | Dark-themed SVG architecture/cloud/infra diagrams as HTML. | `creative/architecture-diagram` |
|
||||
| [`ascii-art`](/docs/user-guide/skills/bundled/creative/creative-ascii-art) | ASCII art: pyfiglet, cowsay, boxes, image-to-ascii. | `creative/ascii-art` |
|
||||
| [`ascii-video`](/docs/user-guide/skills/bundled/creative/creative-ascii-video) | ASCII video: convert video/audio to colored ASCII MP4/GIF. | `creative/ascii-video` |
|
||||
| [`baoyu-article-illustrator`](/docs/user-guide/skills/bundled/creative/creative-baoyu-article-illustrator) | Article illustrations: type × style × palette consistency. | `creative/baoyu-article-illustrator` |
|
||||
| [`baoyu-comic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-comic) | Knowledge comics (知识漫画): educational, biography, tutorial. | `creative/baoyu-comic` |
|
||||
| [`baoyu-infographic`](/docs/user-guide/skills/bundled/creative/creative-baoyu-infographic) | Infographics: 21 layouts x 21 styles (信息图, 可视化). | `creative/baoyu-infographic` |
|
||||
| [`claude-design`](/docs/user-guide/skills/bundled/creative/creative-claude-design) | Design one-off HTML artifacts (landing, deck, prototype). | `creative/claude-design` |
|
||||
|
||||
@@ -13,6 +13,21 @@ Hermes has two slash-command surfaces, both driven by a central `COMMAND_REGISTR
|
||||
|
||||
Installed skills are also exposed as dynamic slash commands on both surfaces. That includes bundled skills like `/plan`, which opens plan mode and saves markdown plans under `.hermes/plans/` relative to the active workspace/backend working directory.
|
||||
|
||||
## Permissions and admin/user split
|
||||
|
||||
Every messaging platform that supports a per-user allowlist (Telegram, Discord, Slack, Matrix, Mattermost, Signal, …) also supports a two-tier slash command split: **admins** get every registered command, **regular users** only get the names you list in `user_allowed_commands` (plus the always-allowed floor `/help` and `/whoami`). Configure `allow_admin_from` and `user_allowed_commands` (and the per-group equivalents `group_allow_admin_from` / `group_user_allowed_commands`) inside the platform's `extra:` block in `~/.hermes/gateway-config.yaml`.
|
||||
|
||||
See the per-platform docs for examples — the structure is identical across platforms:
|
||||
|
||||
- [Telegram](../user-guide/messaging/telegram.md#slash-command-access-control)
|
||||
- [Discord](../user-guide/messaging/discord.md)
|
||||
- [Slack](../user-guide/messaging/slack.md)
|
||||
- [Matrix](../user-guide/messaging/matrix.md)
|
||||
- [Mattermost](../user-guide/messaging/mattermost.md)
|
||||
- [Signal](../user-guide/messaging/signal.md)
|
||||
|
||||
If `allow_admin_from` is unset for a scope, that scope stays in unrestricted backward-compat mode — every allowed user can run every command.
|
||||
|
||||
## Interactive CLI slash commands
|
||||
|
||||
Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-insensitive.
|
||||
@@ -21,7 +36,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/new` (alias: `/reset`) | Start a new session (fresh session ID + history) |
|
||||
| `/new [name]` (alias: `/reset`) | Start a new session (fresh session ID + history). Optional `[name]` sets the initial session title — e.g. `/new my-experiment` opens a fresh session already titled `my-experiment` so it's easy to find later with `/resume` or `/sessions`. |
|
||||
| `/clear` | Clear screen and start a new session |
|
||||
| `/history` | Show conversation history |
|
||||
| `/save` | Save the current conversation |
|
||||
@@ -35,10 +50,11 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
|
||||
| `/queue <prompt>` (alias: `/q`) | Queue a prompt for the next turn (doesn't interrupt the current agent response). |
|
||||
| `/steer <prompt>` | Inject a mid-run note that arrives at the agent **after the next tool call** — no interrupt, no new user turn. The text is appended to the last tool result's content once the current tool completes, giving the agent new context without breaking the current tool-calling loop. Use this to nudge direction mid-task (e.g. "focus on the auth module" while the agent is running tests). |
|
||||
| `/goal <text>` | Set a standing goal Hermes works toward across turns — our take on the Ralph loop. After each turn an auxiliary judge model decides whether the goal is done; if not, Hermes auto-continues. Subcommands: `/goal status`, `/goal pause`, `/goal resume`, `/goal clear`. Budget defaults to 20 turns (`goals.max_turns`); any real user message preempts the continuation loop, and state survives `/resume`. See [Persistent Goals](/docs/user-guide/features/goals) for the full walkthrough. |
|
||||
| `/subgoal <text>` | Append a user-supplied criterion to the active goal mid-loop. The continuation prompt surfaces all subgoals to the agent verbatim, and the judge factors them into its DONE/CONTINUE verdict — so the goal isn't marked done until the original goal **and** every subgoal are met. Subcommands: `/subgoal` (list), `/subgoal remove <N>`, `/subgoal clear`. Requires an active `/goal`. |
|
||||
| `/resume [name]` | Resume a previously-named session |
|
||||
| `/sessions` | Browse and resume previous sessions in an interactive picker |
|
||||
| `/redraw` | Force a full UI repaint (recovers from terminal drift after tmux resize, mouse selection artifacts, etc.) |
|
||||
| `/status` | Show session info |
|
||||
| `/status` | Show session info — model, provider, profile, session ID, working directory, title, created/updated timestamps, token totals, agent-running state — followed by a local **Session recap** block (recent user/assistant turn counts, tool result count, top tools used, last few files touched, the latest user prompt, and the latest assistant reply). The recap is computed locally from the in-memory conversation; no LLM call, no prompt-cache impact. |
|
||||
| `/agents` (alias: `/tasks`) | Show active agents and running tasks across the current session. |
|
||||
| `/background <prompt>` (alias: `/bg`, `/btw`) | Run a prompt in a separate background session. The agent processes your prompt independently — your current session stays free for other work. Results appear as a panel when the task finishes. See [CLI Background Sessions](/docs/user-guide/cli#background-sessions). |
|
||||
| `/branch [name]` (alias: `/fork`) | Branch the current session (explore a different path) |
|
||||
@@ -86,7 +102,8 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
|
||||
| `/help` | Show this help message |
|
||||
| `/usage` | Show token usage, cost breakdown, session duration, and — when available from the active provider — an **Account limits** section with remaining quota / credits / plan usage pulled live from the provider's API. |
|
||||
| `/insights` | Show usage insights and analytics (last 30 days) |
|
||||
| `/platforms` (alias: `/gateway`) | Show gateway/messaging platform status |
|
||||
| `/platforms` (alias: `/gateway`) | Show gateway/messaging platform status (CLI-only summary view). |
|
||||
| `/platform <list\|pause\|resume> [name]` | Operate a running gateway platform. `/platform list` lists every adapter and its state (running, paused-by-breaker, manually-paused); `/platform pause <name>` stops dispatching new messages to that adapter without unloading it; `/platform resume <name>` re-enables it. The gateway also auto-pauses an adapter when its circuit breaker trips on repeated retryable failures (network / rate-limit / 5xx) — use `/platform resume <name>` to clear the breaker once the upstream is healthy. Available wherever the gateway is reachable (CLI session, Telegram, Discord, …). |
|
||||
| `/paste` | Attach a clipboard image |
|
||||
| `/copy [number]` | Copy the last assistant response to clipboard (or the Nth-from-last with a number). CLI-only. |
|
||||
| `/image <path>` | Attach a local image file for your next prompt. |
|
||||
@@ -178,7 +195,7 @@ The messaging gateway supports the following built-in commands inside Telegram,
|
||||
|---------|-------------|
|
||||
| `/new` | Start a new conversation. |
|
||||
| `/reset` | Reset conversation history. |
|
||||
| `/status` | Show session info. |
|
||||
| `/status` | Show session info, followed by a local **Session recap** block (recent turn counts, top tools used, files touched, latest prompt + reply). |
|
||||
| `/stop` | Kill all running background processes and interrupt the running agent. |
|
||||
| `/model [provider:model]` | Show or change the model. Supports provider switches (`/model zai:glm-5`), custom endpoints (`/model custom:model`), named custom providers (`/model custom:local:qwen`), auto-detect (`/model custom`), and user-defined aliases (`/model fav`, `/model grok` — see [Custom model aliases](#custom-model-aliases)). Use `--global` to persist the change to config.yaml. **Note:** `/model` can only switch between already-configured providers. To add a new provider or set up API keys, use `hermes model` from your terminal (outside the chat session). |
|
||||
| `/codex-runtime [auto\|codex_app_server\|on\|off]` | Toggle the optional [Codex app-server runtime](../user-guide/features/codex-app-server-runtime). Persists to `model.openai_runtime` in config.yaml and evicts the cached agent so the next message picks up the new runtime. Effective on next session. |
|
||||
@@ -221,3 +238,18 @@ The messaging gateway supports the following built-in commands inside Telegram,
|
||||
- `/sethome`, `/update`, `/restart`, `/approve`, `/deny`, `/topic`, and `/commands` are **messaging-only** commands.
|
||||
- `/status`, `/background`, `/queue`, `/steer`, `/voice`, `/reload-mcp`, `/reload-skills`, `/rollback`, `/debug`, `/fast`, `/footer`, `/curator`, `/kanban`, `/sessions`, and `/yolo` work in **both** the CLI and the messaging gateway.
|
||||
- `/voice join`, `/voice channel`, and `/voice leave` are only meaningful on Discord.
|
||||
|
||||
## Confirmation prompts for destructive commands
|
||||
|
||||
The CLI prompts before running slash commands that throw away unsaved session state. The current destructive set is:
|
||||
|
||||
| Command | What it destroys |
|
||||
|---------|------------------|
|
||||
| `/clear` | Clears the screen and starts a fresh session — current session ID and in-memory history are gone. |
|
||||
| `/new` / `/reset` | Starts a fresh session (new session ID + empty history). |
|
||||
| `/undo` | Removes the last user/assistant exchange from history. |
|
||||
| `/exit --delete` / `/quit --delete` | Exits **and** permanently deletes the current session's SQLite history and on-disk transcripts. |
|
||||
|
||||
For each of these the CLI opens a three-choice modal: **Approve Once** (proceed this time), **Always Approve** (proceed and persist `approvals.destructive_slash_confirm: false` so future destructive commands run without prompting), or **Cancel**.
|
||||
|
||||
Set `approvals.destructive_slash_confirm: false` in `~/.hermes/config.yaml` to disable the prompts globally; set it back to `true` to re-enable. See [Security — Destructive slash command confirmation](../user-guide/security.md#dangerous-command-approval) for context.
|
||||
|
||||
@@ -118,17 +118,19 @@ Scoped to the Feishu document-comment handler. Drives comment read/write operati
|
||||
|
||||
## `kanban` toolset
|
||||
|
||||
Registered only when the agent is spawned by the kanban dispatcher (`HERMES_KANBAN_TASK` env set). Lets workers mark tasks done with structured handoffs, block for human input, heartbeat during long ops, comment on threads, and (for orchestrators) fan out into child tasks. See [Kanban Multi-Agent](/docs/user-guide/features/kanban) for the full workflow.
|
||||
Registered when the agent is either (a) spawned by the kanban dispatcher (`HERMES_KANBAN_TASK` env set) or (b) running in a profile that explicitly enables the `kanban` toolset. Task-scoped workers use lifecycle tools for their assigned task; orchestrator profiles additionally get board-routing tools like `kanban_list` and `kanban_unblock`. See [Kanban Multi-Agent](/docs/user-guide/features/kanban) for the full workflow.
|
||||
|
||||
| Tool | Description | Requires environment |
|
||||
|------|-------------|----------------------|
|
||||
| `kanban_show` | Show the active kanban task assigned to this worker (title, description, comments, dependencies). | `HERMES_KANBAN_TASK` |
|
||||
| `kanban_complete` | Mark the current task done with a structured handoff payload (results, artifacts, follow-ups). | `HERMES_KANBAN_TASK` |
|
||||
| `kanban_block` | Block the current task on a question for the user — the dispatcher pauses, surfaces the question, and resumes once a human replies. | `HERMES_KANBAN_TASK` |
|
||||
| `kanban_heartbeat` | Send a progress heartbeat during a long-running operation so the dispatcher knows the worker is still alive. | `HERMES_KANBAN_TASK` |
|
||||
| `kanban_comment` | Add a comment to the task thread without changing its state — useful for surfacing intermediate findings. | `HERMES_KANBAN_TASK` |
|
||||
| `kanban_create` | (Orchestrator only) Fan out child tasks from the current task. | `HERMES_KANBAN_TASK` + orchestrator role |
|
||||
| `kanban_link` | (Orchestrator only) Link related tasks together (blocks/blocked-by/related). | `HERMES_KANBAN_TASK` + orchestrator role |
|
||||
| `kanban_show` | Show the active kanban task assigned to this worker (title, description, comments, dependencies). | `HERMES_KANBAN_TASK` or `kanban` toolset |
|
||||
| `kanban_list` | List board tasks with filters. Orchestrator-only; hidden from dispatcher-spawned task workers. | profile with `kanban` toolset |
|
||||
| `kanban_complete` | Mark the current task done with a structured handoff payload (results, artifacts, follow-ups). | `HERMES_KANBAN_TASK` or `kanban` toolset |
|
||||
| `kanban_block` | Block the current task on a question for the user — the dispatcher pauses, surfaces the question, and resumes once a human replies. | `HERMES_KANBAN_TASK` or `kanban` toolset |
|
||||
| `kanban_heartbeat` | Send a progress heartbeat during a long-running operation so the dispatcher knows the worker is still alive. | `HERMES_KANBAN_TASK` or `kanban` toolset |
|
||||
| `kanban_comment` | Add a comment to the task thread without changing its state — useful for surfacing intermediate findings. | `HERMES_KANBAN_TASK` or `kanban` toolset |
|
||||
| `kanban_create` | Fan out child tasks from the current task. Used by orchestrators and follow-up-spawning workers. | `HERMES_KANBAN_TASK` or `kanban` toolset |
|
||||
| `kanban_link` | Link tasks with a parent → child dependency edge. | `HERMES_KANBAN_TASK` or `kanban` toolset |
|
||||
| `kanban_unblock` | Return a blocked task to `ready`. Orchestrator-only; hidden from dispatcher-spawned task workers. | profile with `kanban` toolset |
|
||||
|
||||
## `memory` toolset
|
||||
|
||||
@@ -179,7 +181,7 @@ Registered only when the agent is spawned by the kanban dispatcher (`HERMES_KANB
|
||||
|
||||
| Tool | Description | Requires environment |
|
||||
|------|-------------|----------------------|
|
||||
| `vision_analyze` | Analyze images using AI vision. Provides a comprehensive description and answers a specific question about the image content. | — |
|
||||
| `vision_analyze` | Analyze images using AI vision. On vision-capable main models, returns the raw image pixels as a multimodal tool result so the model sees them natively on its next turn. On text-only main models, falls back to an auxiliary vision model that describes the image and returns the description as text. Tool signature is identical either way. | — |
|
||||
|
||||
## `video` toolset
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ Or in-session:
|
||||
| `computer_use` | `computer_use` | Background macOS desktop control via cua-driver — does not steal cursor/focus. Works with any tool-capable model. macOS only; requires `cua-driver` on `$PATH`. |
|
||||
| `image_gen` | `image_generate` | Text-to-image generation via FAL.ai (with opt-in OpenAI / xAI backends). |
|
||||
| `video_gen` | `video_generate` | Text-to-video and image-to-video via plugin-registered backends (xAI Grok-Imagine, FAL.ai Veo 3.1 / Pixverse v6 / Kling O3). Pass `image_url` to animate an image; omit it for text-to-video. |
|
||||
| `kanban` | `kanban_block`, `kanban_comment`, `kanban_complete`, `kanban_create`, `kanban_heartbeat`, `kanban_link`, `kanban_show` | Multi-agent coordination tools — only registered when the agent is spawned by the kanban dispatcher (`HERMES_KANBAN_TASK` env set). Lets workers mark tasks done with structured handoffs, block for human input, heartbeat during long ops, comment on threads, and (for orchestrators) fan out into child tasks. |
|
||||
| `kanban` | `kanban_block`, `kanban_comment`, `kanban_complete`, `kanban_create`, `kanban_heartbeat`, `kanban_link`, `kanban_list`, `kanban_show`, `kanban_unblock` | Multi-agent coordination tools. Registered for dispatcher-spawned task workers (`HERMES_KANBAN_TASK`) and for profiles that explicitly enable the `kanban` toolset. Workers mark tasks done, block, heartbeat, comment, and create/link follow-up tasks; orchestrator profiles additionally get board-routing tools like list/unblock. |
|
||||
| `memory` | `memory` | Persistent cross-session memory management. |
|
||||
| `messaging` | `send_message` | Send messages to other platforms (Telegram, Discord, etc.) from within a session. |
|
||||
| `moa` | `mixture_of_agents` | Multi-model consensus via Mixture of Agents. |
|
||||
|
||||
@@ -68,9 +68,12 @@ A persistent status bar sits above the input area, updating in real time:
|
||||
| Token count | Context tokens used / max context window |
|
||||
| Context bar | Visual fill indicator with color-coded thresholds |
|
||||
| Cost | Estimated session cost (or `n/a` for unknown/zero-priced models) |
|
||||
| 🗜️ N | **Context compression count** — how many times the running session has been auto-compressed. Appears once the first compression fires. |
|
||||
| ▶ N | **Active background tasks** — how many `/background` prompts are still running in the current session. Appears whenever at least one task is in flight. |
|
||||
| Duration | Elapsed session time |
|
||||
| ⚠ YOLO | **YOLO mode warning** — shown whenever `HERMES_YOLO_MODE` is on (either `hermes --yolo` at launch or `/yolo` toggled mid-session). Mirrors the banner-line warning so you can't forget you're in auto-approve mode. |
|
||||
|
||||
The bar adapts to terminal width — full layout at ≥ 76 columns, compact at 52–75, minimal (model + duration only) below 52.
|
||||
The bar adapts to terminal width — full layout at ≥ 76 columns, compact at 52–75, minimal (model + duration, plus the YOLO badge when active) below 52.
|
||||
|
||||
**Context color coding:**
|
||||
|
||||
@@ -125,6 +128,8 @@ Common examples:
|
||||
| `/voice tts` | Toggle spoken playback for Hermes replies |
|
||||
| `/reasoning high` | Increase reasoning effort |
|
||||
| `/title My Session` | Name the current session |
|
||||
| `/status` | Show session info — model/profile/tokens/duration — followed by a local **Session recap** block (recent turn counts, top tools used, files touched, latest user prompt + assistant reply). Pure local compute; no LLM call. |
|
||||
| `/sessions` | Open an interactive session picker right inside the classic CLI (same surface the TUI uses). Type to filter, arrow keys to navigate, Enter to resume. |
|
||||
|
||||
For the full built-in CLI and messaging lists, see [Slash Commands Reference](../reference/slash-commands.md).
|
||||
|
||||
|
||||
@@ -140,6 +140,9 @@ terminal:
|
||||
docker_volumes: # Host directory mounts
|
||||
- "/home/user/projects:/workspace/projects"
|
||||
- "/home/user/data:/data:ro" # :ro for read-only
|
||||
docker_extra_args: # Extra flags appended verbatim to `docker run`
|
||||
- "--gpus=all"
|
||||
- "--network=host"
|
||||
|
||||
# Resource limits
|
||||
container_cpu: 1 # CPU cores (0 = unlimited)
|
||||
@@ -148,6 +151,8 @@ terminal:
|
||||
container_persistent: true # Persist /workspace and /root across sessions
|
||||
```
|
||||
|
||||
**`terminal.docker_extra_args`** (also overridable via `TERMINAL_DOCKER_EXTRA_ARGS='["--gpus=all"]'`) lets you pass arbitrary `docker run` flags that Hermes doesn't surface as first-class keys — `--gpus`, `--network`, `--add-host`, alternative `--security-opt` overrides, etc. Each entry must be a string; the list is appended last to the assembled `docker run` invocation so it can override Hermes' defaults if needed. Use sparingly — flags that conflict with the sandbox hardening (capability drops, `--user`, the workspace bind mount) will silently weaken isolation.
|
||||
|
||||
**Requirements:** Docker Desktop or Docker Engine installed and running. Hermes probes `$PATH` plus common macOS install locations (`/usr/local/bin/docker`, `/opt/homebrew/bin/docker`, Docker Desktop app bundle). Podman is supported out of the box: set `HERMES_DOCKER_BINARY=podman` (or the full path) to force it when both are installed.
|
||||
|
||||
**Container lifecycle:** Hermes reuses a single long-lived container (`docker run -d ... sleep 2h`) for every terminal and file-tool call, across sessions, `/new`, `/reset`, and `delegate_task` subagents, for the lifetime of the Hermes process. Commands run via `docker exec` with a login shell, so working-directory changes, installed packages, and files in `/workspace` all persist from one tool call to the next. The container is stopped and removed on Hermes shutdown (or when the idle-sweep reclaims it).
|
||||
@@ -762,6 +767,16 @@ credential_pool_strategies:
|
||||
|
||||
Options: `fill_first` (default), `round_robin`, `least_used`, `random`. See [Credential Pools](/docs/user-guide/features/credential-pools) for full documentation.
|
||||
|
||||
## Prompt caching
|
||||
|
||||
Hermes turns on cross-session prompt caching automatically when the active provider supports it — no user config needed.
|
||||
|
||||
For Claude on **native Anthropic**, **OpenRouter**, and **Nous Portal**, Hermes attaches `cache_control` breakpoints with the 1-hour TTL (`ttl: "1h"`) on the system prompt and skill blocks. The first send within a fresh hour pays full input rates; subsequent sends across any session within the same hour pull from the cache at the discounted cached-read rate. This means the system prompt, loaded skill content, and the early portion of any long-context include get reused across `hermes` sessions and across forked subagents for the first hour.
|
||||
|
||||
The Qwen Cloud (Alibaba DashScope) upstream caps cache TTL at 5 minutes, so Hermes uses the 5-minute breakpoint TTL there instead. Other Claude-via-third-party paths (AWS Bedrock, Azure Foundry) fall back to the provider's own caching defaults. xAI Grok uses a separate session-pinned conversation-id mechanism — see [xAI prompt caching](/docs/integrations/providers#xai-grok--responses-api--prompt-caching).
|
||||
|
||||
No knob exists to disable this — caching is always-on and saves money even on single-turn conversations because the system prompt alone is a meaningful fraction of the input token count.
|
||||
|
||||
## Auxiliary Models
|
||||
|
||||
Hermes uses "auxiliary" models for side tasks like image analysis, web page summarization, browser screenshot analysis, session-title generation, and context compression. By default (`auxiliary.*.provider: "auto"`), Hermes routes every auxiliary task to your **main chat model** — the same provider/model you picked in `hermes model`. You don't need to configure anything to get started, but be aware that on expensive reasoning models (Opus, MiniMax M2.7, etc.) auxiliary tasks add meaningful cost. If you want cheap-and-fast side tasks regardless of your main model, set `auxiliary.<task>.provider` and `auxiliary.<task>.model` explicitly (for example, Gemini Flash on OpenRouter for vision and web extraction).
|
||||
@@ -1168,12 +1183,13 @@ display:
|
||||
show_reasoning: false # Show model reasoning/thinking above each response (toggle with /reasoning show|hide)
|
||||
streaming: false # Stream tokens to terminal as they arrive (real-time output)
|
||||
show_cost: false # Show estimated $ cost in the CLI status bar
|
||||
timestamps: false # When true, prefixes user and assistant labels with [HH:MM] timestamps in the CLI / TUI transcript
|
||||
tool_preview_length: 0 # Max chars for tool call previews (0 = no limit, show full paths/commands)
|
||||
runtime_footer: # Gateway: append a runtime-context footer to final replies
|
||||
enabled: false
|
||||
fields: ["model", "context_pct", "cwd"]
|
||||
file_mutation_verifier: true # Append an advisory footer when write_file/patch calls failed this turn
|
||||
language: en # UI language for static messages (approval prompts, some gateway replies). en | zh | ja | de | es | fr | tr | uk
|
||||
language: en # UI language for static messages (approval prompts, some gateway replies). en | zh | zh-hant | ja | de | es | fr | tr | uk | af | ko | it | ga | pt | ru | hu
|
||||
```
|
||||
|
||||
### File-mutation verifier
|
||||
|
||||
@@ -196,6 +196,10 @@ docker run -it --rm \
|
||||
|
||||
Direct `-e` flags override values from `.env`. This is useful for CI/CD or secrets-manager integrations where you don't want keys on disk.
|
||||
|
||||
:::note Looking for Docker as the **terminal backend**?
|
||||
This page covers running Hermes itself inside Docker. If you want Hermes to execute the agent's `terminal` / `execute_code` calls inside a Docker sandbox container (one persistent container per Hermes process), that's a separate config block — `terminal.backend: docker` plus `terminal.docker_image`, `terminal.docker_volumes`, `terminal.docker_forward_env`, `terminal.docker_run_as_host_user`, and `terminal.docker_extra_args`. See [Configuration → Docker Backend](configuration.md#docker-backend) for the full set.
|
||||
:::
|
||||
|
||||
## Docker Compose example
|
||||
|
||||
For persistent deployment with both the gateway and dashboard, a `docker-compose.yaml` is convenient:
|
||||
|
||||
@@ -218,6 +218,21 @@ Dangerous terminal commands can be routed back to the editor as approval prompts
|
||||
|
||||
On timeout or error, the approval bridge denies the request.
|
||||
|
||||
### Session-scoped edit auto-approval
|
||||
|
||||
ACP exposes a third tier between *allow once* and *allow always*: **Allow for session**. Picking it from the editor's permission prompt records the approval inside the current ACP session only — every subsequent matching command in that session goes through without prompting, but a new ACP session (or restarting the editor) resets the slate and re-prompts the first time.
|
||||
|
||||
| Option | Editor label | Scope | Persisted across restarts |
|
||||
|---|---|---|---|
|
||||
| `allow_once` | Allow once | This one tool call | No |
|
||||
| `allow_session` | Allow for session | All matching calls in this ACP session | No — cleared when the session ends |
|
||||
| `allow_always` | Allow always | All future sessions | Yes (written to the Hermes permanent allowlist) |
|
||||
| `deny` | Deny | This one tool call | No |
|
||||
|
||||
`allow_session` is the right default for an editor workflow where you trust an agent for the duration of a task but don't want to grant a long-lived allowlist entry. The safety trade-off is straightforward: the broader the scope, the less the editor will interrupt you, and the more damage a misbehaving agent (or prompt injection) can do before you notice. Start with `allow_once` for unfamiliar commands; promote to `allow_session` once you've seen the agent run the same pattern correctly a few times; reserve `allow_always` for truly idempotent commands you trust forever (e.g. `git status`).
|
||||
|
||||
The ACP bridge maps these options onto Hermes' internal approval semantics — `allow_always` writes a permanent allowlist entry the same way the CLI does, while `allow_session` only affects the in-process approval cache for the current ACP session.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### ACP agent does not appear in the editor
|
||||
|
||||
@@ -95,7 +95,7 @@ What also works because the MCP callback exposes them:
|
||||
- **`kanban_show` / `kanban_list`** — read-only board queries for the worker to check its own context.
|
||||
- **`kanban_create` / `kanban_unblock` / `kanban_link`** — orchestrator-only operations. Available for orchestrator agents running on the codex runtime that need to dispatch new tasks.
|
||||
|
||||
The kanban tools are gated by `HERMES_KANBAN_TASK` env var the dispatcher sets — that var is propagated to the codex subprocess (codex inherits env) and from there to the spawned `hermes-tools` MCP server subprocess. So the tools see the right task id and gate correctly. For Codex app-server workers, Hermes also passes narrow app-server sandbox overrides when `HERMES_KANBAN_TASK` is present: keep `workspace-write` sandboxing, add only the current board directory (derived from `HERMES_KANBAN_DB`) as an extra writable root, and keep network disabled by default. This avoids the brittle `:danger-no-sandbox` workaround while letting `kanban_complete` / `kanban_block` update the board DB.
|
||||
The kanban tools are gated by `HERMES_KANBAN_TASK` env var the dispatcher sets — that var is propagated to the codex subprocess (codex inherits env) and from there to the spawned `hermes-tools` MCP server subprocess. So the tools see the right task id and gate correctly. For Codex app-server workers, Hermes also passes narrow app-server sandbox overrides when `HERMES_KANBAN_TASK` is present: keep `workspace-write` sandboxing, add the **board DB directory plus every Kanban path the dispatcher pinned** as extra writable roots (`HERMES_KANBAN_WORKSPACES_ROOT`, `HERMES_KANBAN_WORKSPACE`, legacy `HERMES_KANBAN_ROOT` — deduplicated, DB-dir first), and keep network disabled by default. This avoids the brittle `:danger-no-sandbox` workaround while letting `kanban_complete` / `kanban_block` update the board DB **and** letting workers write reports/artifacts under workspace mounts that live outside the DB directory (e.g. `/media/.../kanban-workspaces/...` on a separate drive — [issue #27941](https://github.com/NousResearch/hermes-agent/issues/27941)).
|
||||
|
||||
### Cron jobs
|
||||
|
||||
|
||||
@@ -121,6 +121,35 @@ When `workdir` is set:
|
||||
Jobs with a `workdir` run sequentially on the scheduler tick, not in the parallel pool. This is deliberate — `TERMINAL_CWD` is process-global, so two workdir jobs running at the same time would corrupt each other's cwd. Workdir-less jobs still run in parallel as before.
|
||||
:::
|
||||
|
||||
## Running cron jobs in a specific profile
|
||||
|
||||
By default a cron job inherits whichever Hermes profile owned the gateway / CLI that created it. Pass `--profile <name>` (CLI) or `profile=` (cronjob tool) to re-target the job at a different profile — the scheduler resolves that profile's `HERMES_HOME`, temporarily switches into it for the duration of the run, loads its `.env` + `config.yaml`, and executes the job there:
|
||||
|
||||
```bash
|
||||
# Pin a job to the `night-ops` profile regardless of where it was scheduled
|
||||
hermes cron create "every 1d at 03:00" \
|
||||
"Tail the security log and flag anomalies" \
|
||||
--profile night-ops
|
||||
```
|
||||
|
||||
```python
|
||||
# From a chat, via the cronjob tool
|
||||
cronjob(
|
||||
action="create",
|
||||
schedule="every 1d at 03:00",
|
||||
prompt="Tail the security log and flag anomalies",
|
||||
profile="night-ops",
|
||||
)
|
||||
```
|
||||
|
||||
Use `--profile default` to explicitly pin to the root Hermes profile. The named profile must already exist; the scheduler refuses to create profiles on the fly. To clear a profile pin during `cron edit`, pass an empty string (`--profile ""` or `profile=""`) — the job reverts to running in whatever profile the scheduler itself is in.
|
||||
|
||||
If the pinned profile is later deleted, the scheduler logs a warning and falls back to running the job in its current profile rather than crashing — so a stale `profile` reference never wedges a job.
|
||||
|
||||
:::note Serialization
|
||||
Jobs with a `profile` set also run sequentially, for the same reason as `workdir`-pinned jobs: switching `HERMES_HOME` is a process-global mutation, so two profile-pinned jobs running in parallel would race each other. Unpinned jobs still run in the normal parallel pool.
|
||||
:::
|
||||
|
||||
## Editing jobs
|
||||
|
||||
You do not need to delete and recreate jobs just to change them.
|
||||
@@ -258,6 +287,17 @@ Semantics: `all` expands to every platform with a configured home channel. Zero
|
||||
|
||||
`all` composes with explicit targets. `origin,all` delivers to the origin chat *plus* every other connected home channel, de-duplicating by `(platform, chat_id, thread_id)`.
|
||||
|
||||
### Telegram cron topic (`TELEGRAM_CRON_THREAD_ID`)
|
||||
|
||||
When Telegram topic mode is enabled, the root DM is reserved as a system lobby — replies sent there are rebuffed with a lobby reminder and `reply_to_message_id` is dropped, so you cannot reply to a cron message that landed in the main chat.
|
||||
|
||||
Point cron at a dedicated forum topic instead:
|
||||
|
||||
1. In Telegram, open the bot DM and create a topic named e.g. `Cron`. Long-press the topic header → **Copy link**; the trailing integer is the topic's `message_thread_id`.
|
||||
2. Set `TELEGRAM_CRON_THREAD_ID=<that id>` in your `.env`.
|
||||
|
||||
This applies only to cron deliveries. `TELEGRAM_HOME_CHANNEL_THREAD_ID` (used elsewhere, e.g. restart notifications) is unchanged. Explicit `deliver="telegram:chat_id:thread_id"` targets continue to win over the env var. Replies to cron messages now arrive in the existing topic session, so you can act on them directly.
|
||||
|
||||
### Response wrapping
|
||||
|
||||
By default, delivered cron output is wrapped with a header and footer so the recipient knows it came from a scheduled task:
|
||||
|
||||
@@ -217,6 +217,10 @@ Every curator run writes a timestamped directory under `~/.hermes/logs/curator/`
|
||||
|
||||
`REPORT.md` is a quick way to see what a given run did — which skills transitioned, what the LLM reviewer said, which skills it patched. Good for auditing without having to grep `agent.log`.
|
||||
|
||||
### Rename map in the summary
|
||||
|
||||
If a run consolidated multiple skills under an umbrella (or merged near-duplicates), the user-visible summary printed at the end of the run includes an explicit rename map showing every `old-name → new-name` pair the curator applied. This is in addition to per-skill transition lines, so when a wave of renames lands you can spot them at a glance without diffing the JSON report. The hint also surfaces under `hermes curator pin` so you can pin the umbrella name immediately if you want to lock the new label in.
|
||||
|
||||
## Restoring an archived skill
|
||||
|
||||
If the curator archived something you still want:
|
||||
|
||||
@@ -268,6 +268,7 @@ delegation:
|
||||
# orchestrator_enabled: true # Disable to force all children to leaf role.
|
||||
model: "google/gemini-3-flash-preview" # Optional provider/model override
|
||||
provider: "openrouter" # Optional built-in provider
|
||||
api_mode: anthropic_messages # optional; auto-detected from base_url for anthropic_messages endpoints
|
||||
|
||||
# Or use a direct custom endpoint instead of provider:
|
||||
delegation:
|
||||
@@ -277,6 +278,8 @@ delegation:
|
||||
# api_mode: "anthropic_messages" # Optional. Wire protocol override for base_url ("chat_completions", "codex_responses", or "anthropic_messages"). Empty = auto-detect from URL (e.g. /anthropic suffix). Set explicitly for endpoints the heuristic can't classify (Azure AI Foundry, MiniMax, Zhipu GLM, LiteLLM proxies, …).
|
||||
```
|
||||
|
||||
When `base_url` points at an Anthropic-compatible endpoint — for example a path ending in `/anthropic`, an Azure Foundry Claude route, or a MiniMax `/anthropic` proxy — `api_mode` is auto-detected as `anthropic_messages` so the subagent uses the right wire format without you setting anything. Set `api_mode` explicitly when the auto-detection guess is wrong (rare).
|
||||
|
||||
:::tip
|
||||
The agent handles delegation automatically based on the task complexity. You don't need to explicitly ask it to delegate — it will do so when it makes sense.
|
||||
:::
|
||||
|
||||
@@ -81,7 +81,7 @@ Both `provider` and `model` are **required**. If either is missing, the fallback
|
||||
| Kimi / Moonshot (China) | `kimi-coding-cn` | `KIMI_CN_API_KEY` |
|
||||
| StepFun | `stepfun` | `STEPFUN_API_KEY` |
|
||||
| Tencent TokenHub | `tencent-tokenhub` | `TOKENHUB_API_KEY` |
|
||||
| Azure AI Foundry | `azure-foundry` | `AZURE_FOUNDRY_API_KEY` + `AZURE_FOUNDRY_BASE_URL` |
|
||||
| Microsoft Foundry | `azure-foundry` | `AZURE_FOUNDRY_API_KEY` + `AZURE_FOUNDRY_BASE_URL` |
|
||||
| LM Studio (local) | `lmstudio` | `LM_API_KEY` (or none for local) + `LM_BASE_URL` |
|
||||
| Hugging Face | `huggingface` | `HF_TOKEN` |
|
||||
| Custom endpoint | `custom` | `base_url` + `key_env` (see below) |
|
||||
|
||||
@@ -47,6 +47,21 @@ What you'll see:
|
||||
|
||||
Works identically on the CLI and every gateway platform (Telegram, Discord, Slack, Matrix, Signal, WhatsApp, SMS, iMessage, Webhook, API server, and the web dashboard).
|
||||
|
||||
## Adding criteria mid-goal: `/subgoal`
|
||||
|
||||
While a goal is active you can append extra acceptance criteria with `/subgoal <text>` without resetting the loop. Each call adds one numbered item to the goal's subgoal list; the **continuation prompt** the agent sees on the next turn includes the original goal plus an "Additional criteria the user added mid-loop" block, and the **judge prompt** is rewritten so the verdict must consider every subgoal — the goal isn't marked done until the original objective **and** every subgoal are met.
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `/subgoal <text>` | Append a new criterion to the active goal. Requires an active `/goal`. |
|
||||
| `/subgoal` (no args) | Show the current numbered subgoal list. |
|
||||
| `/subgoal remove <N>` | Remove the Nth subgoal (1-based). |
|
||||
| `/subgoal clear` | Drop every subgoal but keep the original goal intact. |
|
||||
|
||||
Subgoals are persisted alongside the goal in `SessionDB.state_meta`, so they survive `/resume`. Setting a new `/goal <text>` replaces the goal and clears the subgoal list; `/goal clear` does the same.
|
||||
|
||||
Use this when you start a loop ("fix the failing tests") and notice partway through that you also want it to "and add a regression test for the bug you just patched" — `/subgoal add a regression test` tightens the success criteria without breaking the running loop.
|
||||
|
||||
## Behavior details
|
||||
|
||||
### The judge
|
||||
|
||||
@@ -236,10 +236,11 @@ A deploy task that can't spawn its worker because `AWS_ACCESS_KEY_ID` isn't set
|
||||
|
||||
```bash
|
||||
hermes kanban create "Deploy to staging (missing creds)" \
|
||||
--assignee deploy-bot --tenant ops
|
||||
--assignee deploy-bot --tenant ops \
|
||||
--max-retries 3
|
||||
```
|
||||
|
||||
The dispatcher tries to spawn the worker. Spawn fails (`RuntimeError: AWS_ACCESS_KEY_ID not set`). The dispatcher releases the claim, increments a failure counter, and tries again next tick. After three consecutive failures (the default `failure_limit`), the circuit trips: the task goes to `blocked` with outcome `gave_up`. No more retries until a human unblocks it.
|
||||
The dispatcher tries to spawn the worker. Spawn fails (`RuntimeError: AWS_ACCESS_KEY_ID not set`). The dispatcher releases the claim, increments a failure counter, and tries again next tick. Because this example sets `--max-retries 3`, the circuit trips after three consecutive failures: the task goes to `blocked` with outcome `gave_up`. If you omit the flag, Hermes uses `kanban.failure_limit` (default: 2). No more retries until a human unblocks it.
|
||||
|
||||
Click the blocked task:
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ They coexist: a kanban worker may call `delegate_task` internally during its run
|
||||
- **Workspace** — the directory a worker operates in. Three kinds:
|
||||
- `scratch` (default) — fresh tmp dir under `~/.hermes/kanban/workspaces/<id>/` (or `~/.hermes/kanban/boards/<slug>/workspaces/<id>/` on non-default boards).
|
||||
- `dir:<path>` — an existing shared directory (Obsidian vault, mail ops dir, per-account folder). **Must be an absolute path.** Relative paths like `dir:../tenants/foo/` are rejected at dispatch because they'd resolve against whatever CWD the dispatcher happens to be in, which is ambiguous and a confused-deputy escape vector. The path is otherwise trusted — it's your box, your filesystem, the worker runs with your uid. This is the trusted-local-user threat model; kanban is single-host by design.
|
||||
- `worktree` — a git worktree under `.worktrees/<id>/` for coding tasks. Worker-side `git worktree add` creates it.
|
||||
- `worktree` — a git worktree under `.worktrees/<id>/` for coding tasks. Use `worktree:<path>` to pin the exact target path. Worker-side `git worktree add` creates it, using `--branch` when provided.
|
||||
- **Dispatcher** — a long-lived loop that, every N seconds (default 60): reclaims stale claims, reclaims crashed workers (PID gone but TTL not yet expired), promotes ready tasks, atomically claims, spawns assigned profiles. Runs **inside the gateway** by default (`kanban.dispatch_in_gateway: true`). One dispatcher sweeps all boards per tick; workers are spawned with `HERMES_KANBAN_BOARD` pinned so they can't see other boards. After `kanban.failure_limit` consecutive spawn failures on the same task (default: 2) the dispatcher auto-blocks it with the last error as the reason — prevents thrashing on tasks whose profile doesn't exist, workspace can't mount, etc.
|
||||
- **Tenant** — optional string namespace *within* a board. One specialist fleet can serve multiple businesses (`--tenant business-a`) with data isolation by workspace path and memory key prefix. Tenants are a soft filter; boards are the hard isolation boundary.
|
||||
|
||||
@@ -290,7 +290,7 @@ Three reasons:
|
||||
2. **No shell-quoting fragility.** Passing `--metadata '{"files": [...]}'` through shlex + argparse is a latent footgun. Structured tool args skip it entirely.
|
||||
3. **Better errors.** Tool results are structured JSON the model can reason about, not stderr strings it has to parse.
|
||||
|
||||
**Zero schema footprint on normal sessions.** A regular `hermes chat` session has zero `kanban_*` tools in its schema. The `check_fn` on each tool only returns True when `HERMES_KANBAN_TASK` is set, which only happens when the dispatcher spawned this process. No tool bloat for users who never touch kanban.
|
||||
**Zero schema footprint on normal sessions.** A regular `hermes chat` session has zero `kanban_*` tools in its schema unless the active profile explicitly enables the `kanban` toolset for orchestrator work. Dispatcher-spawned task workers get task-scoped tools because `HERMES_KANBAN_TASK` is set; orchestrator profiles get the broader routing surface through config. No tool bloat for users who never touch kanban.
|
||||
|
||||
The `kanban-worker` and `kanban-orchestrator` skills teach the model which tool to call when and in what order.
|
||||
|
||||
@@ -334,9 +334,16 @@ Any profile that should be able to work kanban tasks must load the `kanban-worke
|
||||
|
||||
1. On spawn, call `kanban_show()` to read title + body + parent handoffs + prior attempts + full comment thread.
|
||||
2. `cd $HERMES_KANBAN_WORKSPACE` (via the terminal tool) and do the work there.
|
||||
3. Call `kanban_heartbeat(note="...")` every few minutes during long operations.
|
||||
3. Call `kanban_heartbeat(note="...")` every few minutes during long operations. **If your work may run longer than 1 hour, call `kanban_heartbeat` at least once an hour** — the dispatcher reclaims tasks that have been running past `kanban.dispatch_stale_timeout_seconds` (default 4 h) with no heartbeat in the last hour, on the assumption the worker crashed without cleanup. A reclaim is benign (the task goes back to `ready` for re-dispatch without a failure-counter tick) but you lose your current run's progress.
|
||||
4. Complete with `kanban_complete(summary="...", metadata={...})`, or `kanban_block(reason="...")` if stuck.
|
||||
|
||||
That final `kanban_complete` / `kanban_block` call is part of the worker
|
||||
protocol. If the worker process exits with status 0 while the task is still
|
||||
`running`, the dispatcher treats that as a protocol violation, emits a
|
||||
`protocol_violation` event, and auto-blocks the task on the next tick instead
|
||||
of respawning it into the same loop. This usually means the model wrote a
|
||||
plain-text answer and exited without using the Kanban tool surface.
|
||||
|
||||
`kanban-worker` is a bundled skill, synced into every profile during install and
|
||||
update — there is no separate Skills Hub install step. Verify it is present in
|
||||
whichever profile you use for kanban workers (`researcher`, `writer`, `ops`,
|
||||
@@ -449,7 +456,7 @@ hermes dashboard # "Kanban" tab appears in the nav, after "Skills"
|
||||
- **Per-profile lanes inside Running** — toolbar checkbox toggles sub-grouping of the Running column by assignee.
|
||||
- **Live updates via WebSocket** — the plugin tails the append-only `task_events` table on a short poll interval; the board reflects changes the instant any profile (CLI, gateway, or another dashboard tab) acts. Reloads are debounced so a burst of events triggers a single refetch.
|
||||
- **Drag-drop** cards between columns to change status. The drop sends `PATCH /api/plugins/kanban/tasks/:id` which routes through the same `kanban_db` code the CLI uses — the three surfaces can never drift. Moves into destructive statuses (`done`, `archived`, `blocked`) prompt for confirmation. Touch devices use a pointer-based fallback so the board is usable from a tablet.
|
||||
- **Inline create** — click `+` on any column header to type a title, assignee, priority, and (optionally) a parent task from a dropdown over every existing task. Creating from the Triage column automatically parks the new task in triage.
|
||||
- **Inline create** — click `+` on any column header to type a title, assignee, priority, and (optionally) a parent task from a dropdown over every existing task. Press Enter to create the task, Shift+Enter to insert a newline in the title field, or Escape to cancel. Creating from the Triage column automatically parks the new task in triage.
|
||||
- **Multi-select with bulk actions** — shift/ctrl-click a card or tick its checkbox to add it to the selection. A bulk action bar appears at the top with batch status transitions, archive, and reassign (by profile dropdown, or "(unassign)"). Destructive batches confirm first. Per-id partial failures are reported without aborting the rest.
|
||||
- **Click a card** (without shift/ctrl) to open a side drawer (Escape or click-outside closes) with:
|
||||
- **Editable title** — click the heading to rename.
|
||||
@@ -494,6 +501,7 @@ And the two auxiliary LLM slots:
|
||||
|
||||
The GUI is strictly a **read-through-the-DB + write-through-kanban_db** layer with no domain logic of its own:
|
||||
|
||||
<!-- ascii-guard-ignore -->
|
||||
```
|
||||
┌────────────────────────┐ WebSocket (tails task_events)
|
||||
│ React SPA (plugin) │ ◀──────────────────────────────────┐
|
||||
@@ -513,6 +521,7 @@ The GUI is strictly a **read-through-the-DB + write-through-kanban_db** layer wi
|
||||
│ (WAL, shared) │
|
||||
└────────────────────────┘
|
||||
```
|
||||
<!-- ascii-guard-ignore-end -->
|
||||
|
||||
### REST surface
|
||||
|
||||
@@ -588,9 +597,11 @@ This is the surface **you** (or scripts, cron, the dashboard) use to drive the b
|
||||
hermes kanban init # create kanban.db + print daemon hint
|
||||
hermes kanban create "<title>" [--body ...] [--assignee <profile>]
|
||||
[--parent <id>]... [--tenant <name>]
|
||||
[--workspace scratch|worktree|dir:<path>]
|
||||
[--workspace scratch|worktree|worktree:<path>|dir:<path>]
|
||||
[--branch <name>]
|
||||
[--priority N] [--triage] [--idempotency-key KEY]
|
||||
[--max-runtime 30m|2h|1d|<seconds>]
|
||||
[--max-retries N]
|
||||
[--skill <name>]...
|
||||
[--json]
|
||||
hermes kanban list [--mine] [--assignee P] [--status S] [--tenant T] [--archived] [--json]
|
||||
@@ -633,6 +644,8 @@ hermes kanban gc [--event-retention-days N] # workspaces + old events
|
||||
|
||||
All commands are also available as a slash command in the interactive CLI and in the messaging gateway (see [`/kanban` slash command](#kanban-slash-command) below).
|
||||
|
||||
`--max-retries` is a per-task circuit-breaker override for the dispatcher. `--max-retries 1` blocks the task on the first non-successful attempt, while `--max-retries 3` allows two retries and blocks on the third failure. Omit it to use `kanban.failure_limit` from `config.yaml`, then the built-in default.
|
||||
|
||||
## `/kanban` slash command {#kanban-slash-command}
|
||||
|
||||
Every `hermes kanban <action>` verb is also reachable as `/kanban <action>` — from inside an interactive `hermes chat` session **and** from any gateway platform (Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, email, SMS). Both surfaces call the exact same `hermes_cli.kanban.run_slash()` entry point that reuses the `hermes kanban` argparse tree, so the argument surface, flags, and output format are identical across CLI, `/kanban`, and `hermes kanban`. You don't have to leave the chat to drive the board.
|
||||
@@ -820,8 +833,11 @@ Every transition appends a row to `task_events`. Each row carries an optional `r
|
||||
| `reclaimed` | `{stale_lock}` | Claim TTL expired without a completion; task goes back to `ready`. |
|
||||
| `crashed` | `{pid, claimer}` | Worker PID no longer alive but TTL hadn't expired yet. |
|
||||
| `timed_out` | `{pid, elapsed_seconds, limit_seconds, sigkill}` | `max_runtime_seconds` exceeded; dispatcher SIGTERM'd (then SIGKILL'd after 5 s grace) and re-queued. |
|
||||
| `stale` | `{elapsed_seconds, last_heartbeat_at, heartbeat_age_seconds, timeout_seconds, pid, terminated}` | Task ran longer than `kanban.dispatch_stale_timeout_seconds` (default 4 h) AND no `kanban_heartbeat` arrived in the last hour. Dispatcher SIGTERM'd the host-local worker (if any), reset the task to `ready` for re-dispatch. Does NOT tick the failure counter (stale is dispatcher-side absence detection, not a worker fault). Workers running long operations should call `kanban_heartbeat` at least once an hour to avoid this. |
|
||||
| `respawn_guarded` | `{reason}` | Dispatcher refused to re-spawn this ready task this tick. Reasons: `blocker_auth` (last failure was a quota/auth/429 error — wait for the rate window to reset), `recent_success` (a completed run happened in the last hour — wait for review before re-running), `active_pr` (a GitHub PR URL appears in a recent comment — a prior worker already opened a PR). The task stays in `ready`; the next tick gets another chance to spawn. If the underlying condition persists, the normal `consecutive_failures` circuit breaker will auto-block via `gave_up` after `failure_limit` failures. |
|
||||
| `spawn_failed` | `{error, failures}` | One spawn attempt failed (missing PATH, workspace unmountable, …). Counter increments; task returns to `ready` for retry. |
|
||||
| `gave_up` | `{failures, error}` | Circuit breaker fired after N consecutive `spawn_failed`. Task auto-blocks with the last error. Default N = 5; override via `--failure-limit`. |
|
||||
| `protocol_violation` | `{pid, claimer, exit_code}` | Worker exited successfully while the task was still `running`, usually because it answered without calling `kanban_complete` or `kanban_block`. The dispatcher also emits `gave_up` and auto-blocks immediately instead of retrying. |
|
||||
| `gave_up` | `{failures, effective_limit, limit_source, error}` | Circuit breaker fired after N consecutive non-successful attempts. Task auto-blocks with the last error. The effective limit resolves as task `max_retries`, then dispatcher `failure_limit` / `kanban.failure_limit`, then the built-in default. |
|
||||
|
||||
`hermes kanban tail <id>` shows these for a single task. `hermes kanban watch` streams them board-wide.
|
||||
|
||||
|
||||
@@ -127,6 +127,30 @@ mcp_servers:
|
||||
Authorization: "Bearer ***"
|
||||
```
|
||||
|
||||
## Built-in presets
|
||||
|
||||
For well-known MCP servers, `hermes mcp add` accepts a `--preset` flag that fills in the transport details so you don't have to look up the command and args. The preset only supplies defaults — anything else (env vars, headers, filtering) you pass on the same command line still wins.
|
||||
|
||||
| Preset | What it wires up |
|
||||
|---|---|
|
||||
| `codex` | The Codex CLI's MCP server (`codex mcp-server` over stdio). Requires the `codex` CLI on PATH. |
|
||||
|
||||
```bash
|
||||
# Add Codex CLI as an MCP server in one line
|
||||
hermes mcp add codex --preset codex
|
||||
```
|
||||
|
||||
That writes the equivalent of:
|
||||
|
||||
```yaml
|
||||
mcp_servers:
|
||||
codex:
|
||||
command: "codex"
|
||||
args: ["mcp-server"]
|
||||
```
|
||||
|
||||
You can pick any local name (`hermes mcp add my-codex --preset codex` is fine); the preset only provides the `command`/`args` defaults.
|
||||
|
||||
## How Hermes registers MCP tools
|
||||
|
||||
Hermes prefixes MCP tools so they do not collide with built-in names:
|
||||
@@ -554,7 +578,7 @@ The gateway does NOT need to be running for read operations (listing conversatio
|
||||
|
||||
### Current limits
|
||||
|
||||
- Stdio transport only (no HTTP MCP transport yet)
|
||||
- The embedded `hermes mcp serve` exposes a **stdio-only** MCP server today. If you need an HTTP MCP server, run a separate adapter — or, much more commonly, use the MCP **client** side of Hermes, which already speaks both stdio and HTTP (`url` + `headers` in `mcp_servers.yaml` / `config.yaml`; see [HTTP servers](#http-servers) above).
|
||||
- Event polling at ~200ms intervals via mtime-optimized DB polling (skips work when files are unchanged)
|
||||
- No `claude/channel` push notification protocol yet
|
||||
- Text-only sends (no media/attachment sending through `messages_send`)
|
||||
|
||||
@@ -39,6 +39,7 @@ Hermes Agent includes a rich set of capabilities that extend far beyond basic ch
|
||||
- **[Provider Routing](provider-routing.md)** — Fine-grained control over which AI providers handle your requests. Optimize for cost, speed, or quality with sorting, whitelists, blacklists, and priority ordering.
|
||||
- **[Fallback Providers](fallback-providers.md)** — Automatic failover to backup LLM providers when your primary model encounters errors, including independent fallback for auxiliary tasks like vision and compression.
|
||||
- **[Credential Pools](credential-pools.md)** — Distribute API calls across multiple keys for the same provider. Automatic rotation on rate limits or failures.
|
||||
- **[Prompt caching](../configuration#prompt-caching)** — Built-in cross-session 1-hour prefix cache for Claude on native Anthropic, OpenRouter, and Nous Portal. Always-on; no configuration required.
|
||||
- **[Memory Providers](memory-providers.md)** — Plug in external memory backends (Honcho, OpenViking, Mem0, Hindsight, Holographic, RetainDB, ByteRover, Supermemory) for cross-session user modeling and personalization beyond the built-in memory system.
|
||||
- **[API Server](api-server.md)** — Expose Hermes as an OpenAI-compatible HTTP endpoint. Connect any frontend that speaks the OpenAI format — Open WebUI, LobeChat, LibreChat, and more.
|
||||
- **[IDE Integration (ACP)](acp.md)** — Use Hermes inside ACP-compatible editors such as VS Code, Zed, and JetBrains. Chat, tool activity, file diffs, and terminal commands render inside your editor.
|
||||
|
||||
@@ -107,6 +107,35 @@ platforms: [macos, linux] # macOS and Linux
|
||||
|
||||
When set, the skill is automatically hidden from the system prompt, `skills_list()`, and slash commands on incompatible platforms. If omitted, the skill loads on all platforms.
|
||||
|
||||
## Skill output and media delivery
|
||||
|
||||
When a skill response (or any agent response) includes a bare absolute path to a media file — for example `/home/user/screenshots/diagram.png` — the gateway auto-detects it, strips it from the visible text, and delivers the file natively to the user's chat (Telegram photo, Discord attachment, etc.) instead of leaving the raw path in the message.
|
||||
|
||||
For audio specifically, the `[[audio_as_voice]]` directive promotes audio files to native voice-message bubbles on platforms that support them (Telegram, WhatsApp).
|
||||
|
||||
### Forcing document-style delivery: `[[as_document]]`
|
||||
|
||||
Sometimes you want the **opposite** of inline preview: you want the file delivered as a downloadable attachment, not a re-compressed image bubble. The classic example is a high-resolution screenshot or chart — Telegram's `sendPhoto` recompresses it to ~200 KB at 1280 px, destroying readability. A 1-2 MB PNG sent via `sendDocument` keeps the original bytes intact.
|
||||
|
||||
If a response (or any text inside it — typically the last line) contains the literal directive `[[as_document]]`, every media path extracted from that response is delivered as a document/file attachment rather than an image bubble:
|
||||
|
||||
```
|
||||
Here is your rendered chart:
|
||||
|
||||
/home/user/.hermes/cache/chart-q4-2025.png
|
||||
|
||||
[[as_document]]
|
||||
```
|
||||
|
||||
The directive is stripped before delivery, so users never see it. Granularity is intentionally all-or-nothing per response: emit `[[as_document]]` once and every image path in the same response is delivered as a document. This mirrors the scope of `[[audio_as_voice]]`.
|
||||
|
||||
Use it from a skill when:
|
||||
|
||||
- You produce screenshots or charts the user needs as files (for editing in another tool, archiving, sharing intact).
|
||||
- The default lossy preview would obscure detail (small text, pixel-accurate diagrams, color-sensitive renders).
|
||||
|
||||
Platforms without a separate document path (e.g. SMS) fall back to whatever attachment mechanism they have.
|
||||
|
||||
### Conditional Activation (Fallback Skills)
|
||||
|
||||
Skills can automatically show or hide themselves based on which tools are available in the current session. This is most useful for **fallback skills** — free or local alternatives that should only appear when a premium tool is unavailable.
|
||||
@@ -230,6 +259,91 @@ Paths support `~` expansion and `${VAR}` environment variable substitution.
|
||||
|
||||
All four skills appear in your skill index. If you create a new skill called `my-custom-workflow` locally, it shadows the external version.
|
||||
|
||||
## Skill Bundles
|
||||
|
||||
Skill bundles are tiny YAML files that group several skills under a single slash command. When you run `/<bundle-name>`, every skill listed in the bundle loads at once — useful when a particular task always benefits from the same set of skills together.
|
||||
|
||||
### Quick example
|
||||
|
||||
```bash
|
||||
# Create a bundle for backend feature work
|
||||
hermes bundles create backend-dev \
|
||||
--skill github-code-review \
|
||||
--skill test-driven-development \
|
||||
--skill github-pr-workflow \
|
||||
-d "Backend feature work — review, test, PR workflow"
|
||||
```
|
||||
|
||||
Then in the CLI or any gateway platform:
|
||||
|
||||
```
|
||||
/backend-dev refactor the auth middleware
|
||||
```
|
||||
|
||||
The agent receives all three skills loaded into one user message, with any text after the slash command attached as a user instruction.
|
||||
|
||||
### YAML schema
|
||||
|
||||
Bundles live in **`~/.hermes/skill-bundles/<slug>.yaml`** and look like this:
|
||||
|
||||
```yaml
|
||||
name: backend-dev
|
||||
description: Backend feature work — review, test, PR workflow.
|
||||
skills:
|
||||
- github-code-review
|
||||
- test-driven-development
|
||||
- github-pr-workflow
|
||||
instruction: |
|
||||
Always start by writing failing tests, then implement.
|
||||
Open the PR through the standard workflow with co-author tags.
|
||||
```
|
||||
|
||||
Fields:
|
||||
- `name` (optional — defaults to the filename stem) — the bundle's display name. Normalized to a hyphen slug for the slash command (`Backend Dev` → `/backend-dev`).
|
||||
- `description` (optional) — short text shown in `/bundles` and `hermes bundles list`.
|
||||
- `skills` (required, non-empty list) — skill names or paths relative to your skills directory. Use the same identifier you'd pass to `/<skill-name>`.
|
||||
- `instruction` (optional) — extra guidance prepended to the loaded skill content. Useful for codifying "how we always use these together."
|
||||
|
||||
### Managing bundles
|
||||
|
||||
```bash
|
||||
# List all installed bundles
|
||||
hermes bundles list
|
||||
|
||||
# Inspect one bundle
|
||||
hermes bundles show backend-dev
|
||||
|
||||
# Create a bundle interactively (omit --skill flags to enter them one per line)
|
||||
hermes bundles create research
|
||||
|
||||
# Overwrite an existing bundle
|
||||
hermes bundles create backend-dev --skill ... --force
|
||||
|
||||
# Delete a bundle
|
||||
hermes bundles delete backend-dev
|
||||
|
||||
# Re-scan ~/.hermes/skill-bundles/ and report changes
|
||||
hermes bundles reload
|
||||
```
|
||||
|
||||
From inside a chat session, `/bundles` lists every installed bundle and its skills.
|
||||
|
||||
### Behavior
|
||||
|
||||
- **Bundles take precedence over individual skills** when slugs collide. If you name a bundle `research` and you also have a skill called `research`, `/research` invokes the bundle. This is intentional — you opted into the bundle by naming it.
|
||||
- **Missing skills are skipped, not fatal.** If a bundle lists `skill-foo` and you haven't installed it, the bundle still loads the skills that do resolve, and the agent gets a note listing what was skipped.
|
||||
- **Bundles work in every surface** — interactive CLI, TUI, dashboard chat, and every gateway platform (Telegram, Discord, Slack, …) — because dispatch is centralized in the same place as individual skill commands.
|
||||
- **Bundles do not invalidate the prompt cache.** They generate a fresh user message at invocation time, the same way `/<skill-name>` does — no system prompt mutation.
|
||||
|
||||
### When bundles beat installing each skill manually
|
||||
|
||||
Use a bundle when:
|
||||
- You always pair the same skills for a recurring task (`/backend-dev`, `/release-prep`, `/incident-response`).
|
||||
- You want a one-character-shorter mental model than typing several `/skill` invocations in a row.
|
||||
- You want to ship a team-wide "task profile" by checking the bundle YAML into a shared dotfiles repo and symlinking it into `~/.hermes/skill-bundles/`.
|
||||
|
||||
A bundle is just a YAML alias — it doesn't install skills for you. The skills themselves must already be present (in `~/.hermes/skills/` or an external skill directory). Otherwise the bundle invocation just skips the missing ones.
|
||||
|
||||
## Agent-Managed Skills (skill_manage tool)
|
||||
|
||||
The agent can create, update, and delete its own skills via the `skill_manage` tool. This is the agent's **procedural memory** — when it figures out a non-trivial workflow, it saves the approach as a skill for future reuse.
|
||||
@@ -296,7 +410,7 @@ hermes skills tap add myorg/skills-repo # Add a custom GitHub source
|
||||
| `well-known` | `well-known:https://mintlify.com/docs/.well-known/skills/mintlify` | Skills served directly from `/.well-known/skills/index.json` on a website. Search using the site or docs URL. |
|
||||
| `url` | `https://sharethis.chat/SKILL.md` | Direct HTTP(S) URL to a single-file `SKILL.md`. Name resolution: frontmatter → URL slug → interactive prompt → `--name` flag. |
|
||||
| `github` | `openai/skills/k8s` | Direct GitHub repo/path installs and custom taps. |
|
||||
| `clawhub`, `lobehub`, `claude-marketplace` | Source-specific identifiers | Community or marketplace integrations. |
|
||||
| `clawhub`, `lobehub`, `browse-sh`, `claude-marketplace` | Source-specific identifiers | Community or marketplace integrations. |
|
||||
|
||||
### Integrated hubs and registries
|
||||
|
||||
@@ -388,7 +502,24 @@ Hermes can search and convert agent entries from LobeHub's public catalog into i
|
||||
- Backing repo: [lobehub/lobe-chat-agents](https://github.com/lobehub/lobe-chat-agents)
|
||||
- Hermes source id: `lobehub`
|
||||
|
||||
#### 8. Direct URL (`url`)
|
||||
#### 8. browse.sh (`browse-sh`)
|
||||
|
||||
Hermes integrates with [browse.sh](https://browse.sh), Browserbase's catalog of 200+ site-specific browser-automation SKILL.md files (Airbnb, Amazon, arXiv, 12306.cn, Etsy, Xero, and many more). Each skill describes how to drive one website end-to-end and is suitable for use with Hermes' browser tools and any browser-automation skills you already have installed.
|
||||
|
||||
- Site: [browse.sh](https://browse.sh/)
|
||||
- Catalog API: `https://browse.sh/api/skills`
|
||||
- Hermes source id: `browse-sh`
|
||||
- Trust level: `community`
|
||||
|
||||
```bash
|
||||
hermes skills search airbnb --source browse-sh
|
||||
hermes skills inspect browse-sh/airbnb.com/search-listings-ddgioa
|
||||
hermes skills install browse-sh/airbnb.com/search-listings-ddgioa
|
||||
```
|
||||
|
||||
Identifiers use the form `browse-sh/<hostname>/<task-id>` and match the slug exposed by the browse.sh catalog. Content is resolved through the per-skill detail endpoint (`/api/skills/<slug>` → `skillMdUrl`), not through the catalog's GitHub `sourceUrl`.
|
||||
|
||||
#### 9. Direct URL (`url`)
|
||||
|
||||
Install a single-file `SKILL.md` directly from any HTTP(S) URL — useful when an author hosts a skill on their own site (no hub listing, no GitHub path to type). Hermes fetches the URL, parses the YAML frontmatter, security-scans it, and installs.
|
||||
|
||||
|
||||
@@ -202,3 +202,9 @@ When a user attaches an image — from the CLI clipboard, the gateway (Telegram/
|
||||
You don't configure this — Hermes looks up your current model's capability in the provider metadata and picks the right path automatically. The practical effect: you can switch between vision and non-vision models mid-session and image handling "just works" without changing your workflow. Text-only models get coherent context about the image rather than a broken multimodal payload they'd have to reject.
|
||||
|
||||
Which auxiliary model handles the text-description path is configurable under `auxiliary.vision` — see [Auxiliary Models](/docs/user-guide/configuration#auxiliary-models).
|
||||
|
||||
### `vision_analyze` has the same dual behavior
|
||||
|
||||
The `vision_analyze` tool itself follows the same routing. When the active main model is vision-capable **and** its provider supports image content inside tool results (currently the Anthropic, OpenAI, Azure-OpenAI, and Gemini 3.x stacks), `vision_analyze` short-circuits the auxiliary describer and returns the raw image pixels as a multimodal tool-result envelope. The main model sees the image natively on its next turn — no aux call, no text-summary information loss, no extra latency.
|
||||
|
||||
For text-only main models (or providers whose tool-result channel doesn't carry images), `vision_analyze` falls back to the legacy path: it asks the configured auxiliary vision model to describe the image and returns the description as plain text. Either way the calling tool signature is the same — the tool decides which path to take at runtime based on the active model.
|
||||
|
||||
@@ -391,6 +391,11 @@ voice:
|
||||
|
||||
# Speech-to-Text
|
||||
stt:
|
||||
enabled: true # set to false to skip auto-transcription —
|
||||
# the gateway still caches the audio file and
|
||||
# passes its path to the agent as part of the
|
||||
# inbound message, useful for custom pipelines
|
||||
# (diarization, alignment, archival, etc.)
|
||||
provider: "local" # "local" (free) | "groq" | "openai"
|
||||
local:
|
||||
model: "base" # tiny, base, small, medium, large-v3
|
||||
|
||||
@@ -20,9 +20,14 @@ Both are configured through a single backend selection. Providers are chosen via
|
||||
|----------|---------|--------|---------|-------|-----------|
|
||||
| **Firecrawl** (default) | `FIRECRAWL_API_KEY` | ✔ | ✔ | ✔ | 500 credits/mo |
|
||||
| **SearXNG** | `SEARXNG_URL` | ✔ | — | — | ✔ Free (self-hosted) |
|
||||
| **Brave Search (free tier)** | `BRAVE_SEARCH_API_KEY` | ✔ | — | — | 2 000 queries/mo |
|
||||
| **DDGS (DuckDuckGo)** | — (no key) | ✔ | — | — | ✔ Free |
|
||||
| **Tavily** | `TAVILY_API_KEY` | ✔ | ✔ | ✔ | 1 000 searches/mo |
|
||||
| **Exa** | `EXA_API_KEY` | ✔ | ✔ | — | 1 000 searches/mo |
|
||||
| **Parallel** | `PARALLEL_API_KEY` | ✔ | ✔ | — | Paid |
|
||||
| **xAI (Grok)** | `XAI_API_KEY` or `hermes auth login xai-oauth` | ✔ | — | — | Paid (SuperGrok or per-token) |
|
||||
|
||||
Brave Search, DDGS, and xAI are **search-only** — pair any of them with Firecrawl/Tavily/Exa/Parallel when you also need `web_extract`. DDGS uses the [`ddgs` Python package](https://pypi.org/project/ddgs/) under the hood; if it isn't already installed, run `pip install ddgs` (or let Hermes lazy-install it on first use). xAI runs Grok's server-side `web_search` tool on the Responses API — results are LLM-generated rather than index-backed, so titles, descriptions, and URL choice are all model output (see the [trust-model caveat](#xai-grok) below).
|
||||
|
||||
**Per-capability split:** you can use different providers for search and extract independently — for example SearXNG (free) for search and Firecrawl for extract. See [Per-capability configuration](#per-capability-configuration) below.
|
||||
|
||||
@@ -269,6 +274,53 @@ Get access at [parallel.ai](https://parallel.ai).
|
||||
|
||||
---
|
||||
|
||||
### xAI (Grok) {#xai-grok}
|
||||
|
||||
Routes `web_search` through Grok's server-side [web_search tool](https://docs.x.ai/developers/tools/web-search) on the Responses API. Grok runs the actual searching and returns the top results as structured JSON.
|
||||
|
||||
Works with either credential path — no new env vars, no new setup wizard:
|
||||
|
||||
```bash
|
||||
# ~/.hermes/.env (env-var path)
|
||||
XAI_API_KEY=sk-xai-your-key-here
|
||||
```
|
||||
|
||||
or for SuperGrok subscribers:
|
||||
|
||||
```bash
|
||||
hermes auth login xai-oauth
|
||||
```
|
||||
|
||||
Then select xAI as the search backend:
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/config.yaml
|
||||
web:
|
||||
backend: "xai"
|
||||
```
|
||||
|
||||
**Optional knobs:**
|
||||
|
||||
```yaml
|
||||
web:
|
||||
backend: "xai"
|
||||
xai:
|
||||
model: grok-4.3 # reasoning model required by web_search (default)
|
||||
allowed_domains: # optional, max 5 — mutex with excluded_domains
|
||||
- arxiv.org
|
||||
excluded_domains: # optional, max 5
|
||||
- example-spam.com
|
||||
timeout: 90 # seconds (default)
|
||||
```
|
||||
|
||||
**Search-only** — pair with Firecrawl / Tavily / Exa / Parallel if you also need `web_extract`. On 401 the provider performs a single forced OAuth-token refresh and retries (covers mid-window revocation and opaque tokens the proactive expiry check can't decode); env-var credentials skip the retry.
|
||||
|
||||
:::caution Trust model
|
||||
Unlike index-backed providers (Brave, Tavily, Exa) which return verbatim search-engine results, xAI is an LLM choosing which URLs to surface and writing the titles and descriptions itself. The *content* of the query influences the output, so a maliciously crafted query (e.g. injected via untrusted upstream input the agent picked up) can in principle steer Grok into emitting attacker-chosen URLs. Treat returned URLs the same way you'd treat any model-generated link — validate before fetching, especially if the query came from untrusted input.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Single backend
|
||||
@@ -278,7 +330,7 @@ Set one provider for all web capabilities:
|
||||
```yaml
|
||||
# ~/.hermes/config.yaml
|
||||
web:
|
||||
backend: "searxng" # firecrawl | searxng | tavily | exa | parallel
|
||||
backend: "searxng" # firecrawl | searxng | brave-free | ddgs | tavily | exa | parallel | xai
|
||||
```
|
||||
|
||||
### Per-capability configuration
|
||||
@@ -311,6 +363,8 @@ If no backend is explicitly configured, Hermes picks the first available one bas
|
||||
| `EXA_API_KEY` | exa |
|
||||
| `SEARXNG_URL` | searxng |
|
||||
|
||||
xAI Web Search is **not** in the auto-detection chain — having `XAI_API_KEY` set (or being signed in via xAI Grok OAuth) does not automatically route web traffic through xAI, since those credentials are also used for inference / TTS / image gen and the user may want a different backend for web. Opt in explicitly with `web.backend: "xai"`.
|
||||
|
||||
---
|
||||
|
||||
## Verify your setup
|
||||
|
||||
@@ -26,7 +26,7 @@ The tool's `check_fn` runs the xAI credential resolver every time the model's to
|
||||
|
||||
## Enabling the tool
|
||||
|
||||
Off by default. Enable in `hermes tools`:
|
||||
Auto-enables when xAI credentials (OAuth token or `XAI_API_KEY`) are present. Disable explicitly via `hermes tools` → Search → x_search if you don't want this.
|
||||
|
||||
```bash
|
||||
hermes tools
|
||||
|
||||
@@ -634,6 +634,24 @@ When the flag is on, any uploaded file is downloaded, cached under `~/.hermes/ca
|
||||
|
||||
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:
|
||||
|
||||
@@ -443,6 +443,84 @@ Each platform has its own toolset:
|
||||
| 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.
|
||||
|
||||
### Progress bubble cleanup (opt-in)
|
||||
|
||||
Tool-progress messages, the "still working…" heartbeat, and status-callback bubbles can 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)
|
||||
|
||||
@@ -345,6 +345,34 @@ Add this to your `~/.hermes/.env`:
|
||||
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 `!`).
|
||||
:::
|
||||
|
||||
@@ -225,6 +225,33 @@ To find a channel ID in Mattermost: open the channel, click the channel name hea
|
||||
|
||||
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
|
||||
|
||||
@@ -389,6 +389,33 @@ Set this to `true` in busy workspaces where Slack's default "the bot remembers t
|
||||
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
|
||||
|
||||
@@ -256,6 +256,16 @@ TELEGRAM_HOME_CHANNEL_NAME="My Notes"
|
||||
Group chat IDs are negative numbers (e.g., `-1001234567890`). Your personal DM chat ID is the same as your user ID.
|
||||
:::
|
||||
|
||||
### Cron deliveries in topic mode
|
||||
|
||||
If you have topic mode enabled in your bot DM, cron messages delivered to the root chat land in the system-only lobby — replying there opens no session and you see the "main chat is reserved for system commands" notice. Create a dedicated forum topic (e.g. `Cron`) and set:
|
||||
|
||||
```bash
|
||||
TELEGRAM_CRON_THREAD_ID=<topic_thread_id>
|
||||
```
|
||||
|
||||
`TELEGRAM_CRON_THREAD_ID` overrides `TELEGRAM_HOME_CHANNEL_THREAD_ID` for cron deliveries only. Replies in that topic continue the topic's existing session.
|
||||
|
||||
## Voice Messages
|
||||
|
||||
### Incoming Voice (Speech-to-Text)
|
||||
@@ -266,6 +276,25 @@ Voice messages you send on Telegram are automatically transcribed by Hermes's co
|
||||
- `groq` uses Groq Whisper and requires `GROQ_API_KEY`
|
||||
- `openai` uses OpenAI Whisper and requires `VOICE_TOOLS_OPENAI_KEY`
|
||||
|
||||
#### Skipping STT: pass the raw audio file to the agent
|
||||
|
||||
If you'd rather have the **agent itself** handle audio — for diarization, a custom transcription tool, or just archiving the recording — set `stt.enabled: false` in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
stt:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
With STT disabled, the gateway still downloads the voice/audio attachment into Hermes's audio cache, but **does not transcribe it**. The agent receives the message with a marker like:
|
||||
|
||||
```
|
||||
[The user sent a voice message: /home/<user>/.hermes/cache/audio/<hash>.ogg]
|
||||
```
|
||||
|
||||
Your tools or skills can then read that path directly (e.g., hand it off to a local diarization pipeline, a richer transcription model, or upload it to long-term storage). The file extension reflects the original format Telegram delivered (`.ogg` for voice notes, `.mp3`/`.m4a`/etc. for audio attachments).
|
||||
|
||||
This pairs naturally with the [local Bot API server](#large-files-20mb--via-local-bot-api-server) section below, which lifts Telegram's 20MB getFile ceiling to 2GB — useful when the recordings you want to process are longer than a couple of minutes.
|
||||
|
||||
### Outgoing Voice (Text-to-Speech)
|
||||
|
||||
When the agent generates audio via TTS, it's delivered as native Telegram **voice bubbles** — the round, inline-playable kind.
|
||||
@@ -285,6 +314,135 @@ Without ffmpeg, Edge TTS audio is sent as a regular audio file (still playable,
|
||||
|
||||
Configure the TTS provider in your `config.yaml` under the `tts.provider` key.
|
||||
|
||||
## Large Files (>20MB) via Local Bot API Server
|
||||
|
||||
Telegram's **public** Bot API caps `getFile` downloads at **20 MB**, so any voice note, audio file, video, or document larger than that is silently rejected by Hermes with a "too large" reply. The documented way around this is to run a **local** [telegram-bot-api](https://github.com/tdlib/telegram-bot-api) daemon — the same server software Telegram uses, but running on your network. A local server raises the file ceiling to **2 GB** and Hermes auto-lifts its own internal cap when it sees a custom `base_url` configured.
|
||||
|
||||
This unlocks workflows like:
|
||||
|
||||
- Sending long voice memos (45-minute meetings, podcasts) to the bot
|
||||
- Uploading large videos for vision-tool processing
|
||||
- Archiving raw audio for offline pipelines like diarization, alignment, or training data
|
||||
|
||||
### Step 1: Obtain Telegram API credentials
|
||||
|
||||
The local server talks directly to Telegram's MTProto layer (not the public Bot API), so it needs **MTProto credentials**:
|
||||
|
||||
1. Visit [my.telegram.org/apps](https://my.telegram.org/apps) and sign in with your Telegram account.
|
||||
2. Create a new application (any name and short description will do).
|
||||
3. Copy the `api_id` and `api_hash` — both are required.
|
||||
|
||||
### Step 2: Run the telegram-bot-api server
|
||||
|
||||
The community-maintained [`aiogram/telegram-bot-api`](https://hub.docker.com/r/aiogram/telegram-bot-api) Docker image is the easiest path. A minimal `docker-compose.yaml` (use `--local` mode to enable the higher limits):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
tg-bot-api:
|
||||
image: aiogram/telegram-bot-api:latest
|
||||
container_name: tg-bot-api
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:8081:8081" # bind to loopback only; see security note
|
||||
environment:
|
||||
TELEGRAM_API_ID: "12345" # your api_id from Step 1
|
||||
TELEGRAM_API_HASH: "abcdef..." # your api_hash from Step 1
|
||||
TELEGRAM_LOCAL: "1" # enable --local mode (raises 20MB → 2GB)
|
||||
volumes:
|
||||
- ./tg-bot-api-data:/var/lib/telegram-bot-api
|
||||
```
|
||||
|
||||
Bring it up:
|
||||
|
||||
```bash
|
||||
docker compose up -d tg-bot-api
|
||||
docker logs --tail 20 tg-bot-api
|
||||
```
|
||||
|
||||
:::warning Security
|
||||
The local Bot API server takes your bot token in the URL path (e.g. `/bot<TOKEN>/getMe`) with **no additional auth**. Anyone who can reach the port can fully control your bot — read every message it can see, send messages as it, etc. Bind the container to `127.0.0.1` and/or front it with a reverse proxy on a private network. **Never expose port 8081 to the public internet.**
|
||||
:::
|
||||
|
||||
### Step 3: Log the bot out of the public API (one-time)
|
||||
|
||||
A bot can only be active on **one** Bot API server at a time. If your bot was already running against `api.telegram.org` (which it almost certainly was), you must explicitly log it out there before the local server will accept it:
|
||||
|
||||
```bash
|
||||
curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/logOut"
|
||||
# expected response: {"ok":true,"result":true}
|
||||
```
|
||||
|
||||
This is a one-shot migration step — you don't repeat it on every restart. Telegram delivers any messages received after `logOut` through the new server instead.
|
||||
|
||||
Verify the local server can talk to Telegram on the bot's behalf:
|
||||
|
||||
```bash
|
||||
curl "http://127.0.0.1:8081/bot<YOUR_BOT_TOKEN>/getMe"
|
||||
# expected response: {"ok":true,"result":{"id":...,"is_bot":true,...}}
|
||||
```
|
||||
|
||||
### Step 4: Point Hermes at the local server
|
||||
|
||||
Add the URLs under `platforms.telegram.extra` in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
telegram:
|
||||
extra:
|
||||
base_url: "http://127.0.0.1:8081/bot"
|
||||
base_file_url: "http://127.0.0.1:8081/file/bot"
|
||||
local_mode: true # see Step 5 below — only set this if the bot's data
|
||||
# directory is readable by the Hermes process
|
||||
```
|
||||
|
||||
:::caution Use `platforms.telegram.extra`, not `telegram.extra`
|
||||
At the moment only the `platforms.<name>.extra` form is deep-merged into the platform config. Keys placed directly under a top-level `telegram.extra` block are silently dropped.
|
||||
:::
|
||||
|
||||
When `base_url` is set, Hermes:
|
||||
|
||||
- Builds the python-telegram-bot client against the local server
|
||||
- Auto-lifts its internal document/audio size cap from 20 MB → 2 GB
|
||||
- Reports the active limit in the "too large" error message (`Maximum: 2048 MB.`) so it's obvious which mode you're in
|
||||
|
||||
Restart the gateway and look for a confirmation log line:
|
||||
|
||||
```bash
|
||||
hermes gateway restart
|
||||
grep -E "Using custom Telegram base_url|Using Telegram local_mode" ~/.hermes/logs/gateway.log | tail
|
||||
```
|
||||
|
||||
### Step 5: `local_mode` — file access on disk
|
||||
|
||||
The local server has **two ways** to deliver files:
|
||||
|
||||
1. **Without `--local`** (the default): files are served over HTTP at `/file/bot<TOKEN>/<path>`, same as the public Bot API. The 20MB ceiling stays in effect. Useful as a network-fix only (e.g. when `api.telegram.org` is unreachable but you can self-host); not what you want for the size lift.
|
||||
2. **With `--local`** (set via `TELEGRAM_LOCAL=1` above): files are written to the server's filesystem and the `getFile` response returns an **absolute path** instead of an HTTP URL. The 20MB ceiling is lifted. Hermes must then read the bytes **from disk**, not over HTTP.
|
||||
|
||||
To make the disk-read path work, set `local_mode: true` in the config above **and** make sure the Hermes process can read the path the server returns. Two scenarios:
|
||||
|
||||
- **Same machine** — telegram-bot-api and Hermes run on the same host. Bind-mount the data volume to a directory that Hermes can read (e.g., `/var/lib/telegram-bot-api`), and make sure the file ownership matches. The container drops privileges to its internal `telegram-bot-api` user (uid varies by image); the simplest fix is to add `user: "<UID>:<GID>"` to the compose service so files are owned by a uid Hermes already runs as.
|
||||
- **Different machines** — the bot server runs on one host (e.g., a NAS, a separate VM) and Hermes on another. The server's data directory must be shared with the Hermes machine at the **same absolute path** the server reports (typically `/var/lib/telegram-bot-api`). NFS works well for this; CIFS/SMB with `uid=` mount remapping is friendlier if you don't want to deal with uid mismatches at the filesystem level.
|
||||
|
||||
If `local_mode: true` is set but Hermes can't `stat` the returned file path (permissions or wrong mount), python-telegram-bot silently falls back to an HTTP `getFile` against the local server — which in `--local` mode responds with `404 Not Found`. The symptom shows up in `gateway.log` as:
|
||||
|
||||
```
|
||||
[Telegram] Failed to cache voice: Not Found
|
||||
telegram.error.InvalidToken: Not Found
|
||||
```
|
||||
|
||||
If you see that, the cap-lift is working but the file-share isn't. Verify `ls -la /var/lib/telegram-bot-api/<TOKEN>/voice/` from the Hermes host as the user the gateway runs as, and confirm a single file is `cat`-able without a permission error.
|
||||
|
||||
### Step 6: Test it
|
||||
|
||||
Send the bot a voice note or audio file that's bigger than 20 MB. Tail the gateway log:
|
||||
|
||||
```bash
|
||||
tail -f ~/.hermes/logs/gateway.log | grep -iE "telegram|cache"
|
||||
```
|
||||
|
||||
You should see a `[Telegram] Cached user voice at /home/<user>/.hermes/cache/audio/...` line and **no** "too large" rejection. Combined with `stt.enabled: false` (above), the path to the original audio file then lands in the agent's inbound message for downstream processing.
|
||||
|
||||
## Group Chat Usage
|
||||
|
||||
Hermes Agent works in Telegram group chats with a few considerations:
|
||||
@@ -297,9 +455,43 @@ Hermes Agent works in Telegram group chats with a few considerations:
|
||||
- `@botusername` mentions
|
||||
- `/command@botusername` (Telegram's bot-menu command form that includes the bot name)
|
||||
- matches for one of your configured regex wake words in `telegram.mention_patterns`
|
||||
- In groups with multiple Hermes bots, `telegram.exclusive_bot_mentions` keeps routing deterministic. When a message explicitly mentions one or more Telegram bot usernames, only the mentioned bot profiles process it; other Hermes bots ignore it before reply and wake-word fallbacks run. This is enabled by default.
|
||||
- Use `telegram.ignored_threads` to keep Hermes silent in specific Telegram forum topics, even when the group would otherwise allow free responses or mention-triggered replies
|
||||
- If `telegram.require_mention` is left unset or false, Hermes keeps the previous open-group behavior and responds to normal group messages it can see
|
||||
|
||||
### Multiple Hermes bots in one group
|
||||
|
||||
If you run several Hermes profiles in the same Telegram group, create one Telegram bot token per profile and start one gateway per profile. Do not reuse the same bot token in multiple running gateways; Telegram will reject concurrent polling for the same token.
|
||||
|
||||
Recommended group config:
|
||||
|
||||
```yaml
|
||||
telegram:
|
||||
require_mention: true
|
||||
exclusive_bot_mentions: true
|
||||
mention_patterns: []
|
||||
```
|
||||
|
||||
With this setup, a group message like `@research_bot @ops_bot summarize this` is processed by `research_bot` and `ops_bot` only. Other Hermes bots in the group stay silent, even if the message is a reply to one of their earlier messages or would otherwise match a shared wake word.
|
||||
|
||||
Set `exclusive_bot_mentions: false` only for legacy groups where explicit mentions should not override reply and wake-word triggers.
|
||||
|
||||
To operate several profiles, run the gateway command once per profile. For example:
|
||||
|
||||
```bash
|
||||
# default profile
|
||||
hermes gateway start
|
||||
hermes gateway status
|
||||
hermes gateway stop
|
||||
|
||||
# named profiles
|
||||
hermes -p research gateway start
|
||||
hermes -p research gateway status
|
||||
hermes -p research gateway stop
|
||||
```
|
||||
|
||||
For a small fixed fleet, use a shell loop or script that calls `hermes gateway <action>` for the default profile and `hermes -p <profile> gateway <action>` for each named profile. This is more reliable than assuming a single process-level command controls every named profile on every service manager.
|
||||
|
||||
### Troubleshooting: works in DMs but not groups
|
||||
|
||||
If the bot responds in a private chat but stays silent in a group, check these
|
||||
@@ -317,6 +509,9 @@ gates in order:
|
||||
4. **Mention filters:** if `telegram.require_mention: true` is set, normal
|
||||
group chatter is ignored unless the message is a slash command, reply to the
|
||||
bot, `@botusername` mention, or configured `mention_patterns` match.
|
||||
5. **Multi-bot routing:** if a group contains several bots, make sure each
|
||||
Hermes profile uses a unique bot token and keep `exclusive_bot_mentions`
|
||||
enabled unless you intentionally want legacy shared-trigger behavior.
|
||||
|
||||
Negative chat IDs are normal for Telegram groups and supergroups. If you use
|
||||
chat-scoped authorization, put those IDs in `TELEGRAM_GROUP_ALLOWED_CHATS`, not
|
||||
@@ -329,6 +524,7 @@ Add this to `~/.hermes/config.yaml`:
|
||||
```yaml
|
||||
telegram:
|
||||
require_mention: true
|
||||
exclusive_bot_mentions: true
|
||||
mention_patterns:
|
||||
- "^\\s*chompy\\b"
|
||||
ignored_threads:
|
||||
@@ -408,6 +604,28 @@ platforms:
|
||||
3. Each topic maps to an isolated session key: `agent:main:telegram:dm:{chat_id}:{thread_id}`
|
||||
4. Messages in each topic have their own conversation history, memory flush, and context window
|
||||
|
||||
### Root DM handling
|
||||
|
||||
By default, messages sent to the root DM (outside any topic) are processed
|
||||
normally. Set `ignore_root_dm: true` to turn the root DM into a lobby — normal
|
||||
messages are silently ignored for users who have DM topics configured, while
|
||||
system commands (`/start`, `/help`, `/status`, etc.) still work.
|
||||
|
||||
```yaml
|
||||
platforms:
|
||||
telegram:
|
||||
extra:
|
||||
ignore_root_dm: true
|
||||
dm_topics:
|
||||
- chat_id: 123456789
|
||||
topics:
|
||||
- name: General
|
||||
```
|
||||
|
||||
The check is **per-chat**: only users with at least one entry in `dm_topics`
|
||||
will have their root DM affected. Users without configured topics are
|
||||
unaffected.
|
||||
|
||||
### Skill binding
|
||||
|
||||
Topics with a `skill` field automatically load that skill when a new session starts in the topic. This works exactly like typing `/skill-name` at the start of a conversation — the skill content is injected into the first message, and subsequent messages see it in the conversation history.
|
||||
@@ -442,7 +660,7 @@ Only authorized users (allowlist via `TELEGRAM_ALLOWED_USERS` / platform auth co
|
||||
| Who activates it | Operator, in `config.yaml` | End user, by sending `/topic` |
|
||||
| Topic list | Fixed set declared in config | User creates/deletes topics freely |
|
||||
| Topic names | Chosen by operator | Chosen by user; auto-renamed to match Hermes session title |
|
||||
| Root DM behavior | Unchanged — normal chat | Becomes a system lobby (non-command messages are rejected) |
|
||||
| Root DM behavior | Normal chat (lobby if `ignore_root_dm: true`) | Becomes a system lobby (non-command messages are rejected) |
|
||||
| Primary use case | Permanent workspaces with optional skill binding | Ad-hoc parallel sessions |
|
||||
| Persistence | `extra.dm_topics` in config | `telegram_dm_topic_mode` + `telegram_dm_topic_bindings` SQLite tables |
|
||||
|
||||
@@ -487,6 +705,18 @@ Every topic gets its own conversation history, model state, tool execution, and
|
||||
|
||||
When Hermes generates a session title for a topic (via the auto-title pipeline, after the first exchange), the Telegram topic itself is renamed to match — e.g. "New Topic" becomes "Database migration plan". The rename is best-effort: failures are logged but don't break the session.
|
||||
|
||||
To disable this and keep your manually-chosen topic names untouched, set:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
telegram:
|
||||
extra:
|
||||
disable_topic_auto_rename: true
|
||||
```
|
||||
|
||||
When this flag is on, Hermes still generates an internal session title (used by `hermes sessions`, the TUI, etc.) but never edits the Telegram topic name. Useful when you organise topics by hand under BotFather Threaded Mode and don't want every first reply to overwrite the title.
|
||||
|
||||
### `/new` inside a topic
|
||||
|
||||
Resets the current topic's session (new session ID, fresh history) without touching other topics. Hermes replies with a reminder that for parallel work, creating another topic (via **All Messages**) is usually what you want.
|
||||
@@ -520,6 +750,7 @@ Shows the current topic's binding: session title, session ID, and hints for `/ne
|
||||
- Each inbound DM message looks up its `(chat_id, thread_id)` binding. If present, the lookup routes the message to the bound session via `SessionStore.switch_session()` so the session-key-to-session-id mapping stays consistent on disk
|
||||
- `/new` inside a topic rewrites the binding row to point at the new session ID, so the next message stays on the fresh session
|
||||
- Topics declared in `extra.dm_topics` are **never auto-renamed** — the operator-chosen name is preserved even when multi-session mode is enabled
|
||||
- Set `extra.disable_topic_auto_rename: true` to turn off auto-rename for **all** topics in the chat (ad-hoc topics created via Threaded Mode included)
|
||||
- The General (pinned top) topic in a forum-enabled DM is treated as the root lobby, regardless of whether Telegram delivers its messages with `message_thread_id=1` or with no thread_id
|
||||
- Root-lobby reminders are rate-limited to one message per 30 seconds per chat — a user who forgets topic mode is on and types ten prompts in the root won't get ten replies
|
||||
- BotFather setup screenshots are rate-limited to one send per 5 minutes per chat — repeated `/topic` attempts while Threads Settings are still disabled won't re-upload the same image
|
||||
@@ -611,7 +842,7 @@ To find a topic's `thread_id`, open the topic in Telegram Web or Desktop and loo
|
||||
|
||||
- **Bot API 9.4 (Feb 2026):** Private Chat Topics — bots can create forum topics in 1-on-1 DM chats via `createForumTopic`. Hermes uses this for two distinct features: operator-curated [Private Chat Topics](#private-chat-topics-bot-api-94) (config-driven, fixed topic list) and user-driven [Multi-session DM mode](#multi-session-dm-mode-topic) (activated by `/topic`, unlimited user-created topics).
|
||||
- **Privacy policy:** Telegram now requires bots to have a privacy policy. Set one via BotFather with `/setprivacy_policy`, or Telegram may auto-generate a placeholder. This is particularly important if your bot is public-facing.
|
||||
- **Bot API 9.5 (Mar 2026): Native streaming via `sendMessageDraft`.** Hermes uses Telegram's native streaming-draft API to render an animated preview of the agent's reply as tokens arrive in private chats. Drops the per-edit jitter you used to see with the legacy `editMessageText` polling path on slow models.
|
||||
- **Bot API 9.5 (Mar 2026): Native streaming via `sendMessageDraft`.** Hermes supports Telegram's native streaming-draft API as an opt-in transport for private chats. The default remains the legacy `editMessageText` path because draft previews can visibly collapse and re-render on some Telegram clients.
|
||||
|
||||
### Streaming transport (`gateway.streaming.transport`)
|
||||
|
||||
@@ -619,9 +850,9 @@ When streaming is enabled (`gateway.streaming.enabled: true`), Hermes picks one
|
||||
|
||||
| Value | Behaviour |
|
||||
|---|---|
|
||||
| `auto` (default) | Native draft streaming on supported chats (currently Telegram DMs); legacy edit-based path otherwise. Falls back gracefully if a draft frame fails. |
|
||||
| `auto` | Native draft streaming on supported chats (currently Telegram DMs); legacy edit-based path otherwise. Falls back gracefully if a draft frame fails. |
|
||||
| `draft` | Force native drafts. Logs a downgrade and falls back to edit if the chat doesn't support drafts (e.g. groups/topics). |
|
||||
| `edit` | Legacy progressive `editMessageText` polling for every chat type. |
|
||||
| `edit` (default) | Legacy progressive `editMessageText` polling for every chat type. |
|
||||
| `off` | Disable streaming entirely (final reply only, no progressive updates). |
|
||||
|
||||
In `~/.hermes/config.yaml`:
|
||||
@@ -630,10 +861,12 @@ In `~/.hermes/config.yaml`:
|
||||
gateway:
|
||||
streaming:
|
||||
enabled: true
|
||||
transport: auto # auto | draft | edit | off
|
||||
transport: edit # edit | auto | draft | off
|
||||
```
|
||||
|
||||
**What you'll see in DMs with `auto` (default)** — when the agent generates a reply, Telegram shows an animated draft preview that updates token-by-token. When the reply finishes, it's delivered as a regular message and the draft preview clears naturally on the client. Drafts have no message id, so the final answer is what stays in your chat history.
|
||||
**What you'll see in DMs with `edit` (default)** — the gateway sends a normal preview message and progressively updates it via `editMessageText`, avoiding Telegram's draft-preview collapse/rollback effect.
|
||||
|
||||
**What you'll see in DMs with `auto` or `draft`** — Telegram shows an animated draft preview that updates token-by-token. When the reply finishes, it's delivered as a regular message and the draft preview clears naturally on the client. Drafts have no message id, so the final answer is what stays in your chat history.
|
||||
|
||||
**What about groups, supergroups, forum topics?** Telegram restricts `sendMessageDraft` to private chats (DMs). The gateway transparently falls back to the edit-based path for everything else — same UX as before.
|
||||
|
||||
@@ -711,6 +944,34 @@ TELEGRAM_GROUP_ALLOWED_USERS="-1001234567890"
|
||||
TELEGRAM_GROUP_ALLOWED_CHATS="-1001234567890"
|
||||
```
|
||||
|
||||
### Guest @mention bypass (`guest_mode`)
|
||||
|
||||
In a typical setup, `group_allowed_chats` is a hard gate: messages from groups outside the list are silently dropped, even if a member explicitly @mentions the bot. That's the right default for support / team bots.
|
||||
|
||||
For more casual setups — friend group chats where you want the bot **mostly silent** but **occasionally available on explicit ping** — enable `guest_mode`:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
telegram:
|
||||
extra:
|
||||
group_allowed_chats:
|
||||
- "-1001234567890" # your main allowlisted group
|
||||
guest_mode: true # non-allowlisted groups: allow on @mention only
|
||||
```
|
||||
|
||||
Env equivalent:
|
||||
|
||||
```bash
|
||||
TELEGRAM_GUEST_MODE=true
|
||||
```
|
||||
|
||||
Default: `false`.
|
||||
|
||||
With `guest_mode: true`, a message from a non-allowlisted group is processed **only** if it explicitly @mentions the bot. The mention is required every turn — there's no session stickiness for guest interactions, so the bot never auto-engages in a friend group thread it isn't pinged into.
|
||||
|
||||
DMs and allowlisted groups behave exactly as before.
|
||||
|
||||
## 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 platform's `extra` block:
|
||||
@@ -920,6 +1181,32 @@ Tap a button to answer, or tap **Other** to type a free-form response (the next
|
||||
|
||||
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.
|
||||
|
||||
## Push notification volume
|
||||
|
||||
Telegram fires a push notification on every message the bot sends. For long agent turns that emit tool-progress bubbles, streaming updates, and status callbacks, this gets noisy fast. The Telegram adapter has two notification modes:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `important` (default) | Only **final responses**, **approval prompts**, and **slash-command confirmations** ring. Tool progress, streaming chunks, and status messages are delivered with `disable_notification=true`. |
|
||||
| `all` | Every outgoing message fires a push notification. Legacy behavior; opt in if you genuinely want to hear about every tool call. |
|
||||
|
||||
Configure in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
display:
|
||||
platforms:
|
||||
telegram:
|
||||
notifications: important # or "all"
|
||||
```
|
||||
|
||||
Env override (handy for quick A/B testing):
|
||||
|
||||
```bash
|
||||
HERMES_TELEGRAM_NOTIFICATIONS=all
|
||||
```
|
||||
|
||||
Unknown values log a warning and fall back to `important`.
|
||||
|
||||
## Security
|
||||
|
||||
:::warning
|
||||
|
||||
@@ -73,6 +73,8 @@ When YOLO is active, Hermes shows two persistent visual reminders so it's hard t
|
||||
YOLO mode disables **all** dangerous command safety checks for the session — **except** the hardline blocklist (see below). Use only when you fully trust the commands being generated (e.g., well-tested automation scripts in disposable environments).
|
||||
:::
|
||||
|
||||
For destructive session slash commands (`/clear`, `/new` / `/reset`, `/undo`, `/exit --delete`), the CLI also prompts for confirmation before running them. See [Slash Commands — Confirmation prompts for destructive commands](../reference/slash-commands.md#confirmation-prompts-for-destructive-commands).
|
||||
|
||||
### Hardline Blocklist (Always-On Floor)
|
||||
|
||||
Some commands are so catastrophic — irreversible filesystem wipes, fork bombs, direct block-device writes — that Hermes refuses to run them **regardless** of:
|
||||
@@ -605,3 +607,58 @@ TERMINAL_SSH_KEY=~/.ssh/hermes_agent_key
|
||||
```
|
||||
|
||||
The SSH connection details live in `.env` (not `config.yaml`) so they aren't checked in or shared along with profile exports. This keeps the gateway's messaging connections separate from the agent's command execution.
|
||||
|
||||
## Supply-chain advisory checking
|
||||
|
||||
Hermes ships with a built-in advisory scanner that flags Python packages in the active venv that match a curated catalog of known-compromised versions (supply-chain worms like the May 2026 `mistralai 2.4.6` poisoning). Implementation lives in `hermes_cli/security_advisories.py`.
|
||||
|
||||
How it runs:
|
||||
|
||||
- **CLI startup banner.** A one-line warning is printed if any advisory matches, with a pointer to `hermes doctor` for the full remediation.
|
||||
- **`hermes doctor`.** Surfaces every active advisory with version specifics and 2-4 step remediation instructions.
|
||||
- **Gateway startup.** Logged to `gateway.log`; the first interactive message gets a short operator banner.
|
||||
|
||||
Each advisory carries a stable id. Once you have read and acted on it you can dismiss it for good:
|
||||
|
||||
```bash
|
||||
hermes doctor --ack <advisory-id>
|
||||
```
|
||||
|
||||
The ack is persisted to `config.security.acked_advisories` and survives restart. Old advisories are intentionally **not** removed from the catalog — leaving them in place keeps fresh installs warned about historically poisoned versions that might still be cached in a private mirror.
|
||||
|
||||
The check itself is stdlib-only and runs from one `importlib.metadata.version()` lookup per advisory, so it's safe to run on every startup.
|
||||
|
||||
### Lazy install of optional dependencies
|
||||
|
||||
Many features (Mistral TTS, ElevenLabs, Honcho memory, Bedrock, Slack, Matrix, …) depend on Python packages that not every user needs. Hermes installs these **lazily** on first use rather than eagerly under `hermes-agent[all]`. The implementation lives in `tools/lazy_deps.py`.
|
||||
|
||||
The trade-off this fixes:
|
||||
|
||||
- **Fragility.** When one extra's transitive dependency becomes unavailable on PyPI (quarantined for malware, yanked, broken upload), the entire `[all]` resolve would fail and fresh installs would silently fall back to a stripped tier — losing 10+ unrelated extras at once. Lazy install isolates each backend so one poisoned dep can't break unrelated features.
|
||||
- **Bloat.** A user who only ever talks to one provider no longer pulls hundreds of packages they will never import.
|
||||
|
||||
How it works:
|
||||
|
||||
1. A backend module calls `ensure("feature.name")` at the top of its first-import path.
|
||||
2. If the deps are missing, `ensure` checks `security.allow_lazy_installs` in `config.yaml` (default `true`) and runs a venv-scoped `pip install` for the allowlisted specs.
|
||||
3. If the install fails or the user has disabled lazy installs, the call raises `FeatureUnavailable` with the actual pip stderr and a pointer at `hermes tools`.
|
||||
|
||||
Security guarantees enforced by `tools/lazy_deps.py`:
|
||||
|
||||
| Guarantee | What it means |
|
||||
|---|---|
|
||||
| Venv-scoped only | Installs target `sys.executable` in the active venv — never the system Python |
|
||||
| PyPI by name only | Specs accept `"package>=1.0,<2"` syntax. No `--index-url`, `git+https://`, or file: paths — a malicious `config.yaml` cannot redirect the install |
|
||||
| Allowlist | Only specs that appear in the in-tree `LAZY_DEPS` map can be installed via this path. A typo in a feature name does NOT get install-anything semantics |
|
||||
| Opt-out | Set `security.allow_lazy_installs: false` to disable runtime installs entirely. Useful for restricted networks or strict security postures |
|
||||
| No silent retries | Failures surface as `FeatureUnavailable` — no caching of bad state, no retry storms |
|
||||
|
||||
To disable runtime installs:
|
||||
|
||||
```yaml
|
||||
# ~/.hermes/config.yaml
|
||||
security:
|
||||
allow_lazy_installs: false
|
||||
```
|
||||
|
||||
When disabled, backends that need optional deps will tell the user to run the install manually (`pip install …`) or pick a different backend via `hermes tools`.
|
||||
|
||||
@@ -60,6 +60,9 @@ into chat.
|
||||
Use `/compress` when a session gets long, `/new` for a fresh thread, and
|
||||
`hermes sessions prune` only when you want to delete old ended sessions from
|
||||
storage. Compression reduces the active context; it is not a privacy delete.
|
||||
Pass a name to `/new` (e.g. `/new payments-refactor`) to set the new session's
|
||||
initial title up front — useful for finding it later with `/resume <name>` or
|
||||
in the `/sessions` picker.
|
||||
:::
|
||||
|
||||
### Session Sources
|
||||
@@ -412,9 +415,9 @@ session_search()
|
||||
|
||||
Returns recent sessions chronologically (titles, previews, timestamps). Useful when the user asks "what was I working on" without naming a topic.
|
||||
|
||||
### FTS5 Query Syntax
|
||||
### FTS5 query syntax
|
||||
|
||||
The search supports standard FTS5 query syntax:
|
||||
The keyword mode supports standard FTS5 query syntax:
|
||||
|
||||
- Simple keywords: `docker deployment` (FTS5 defaults to AND)
|
||||
- Phrases: `"exact phrase"`
|
||||
@@ -432,6 +435,8 @@ The agent is prompted to use session search automatically:
|
||||
|
||||
> *"When the user references something from a past conversation or you suspect relevant prior context exists, use session_search to recall it before asking them to repeat themselves."*
|
||||
|
||||
Typical triggers: "we did this before", "remember when", "last time", "as I mentioned", or any reference to a project/person/concept that isn't in the current window.
|
||||
|
||||
## Per-Platform Session Tracking
|
||||
|
||||
### Gateway Sessions
|
||||
|
||||
+10
-5
@@ -697,19 +697,24 @@ User docs: https://hermes-agent.nousresearch.com/docs/user-guide/features/curato
|
||||
|
||||
Durable SQLite board for multi-profile / multi-worker collaboration.
|
||||
Users drive it via `hermes kanban <verb>`; dispatcher-spawned workers
|
||||
see a focused `kanban_*` toolset gated by `HERMES_KANBAN_TASK` so the
|
||||
schema footprint is zero outside worker processes.
|
||||
see a focused `kanban_*` toolset gated by `HERMES_KANBAN_TASK`, and
|
||||
orchestrator profiles can opt into the broader `kanban` toolset. Normal
|
||||
sessions still have zero `kanban_*` schema footprint unless configured.
|
||||
|
||||
- **CLI verbs (common):** `init`, `create`, `list` (alias `ls`),
|
||||
`show`, `assign`, `link`, `unlink`, `comment`, `complete`, `block`,
|
||||
`unblock`, `archive`, `tail`. Less common: `watch`, `stats`, `runs`,
|
||||
`log`, `dispatch`, `daemon`, `gc`.
|
||||
- **Worker toolset:** `kanban_show`, `kanban_complete`, `kanban_block`,
|
||||
`kanban_heartbeat`, `kanban_comment`, `kanban_create`, `kanban_link`.
|
||||
- **Worker/orchestrator toolset:** `kanban_show`, `kanban_complete`,
|
||||
`kanban_block`, `kanban_heartbeat`, `kanban_comment`, `kanban_create`,
|
||||
`kanban_link`; profiles that explicitly enable the `kanban` toolset
|
||||
outside a dispatcher-spawned task also get `kanban_list` and
|
||||
`kanban_unblock` for board routing.
|
||||
- **Dispatcher** runs inside the gateway by default
|
||||
(`kanban.dispatch_in_gateway: true`) — reclaims stale claims,
|
||||
promotes ready tasks, atomically claims, spawns assigned profiles.
|
||||
Auto-blocks a task after ~5 consecutive spawn failures.
|
||||
Auto-blocks a task after the configured `kanban.failure_limit`
|
||||
consecutive non-success attempts (default: 2).
|
||||
- **Isolation:** board is the hard boundary (workers get
|
||||
`HERMES_KANBAN_BOARD` pinned in env); tenant is a soft namespace
|
||||
within a board for workspace-path + memory-key isolation.
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: "Baoyu Article Illustrator — Article illustrations: type × style × palette consistency"
|
||||
sidebar_label: "Baoyu Article Illustrator"
|
||||
description: "Article illustrations: type × style × palette consistency"
|
||||
---
|
||||
|
||||
{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
|
||||
|
||||
# Baoyu Article Illustrator
|
||||
|
||||
Article illustrations: type × style × palette consistency.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Bundled (installed by default) |
|
||||
| Path | `skills/creative/baoyu-article-illustrator` |
|
||||
| Version | `1.57.0` |
|
||||
| Author | 宝玉 (JimLiu) |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos, windows |
|
||||
| Tags | `article-illustration`, `creative`, `image-generation` |
|
||||
|
||||
## Reference: full SKILL.md
|
||||
|
||||
:::info
|
||||
The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
|
||||
:::
|
||||
|
||||
# Article Illustrator
|
||||
|
||||
Adapted from [baoyu-article-illustrator](https://github.com/JimLiu/baoyu-skills) for Hermes Agent's tool ecosystem.
|
||||
|
||||
Analyze articles, identify illustration positions, generate images with **Type × Style × Palette** consistency.
|
||||
|
||||
## When to Use
|
||||
|
||||
Trigger this skill when the user asks to illustrate an article, add images to an article, generate illustrations for content, or uses phrases like "为文章配图", "illustrate article", or "add images". The user provides an article (file path or pasted content) and optionally specifies type, style, palette, or density.
|
||||
|
||||
## Three Dimensions
|
||||
|
||||
| Dimension | Controls | Examples |
|
||||
|-----------|----------|----------|
|
||||
| **Type** | Information structure | infographic, scene, flowchart, comparison, framework, timeline |
|
||||
| **Style** | Rendering approach | notion, warm, minimal, blueprint, watercolor, elegant |
|
||||
| **Palette** | Color scheme (optional) | macaron, warm, neon — overrides style's default colors |
|
||||
|
||||
Combine freely: `type=infographic, style=vector-illustration, palette=macaron`.
|
||||
|
||||
Or use presets: `edu-visual` → type + style + palette in one shot. See [style-presets.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/style-presets.md).
|
||||
|
||||
## Types
|
||||
|
||||
| Type | Best For |
|
||||
|------|----------|
|
||||
| `infographic` | Data, metrics, technical |
|
||||
| `scene` | Narratives, emotional |
|
||||
| `flowchart` | Processes, workflows |
|
||||
| `comparison` | Side-by-side, options |
|
||||
| `framework` | Models, architecture |
|
||||
| `timeline` | History, evolution |
|
||||
|
||||
## Styles
|
||||
|
||||
See [references/styles.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/styles.md) for Core Styles, the full gallery, and Type × Style compatibility.
|
||||
|
||||
## Output Structure
|
||||
|
||||
<!-- ascii-guard-ignore -->
|
||||
```
|
||||
{output-dir}/
|
||||
├── source-{slug}.{ext} # Only for pasted content
|
||||
├── outline.md
|
||||
├── prompts/
|
||||
│ └── NN-{type}-{slug}.md
|
||||
└── NN-{type}-{slug}.png
|
||||
```
|
||||
<!-- ascii-guard-ignore-end -->
|
||||
|
||||
**Default output directory**:
|
||||
|
||||
| Input | Output Directory | Markdown Insert Path |
|
||||
|-------|------------------|----------------------|
|
||||
| Article file path | `{article-dir}/imgs/` | `imgs/NN-{type}-{slug}.png` |
|
||||
| Pasted content | `illustrations/{topic-slug}/` (cwd) | `illustrations/{topic-slug}/NN-{type}-{slug}.png` |
|
||||
|
||||
If the user asks for a different layout (e.g., images alongside the article, or a `illustrations/` subdirectory), honor that.
|
||||
|
||||
**Slug**: 2-4 words, kebab-case. **Conflict**: append `-YYYYMMDD-HHMMSS`.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Visualize concepts, not metaphors** — if the article uses a metaphor (e.g., "电锯切西瓜"), illustrate the underlying concept, not the literal image.
|
||||
- **Labels use article data** — actual numbers, terms, and quotes from the article, not generic placeholders.
|
||||
- **Prompt files are reproducibility records** — every illustration must have a saved prompt file under `prompts/` before any image is generated.
|
||||
- **Strip secrets** — scan source content for API keys, tokens, or credentials before writing anything to disk.
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
- [ ] Step 1: Detect reference images (if provided)
|
||||
- [ ] Step 2: Analyze content
|
||||
- [ ] Step 3: Confirm settings (clarify tool, one question at a time)
|
||||
- [ ] Step 4: Generate outline
|
||||
- [ ] Step 5: Generate prompts
|
||||
- [ ] Step 6: Generate images (image_generate)
|
||||
- [ ] Step 7: Finalize
|
||||
```
|
||||
|
||||
### Step 1: Detect Reference Images
|
||||
|
||||
If the user supplies reference images (paths pasted inline, attachments, or a URL):
|
||||
|
||||
1. For each reference, call `vision_analyze` with the path/URL and a question asking for style, palette, composition, and subject. Record the returned description in `{output-dir}/references/NN-ref-{slug}.md` via `write_file`.
|
||||
2. **Do not** try to copy the binary via `write_file` / `read_file` — those are text-only. If you want a local copy for the record, use `terminal` (`cp "$src" "{output-dir}/references/NN-ref-{slug}.{ext}"`). The skill itself never needs to read the binary; it works off the vision description.
|
||||
3. Since `image_generate` doesn't take image inputs, the vision description is what gets embedded in prompts during Step 5.
|
||||
|
||||
Full procedures: [references/workflow.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/workflow.md#step-1-detect-reference-images).
|
||||
|
||||
### Step 2: Analyze
|
||||
|
||||
| Analysis | Output |
|
||||
|----------|--------|
|
||||
| Content type | Technical / Tutorial / Methodology / Narrative |
|
||||
| Purpose | information / visualization / imagination |
|
||||
| Core arguments | 2-5 main points |
|
||||
| Positions | Where illustrations add value |
|
||||
|
||||
Read source (file path → `read_file`, or pasted text) and write the analysis to `{output-dir}/analysis.md` using `write_file`.
|
||||
|
||||
Full procedures: [references/workflow.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/workflow.md#step-2-analyze).
|
||||
|
||||
### Step 3: Confirm Settings
|
||||
|
||||
Use the `clarify` tool. Since `clarify` handles one question at a time, ask the most important question first. Skip any question whose answer is already present in the user's request.
|
||||
|
||||
| Order | Question | Options |
|
||||
|-------|----------|---------|
|
||||
| Q1 | **Preset or Type** | [Recommended preset], [alt preset], or manual: infographic, scene, flowchart, comparison, framework, timeline, mixed |
|
||||
| Q2 | **Density** | minimal (1-2), balanced (3-5), per-section (Recommended), rich (6+) |
|
||||
| Q3 | **Style** *(skip if preset chosen in Q1)* | [Recommended], minimal-flat, sci-fi, hand-drawn, editorial, scene, poster |
|
||||
| Q4 | **Palette** *(optional)* | Default (style colors), macaron, warm, neon |
|
||||
| Q5 | **Language** *(only if article language is ambiguous)* | article language / user language |
|
||||
|
||||
Don't ask more than 2-3 `clarify` questions in a row. If the user already specified these in their request, skip entirely.
|
||||
|
||||
Full procedures: [references/workflow.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/workflow.md#step-3-confirm-settings).
|
||||
|
||||
### Step 4: Generate Outline → `outline.md`
|
||||
|
||||
Save `{output-dir}/outline.md` using `write_file` with frontmatter (type, density, style, palette, image_count) and one entry per illustration:
|
||||
|
||||
```yaml
|
||||
## Illustration 1
|
||||
**Position**: [section/paragraph]
|
||||
**Purpose**: [why]
|
||||
**Visual Content**: [what to show]
|
||||
**Filename**: 01-infographic-concept-name.png
|
||||
```
|
||||
|
||||
Full template: [references/workflow.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/workflow.md#step-4-generate-outline).
|
||||
|
||||
### Step 5: Generate Prompts
|
||||
|
||||
**BLOCKING**: Every illustration must have a saved prompt file before any image is generated — the prompt file is the reproducibility record.
|
||||
|
||||
For each illustration:
|
||||
|
||||
1. Create a prompt file per [references/prompt-construction.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/prompt-construction.md).
|
||||
2. Save to `{output-dir}/prompts/NN-{type}-{slug}.md` using `write_file` with YAML frontmatter.
|
||||
3. Prompts MUST use type-specific templates with structured sections (ZONES / LABELS / COLORS / STYLE / ASPECT).
|
||||
4. LABELS MUST include article-specific data: actual numbers, terms, metrics, quotes.
|
||||
5. Process references (`direct`/`style`/`palette`) per prompt frontmatter — for `direct` usage, embed a textual description of the reference in the prompt (since `image_generate` doesn't take reference-image inputs).
|
||||
|
||||
### Step 6: Generate Images
|
||||
|
||||
For each prompt file:
|
||||
|
||||
1. Call `image_generate(prompt=..., aspect_ratio=...)`. `image_generate` returns a JSON result containing an image URL; it does NOT write to disk and does NOT accept an output path.
|
||||
2. Map the prompt's `ASPECT` to `image_generate`'s enum: `16:9` → `landscape`, `9:16` → `portrait`, `1:1` → `square`. Custom ratios → nearest named aspect.
|
||||
3. Download the returned URL to `{output-dir}/NN-{type}-{slug}.png` via `terminal` (e.g. `curl -sSL -o "{output-dir}/NN-{type}-{slug}.png" "{url}"`).
|
||||
4. On generation failure, auto-retry once.
|
||||
|
||||
Note: the underlying image-generation backend is user-configured (default: FAL FLUX 2 Klein 9B) and is NOT agent-selectable via `image_generate`. Do not write model names into prompts expecting them to route.
|
||||
|
||||
### Step 7: Finalize
|
||||
|
||||
Insert `` after the corresponding paragraph. Alt text: concise description in the article's language.
|
||||
|
||||
Report:
|
||||
|
||||
```
|
||||
Article Illustration Complete!
|
||||
Article: [path] | Type: [type] | Density: [level] | Style: [style] | Palette: [palette or default]
|
||||
Images: X/N generated
|
||||
```
|
||||
|
||||
## Modification
|
||||
|
||||
| Action | Steps |
|
||||
|--------|-------|
|
||||
| Edit | Update prompt → Regenerate → Update reference |
|
||||
| Add | Position → Prompt → Generate → Update outline → Insert |
|
||||
| Delete | Delete files → Remove reference → Update outline |
|
||||
|
||||
## References
|
||||
|
||||
| File | Content |
|
||||
|------|---------|
|
||||
| [references/workflow.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/workflow.md) | Detailed procedures |
|
||||
| [references/usage.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/usage.md) | Invocation examples |
|
||||
| [references/styles.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/styles.md) | Style gallery + Palette gallery |
|
||||
| [references/style-presets.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/style-presets.md) | Preset shortcuts (type + style + palette) |
|
||||
| [references/prompt-construction.md](https://github.com/NousResearch/hermes-agent/blob/main/skills/creative/baoyu-article-illustrator/references/prompt-construction.md) | Prompt templates |
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **Data integrity is paramount** — never summarize, paraphrase, or alter source statistics. "73% increase" stays "73% increase".
|
||||
2. **Strip secrets** — scan source content for API keys, tokens, or credentials before including in any output file.
|
||||
3. **Don't illustrate metaphors literally** — visualize the underlying concept.
|
||||
4. **Prompt files are mandatory** — no image generation without a saved prompt file. The file is what lets you regenerate or switch backends later.
|
||||
5. **`image_generate` aspect ratios** — the tool supports `landscape`, `portrait`, and `square`. Custom ratios map to the nearest option.
|
||||
6. **`image_generate` returns a URL, not a local file** — always download via `terminal` (`curl`) before inserting local image paths into the article.
|
||||
7. **No backend selection from the agent** — `image_generate` uses whatever model the user configured (default: FAL FLUX 2 Klein 9B). Don't write `"use <model> to generate this"` into prompts expecting it to route.
|
||||
@@ -50,6 +50,19 @@ The classic CLI remains available as the default. Anything documented in [CLI In
|
||||
|
||||
Same [skins](features/skins.md) and [personalities](features/personality.md) apply. Switch mid-session with `/skin ares`, `/personality pirate`, and the UI repaints live. See [Skins & Themes](features/skins.md) for the full list of customizable keys and which ones apply to classic vs TUI — the TUI honors the banner palette, UI colors, prompt glyph/color, session display, completion menu, selection bg, `tool_prefix`, and `help_header`.
|
||||
|
||||
### Collapsible banner sections
|
||||
|
||||
The TUI startup banner groups runtime info into four collapsible sections, each rendered with a `▸` / `▾` chevron next to the section title:
|
||||
|
||||
| Section | Default state |
|
||||
|---------|---------------|
|
||||
| Tools | Open |
|
||||
| Skills | Collapsed |
|
||||
| System Prompt | Collapsed |
|
||||
| MCP Servers | Collapsed |
|
||||
|
||||
Click anywhere on a section header (or its chevron) to toggle it. The Tools list opens by default because it's the most-checked section at session start; Skills, System Prompt, and MCP Servers collapse by default so the banner stays compact even when you've installed dozens of skills or wired up many MCP servers. State is local to the banner instance, so the next launch resets to the defaults.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js** ≥ 20 — the TUI runs as a subprocess launched from the Python CLI. `hermes doctor` verifies this.
|
||||
@@ -158,6 +171,9 @@ The status line also shows:
|
||||
|
||||
- **Working directory with git branch** — `~/projects/hermes-agent (docs/two-week-gap-sweep)`. The branch suffix updates when you `git checkout` in a side terminal (mtime-cached) so the TUI reflects your actual active branch, not whatever it was at launch.
|
||||
- **Per-prompt elapsed time** — `⏱ 12s/3m 45s` while the turn is running (live), frozen to `⏲ 32s / 3m 45s` after the turn completes. First number is time since last user message; second is total session duration. Resets on every new prompt.
|
||||
- **`🗜️ N`** — number of times the running session has been auto-compressed. Appears once the first compression fires.
|
||||
- **`▶ N`** — number of `/background` tasks currently running in this session. Appears whenever at least one task is in flight.
|
||||
- **`⚠ YOLO`** — visible warning whenever YOLO mode is on (`hermes --yolo`, `/yolo`, or `HERMES_YOLO_MODE=1`). The same badge also appears in the startup banner so you cannot launch an auto-approving session without noticing.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -215,6 +231,25 @@ Sessions are shared between the TUI and the classic CLI — both write to the sa
|
||||
|
||||
See [Sessions](sessions.md) for lifecycle, search, compression, and export.
|
||||
|
||||
## Attaching to a running gateway
|
||||
|
||||
By default the TUI spawns its own in-process gateway, so each TUI instance is self-contained. If you already have a long-lived gateway running (e.g. `hermes gateway run` in tmux, or the systemd / launchd service), you can point the TUI at that gateway instead — the TUI then becomes a thin client and shares state with every other surface (messaging platforms, web dashboard, other TUI sessions) that's attached to the same gateway.
|
||||
|
||||
Set the websocket URL via env before launching:
|
||||
|
||||
```bash
|
||||
export HERMES_TUI_GATEWAY_URL="ws://localhost:8765/api/ws?token=<auth-token>"
|
||||
hermes --tui
|
||||
```
|
||||
|
||||
The token comes from the gateway's API auth configuration (see [API Server](features/api-server.md)). When the env var is set, the TUI:
|
||||
|
||||
- Skips spawning a local gateway entirely — no duplicate platform adapters, no port conflicts.
|
||||
- Routes every action (slash commands, image attach, browser progress, voice events, …) over the websocket to the shared gateway.
|
||||
- Reconnects automatically if the gateway URL rotates (new token) between requests.
|
||||
|
||||
This is the same channel the web dashboard's embedded TUI uses (see [Web Dashboard](features/web-dashboard.md#chat)) — one gateway, many clients.
|
||||
|
||||
## Reverting to the classic CLI
|
||||
|
||||
Launching `hermes` (without `--tui`) stays on the classic CLI. To make a machine prefer the TUI, set `HERMES_TUI=1` in your shell profile. To go back, unset it.
|
||||
|
||||
@@ -24,7 +24,7 @@ If you prefer a real POSIX environment (for the dashboard's embedded terminal, `
|
||||
Open **PowerShell** (or Windows Terminal) and run:
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex
|
||||
iex (irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1)
|
||||
```
|
||||
|
||||
No admin rights required. The installer goes to `%LOCALAPPDATA%\hermes\` and adds `hermes` to your **User PATH** — open a new terminal after it finishes.
|
||||
@@ -38,11 +38,35 @@ No admin rights required. The installer goes to `%LOCALAPPDATA%\hermes\` and add
|
||||
| Parameter | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `-Branch` | `main` | Clone a specific branch (useful for testing PRs) |
|
||||
| `-Commit` | unset | Pin install to a specific commit SHA (overrides `-Branch`) |
|
||||
| `-Tag` | unset | Pin install to a specific git tag (e.g. `v0.14.0`) |
|
||||
| `-NoVenv` | off | Skip venv creation (advanced — you manage Python yourself) |
|
||||
| `-SkipSetup` | off | Skip the post-install `hermes setup` wizard |
|
||||
| `-HermesHome` | `%LOCALAPPDATA%\hermes` | Override data directory |
|
||||
| `-InstallDir` | `%LOCALAPPDATA%\hermes\hermes-agent` | Override code location |
|
||||
|
||||
The installer auto-retries flaky git fetches and strips BOM from any downloaded `install.ps1` payload, so a UTF-8 BOM picked up during HTTP transit no longer breaks the `[scriptblock]::Create((irm ...))` form.
|
||||
|
||||
### Desktop installer (alternative)
|
||||
|
||||
A thin GUI installer is also available — useful if you'd rather double-click an `.exe` than open PowerShell. Download Hermes Desktop, run the installer, and on first launch the GUI calls `install.ps1` under the hood to provision Python (via `uv`), Node, PortableGit, and the rest of the dependency bootstrap described below. After the first run, the desktop app and the PowerShell-installed `hermes` CLI share the same `%LOCALAPPDATA%\hermes\hermes-agent` install and `%USERPROFILE%\.hermes` data directory — switch between the GUI and the CLI freely.
|
||||
|
||||
Use the desktop installer when you want a familiar Windows install experience or you're handing Hermes to a non-developer; use the PowerShell one-liner when you're already in a terminal.
|
||||
|
||||
### Dependency bootstrap (`dep_ensure`)
|
||||
|
||||
On first launch (and on demand when a missing tool is detected), Hermes runs a small Python bootstrapper — `hermes_cli/dep_ensure.py` — that checks for and lazily installs the non-Python dependencies it needs. On Windows, the relevant ones are:
|
||||
|
||||
| Dependency | Why Hermes needs it |
|
||||
|---|---|
|
||||
| **PortableGit** | Provides `bash.exe` for the terminal tool and `git` for in-session clones. Provisioned at install time, not by `dep_ensure`. |
|
||||
| **Node.js 22** | Required for the browser tool (`agent-browser`), the TUI's web bridge, and the WhatsApp bridge. |
|
||||
| **ffmpeg** | Audio format conversion for TTS / voice messages. |
|
||||
| **ripgrep** | Fast file search — falls back to `grep` if unavailable. |
|
||||
| **npm packages** | `agent-browser`, Playwright Chromium, and any per-toolset Node deps are installed once at first browser-tool use. |
|
||||
|
||||
Each dep has a `shutil.which(...)`-style check; if a binary is missing and the run is interactive, `dep_ensure` offers to install it (deferring to `scripts\install.ps1 -ensure <dep>` for the actual install logic). Non-interactive runs (gateway, cron, headless desktop launches) skip the prompt and surface a clear `this feature needs <dep>` error instead.
|
||||
|
||||
## What the installer actually does
|
||||
|
||||
Top-to-bottom, in order:
|
||||
|
||||
Reference in New Issue
Block a user