hermes-bsd/website/docs/user-guide/docker.md

830 lines
44 KiB
Markdown
Raw Normal View History

docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
---
sidebar_position: 7
title: "Docker"
description: "Running Hermes Agent in Docker and using Docker as a terminal backend"
---
# Hermes Agent — Docker
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
There are two distinct ways Docker intersects with Hermes Agent:
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
1. **Running Hermes IN Docker** — the agent itself runs inside a container (this page's primary focus)
2. **Docker as a terminal backend** — the agent runs on your host but executes every command inside a single, persistent Docker sandbox container that survives across tool calls, `/new`, and subagents for the life of the Hermes process (see [Configuration → Docker Backend](./configuration.md#docker-backend))
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
This page covers option 1. The container stores all user data (config, API keys, sessions, skills, memories) in a single directory mounted from the host at `/opt/data`. The image itself is stateless and can be upgraded by pulling a new version without losing any configuration.
## Quick start
If this is your first time running Hermes Agent, create a data directory on the host and start the container interactively to run the setup wizard:
:::caution Avoid browser-based VPS consoles for the install commands
Some VPS providers (Hetzner Cloud, and several others) offer a browser-based
console for managing hosts. These consoles transmit special characters
incorrectly — `:` may arrive as `;`, `@` may be mis-rendered, and non-English
keyboard layouts fare worse — which silently corrupts `docker run` arguments
like `-v ~/.hermes:/opt/data`, `-e KEY=value`, and pasted API keys / tokens.
**Connect over SSH instead** (`ssh root@<host>`) for copy-paste-safe command
entry. If you must use the browser console, type the commands manually
instead of pasting, and double-check every `:`, `@`, `=`, and `/` in the
result before hitting Enter.
:::
```sh
mkdir -p ~/.hermes
docker run -it --rm \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent setup
```
This drops you into the setup wizard, which will prompt you for your API keys and write them to `~/.hermes/.env`. You only need to do this once. It is highly recommended to set up a chat system for the gateway to work with at this point.
:::tip
Inside the container, run `hermes setup --portal` once — the refresh token persists in the mounted `~/.hermes` volume. See [Nous Portal](/integrations/nous-portal).
:::
## Running in gateway mode
Once configured, run the container in the background as a persistent gateway (Telegram, Discord, Slack, WhatsApp, etc.):
```sh
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
nousresearch/hermes-agent gateway run
```
docs: resync reference, user-guide, developer-guide, and messaging pages against code (#17738) Broad drift audit against origin/main (b52b63396). Reference pages (most user-visible drift): - slash-commands: add /busy, /curator, /footer, /indicator, /redraw, /steer that were missing; drop non-existent /terminal-setup; fix /q footnote (resolves to /queue, not /quit); extend CLI-only list with all 24 CLI-only commands in the registry - cli-commands: add dedicated sections for hermes curator / fallback / hooks (new subcommands not previously documented); remove stale hermes honcho standalone section (the plugin registers dynamically via hermes memory); list curator/fallback/hooks in top-level table; fix completion to include fish - toolsets-reference: document the real 52-toolset count; split browser vs browser-cdp; add discord / discord_admin / spotify / yuanbao; correct hermes-cli tool count from 36 to 38; fix misleading claim that hermes-homeassistant adds tools (it's identical to hermes-cli) - tools-reference: bump tool count 55 -> 68; add 7 Spotify, 5 Yuanbao, 2 Discord toolsets; move browser_cdp/browser_dialog to their own browser-cdp toolset section - environment-variables: add 40+ user-facing HERMES_* vars that were undocumented (--yolo, --accept-hooks, --ignore-*, inference model override, agent/stream/checkpoint timeouts, OAuth trace, per-platform batch tuning for Telegram/Discord/Matrix/Feishu/WeCom, cron knobs, gateway restart/connect timeouts); dedupe the Cron Scheduler section; replace stale QQ_SANDBOX with QQ_PORTAL_HOST User-guide (top level): - cli.md: compression preserves last 20 turns, not 4 (protect_last_n: 20) - configuration.md: display.platforms is the canonical per-platform override key; tool_progress_overrides is deprecated and auto-migrated - profiles.md: model.default is the config key, not model.model - sessions.md: CLI/TUI session IDs use 6-char hex, gateway uses 8 - checkpoints-and-rollback.md: destructive-command list now matches _DESTRUCTIVE_PATTERNS (adds rmdir, cp, install, dd) - docker.md: the container runs as non-root hermes (UID 10000) via gosu; fix install command (uv pip); add missing --insecure on the dashboard compose example (required for non-loopback bind) - security.md: systemctl danger pattern also matches 'restart' - index.md: built-in tool count 47 -> 68 - integrations/index.md: 6 STT providers, 8 memory providers - integrations/providers.md: drop fictional dashscope/qwen aliases Features: - overview.md: 9 image models (not 8), 9 TTS providers (not 5), 8 memory providers (Supermemory was missing) - tool-gateway.md: 9 image models - tools.md: extend common-toolsets list with search / messaging / spotify / discord / debugging / safe - fallback-providers.md: add 6 real providers from PROVIDER_REGISTRY (lmstudio, kimi-coding-cn, stepfun, alibaba-coding-plan, tencent-tokenhub, azure-foundry) - plugins.md: Available Hooks table now includes on_session_finalize, on_session_reset, subagent_stop - built-in-plugins.md: add the 7 bundled plugins the page didn't mention (spotify, google_meet, three image_gen providers, two dashboard examples) - web-dashboard.md: add --insecure and --tui flags - cron.md: hermes cron create takes positional schedule/prompt, not flags Messaging: - telegram.md: TELEGRAM_WEBHOOK_SECRET is now REQUIRED when TELEGRAM_WEBHOOK_URL is set (gateway refuses to start without it per GHSA-3vpc-7q5r-276h). Biggest user-visible drift in the batch. - discord.md: HERMES_DISCORD_TEXT_BATCH_SPLIT_DELAY_SECONDS default is 2.0, not 0.1 - dingtalk.md: document DINGTALK_REQUIRE_MENTION / FREE_RESPONSE_CHATS / MENTION_PATTERNS / HOME_CHANNEL / ALLOW_ALL_USERS that the adapter supports - bluebubbles.md: drop fictional BLUEBUBBLES_SEND_READ_RECEIPTS env var; the setting lives in platforms.bluebubbles.extra only - qqbot.md: drop dead QQ_SANDBOX; add real QQ_PORTAL_HOST and QQ_GROUP_ALLOWED_USERS - wecom-callback.md: replace 'hermes gateway start' (service-only) with 'hermes gateway' for first-time setup Developer-guide: - architecture.md: refresh tool/toolset counts (61/52), terminal backend count (7), line counts for run_agent.py (~13.7k), cli.py (~11.5k), main.py (~10.4k), setup.py (~3.5k), gateway/run.py (~12.2k), mcp_tool.py (~3.1k); add yuanbao adapter, bump platform adapter count 18 -> 20 - agent-loop.md: run_agent.py line count 10.7k -> 13.7k - tools-runtime.md: add vercel_sandbox backend - adding-tools.md: remove stale 'Discovery import added to model_tools.py' checklist item (registry auto-discovery) - adding-platform-adapters.md: mark send_typing / get_chat_info as concrete base methods; only connect/disconnect/send are abstract - acp-internals.md: ACP sessions now persist to SessionDB (~/.hermes/state.db); acp.run_agent call uses use_unstable_protocol=True - cron-internals.md: gateway runs scheduler in a dedicated background thread via _start_cron_ticker, not on a maintenance cycle; locking is cross-process via fcntl.flock (Unix) / msvcrt.locking (Windows) - gateway-internals.md: gateway/run.py ~12k lines - provider-runtime.md: cron DOES support fallback (run_job reads fallback_providers from config) - session-storage.md: SCHEMA_VERSION = 11 (not 9); add migrations 10 and 11 (trigram FTS, inline-mode FTS5 re-index); add api_call_count column to Sessions DDL; document messages_fts_trigram and state_meta in the architecture tree - context-compression-and-caching.md: remove the obsolete 'context pressure warnings' section (warnings were removed for causing models to give up early) - context-engine-plugin.md: compress() signature now includes focus_topic param - extending-the-cli.md: _build_tui_layout_children signature now includes model_picker_widget; add to default layout Also fixed three pre-existing broken links/anchors the build warned about (docker.md -> api-server.md, yuanbao.md -> cron-jobs.md and tips#background-tasks, nix-setup.md -> #container-aware-cli). Regenerated per-skill pages via website/scripts/generate-skill-docs.py so catalog tables and sidebar are consistent with current SKILL.md frontmatter. docusaurus build: clean, no broken links or anchors.
2026-04-29 20:55:59 -07:00
Port 8642 exposes the gateway's [OpenAI-compatible API server](./features/api-server.md) and health endpoint. It's optional if you only use chat platforms (Telegram, Discord, etc.), but required if you want the dashboard or external tools to reach the gateway.
feat(docker): auto-redirect `gateway run` to supervised mode inside s6 image Pre-s6, `docker run nousresearch/hermes-agent gateway run` was the standard invocation: gateway ran as the container's main process, tini reaped zombies, container exit code matched gateway exit code, no supervision. With s6-overlay as PID 1, the same invocation now auto-upgrades to supervised semantics — auto-restart on crash, dashboard supervised alongside (when HERMES_DASHBOARD=1 is set), multiple profile gateways under the same /init. Users get the new behavior with zero changes to their docker run command. A loud one-line breadcrumb on stderr explains the upgrade and points at the opt-out for users who genuinely want pre-s6 foreground semantics. How it works: 1. `_gateway_command_inner` (the `gateway run` handler) checks if we're inside a container with s6 as PID 1. 2. If yes, dispatches `start` to the s6 service manager (registers and starts gateway-default), then `exec sleep infinity` to keep the CMD process alive without binding container lifetime to gateway PID lifetime. The supervised gateway can flap freely; `docker stop` still tears everything down via /init stage 3. 3. If no, falls through to the existing foreground code path unchanged. Host runs of `hermes gateway run` are unaffected. Three gates make the redirect inert outside the intended scope: * `detect_service_manager() != "s6"` — host/non-s6-container runs. * `HERMES_S6_SUPERVISED_CHILD=1` env var (recursion guard) — exported by `S6ServiceManager._render_run_script` for the s6-supervised invocation itself. Without this guard, the supervised `gateway run --replace` would re-enter the redirect and recurse (run → start → run → start → ...) infinitely. * `--no-supervise` CLI flag OR `HERMES_GATEWAY_NO_SUPERVISE=1` env var — explicit user opt-out for CI smoke tests, debugging the foreground startup path, or any case wanting "CMD exit = container exit" semantics. Strict truthiness (1/true/yes, case-insensitive); typos like `=0` do NOT silently opt out. Tests: * Unit tests in tests/hermes_cli/test_gateway_s6_dispatch.py cover all five paths (host no-op, supervised fire, sentinel recursion guard, CLI flag, env var truthy + falsy). The two load-bearing gates (sentinel + opt-out) were mutation-tested by removing each gate in isolation and confirming the dedicated test fails with the expected error. * Docker harness tests in tests/docker/test_gateway_run_supervised.py cover the round trips end-to-end against a built image: redirect fires (sleep-infinity heartbeat + supervised gateway-default slot + breadcrumb), --no-supervise opt-out (foreground gateway, no want-up on the slot), HERMES_GATEWAY_NO_SUPERVISE env var works identically, recursion is impossible (≤1 supervised python gateway-run + exactly 1 sleep-infinity parented to the CMD wrapper), and HERMES_DASHBOARD=1 produces both supervised gateway and supervised dashboard. Docs: * Added a `:::tip Gateway runs supervised` admonition near the main docker.md example explaining the upgrade and pointing at the opt-out. Pre-s6 (tini-based) images still run gateway run as the foreground main process, so the note is scoped to the s6 image only. Trade-off documented in the helper docstring: container exit code under the redirect is sleep's exit code (always 0 on SIGTERM), not the gateway's. That was an explicit design call — the supervised gateway is allowed to flap without taking the container with it, which is what "supervision" means. CI users who want exit-code forwarding can pass --no-supervise.
2026-05-28 11:43:46 +10:00
:::tip Gateway runs supervised
Inside the official Docker image, `gateway run` is **automatically supervised by s6-overlay**: if the gateway process crashes it's restarted within a couple of seconds without losing the container, and the dashboard (when `HERMES_DASHBOARD=1` is set) is supervised alongside it. The `gateway run` CMD process itself is a `sleep infinity` heartbeat that keeps the container alive while s6 manages the actual gateway process — so `docker stop` still shuts everything down cleanly, but `docker logs` shows the supervised gateway's output.
You'll see a one-line breadcrumb in `docker logs` confirming the upgrade. To opt out — and get the historical "gateway is the container's main process, container exit = gateway exit" semantics — pass `--no-supervise` or set `HERMES_GATEWAY_NO_SUPERVISE=1`. The opt-out is useful for CI smoke tests that want the container to exit with the gateway's status code; for production deployments the supervised default is strictly better.
This behavior applies to the s6-based image only. Earlier (tini-based) images still run `gateway run` as the foreground main process.
:::
fix(docker): tee supervised gateway stdout to docker logs Follow-up to #33583 (the gateway-run-supervised redirect). Before this fix, the supervised gateway's stdout (most visibly the "Hermes Gateway Starting…" rich-console banner) was swallowed by `s6-log` into the rotated file at `${HERMES_HOME}/logs/gateways/<profile>/current` and never reached `docker logs`. Operational signal lived in two places: * **docker logs** — saw stderr (Python `logging` defaults to stderr), so warnings/errors were visible. * **the rotated file** — saw stdout (rich banners, `print()` output, third-party libs that wrote to fd 1). This was surprising for users coming from the pre-s6 image, where `docker run … gateway run` produced a single unified stream in `docker logs`. They'd see partial output, conclude something was broken, and dig around for the missing pieces. Fix: add the `1` s6-log action directive before the file destination so each line is forwarded to s6-log's stdout — which propagates up the s6-supervise pipeline to /init's stdout = container stdout = `docker logs`. The file destination is preserved as a second destination, so the rotated log (with ISO 8601 timestamps) still exists for `hermes logs` and for survival across container restarts. Trade-off considered: timestamps. Putting `T` between `1` and the file destination (not before `1`) means: * docker logs sees raw lines — Python's logging formatter has its own timestamps, and `docker logs --timestamps` adds another layer when desired. No double-stamping in the common reading path. * The persisted file gets s6-log's ISO 8601 timestamp so even output that lacked a Python-logger timestamp (rich banners, third-party raw prints) is correlatable in `current`. Verification: * New unit-test assertion in `test_service_manager.py` locks the `s6-log 1` directive into the rendered run-script. Mutation- tested by reverting to the pre-fix script (no `1`); the assert catches it cleanly. * New docker-harness test `test_supervised_gateway_stdout_reaches_docker_logs` builds the image, runs `docker run … gateway run`, and asserts the unique `⚕` banner glyph reaches `docker logs`. Also verifies the rotated file still contains the banner (no regression on the existing file destination). Mutation-tested end-to-end: built a deliberately-broken image without the `1` directive and the test failed exactly as designed, citing the banner present in `current` but absent from `docker logs`. * `website/docs/user-guide/docker.md` gains a new `:::note Where gateway logs go` admonition documenting both destinations and the audit-log file at `${HERMES_HOME}/logs/container-boot.log`. Existing functionality preserved: every other docker-harness test still passes against the new image. Unit-test sweep across `tests/hermes_cli/` (5561 tests) is green.
2026-05-28 13:06:11 +10:00
:::note Where gateway logs go
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
See the [Where the logs go](#where-the-logs-go) section below for the full routing map (per-profile gateways, dashboard, boot reconciler, container-wide `docker logs`).
fix(docker): tee supervised gateway stdout to docker logs Follow-up to #33583 (the gateway-run-supervised redirect). Before this fix, the supervised gateway's stdout (most visibly the "Hermes Gateway Starting…" rich-console banner) was swallowed by `s6-log` into the rotated file at `${HERMES_HOME}/logs/gateways/<profile>/current` and never reached `docker logs`. Operational signal lived in two places: * **docker logs** — saw stderr (Python `logging` defaults to stderr), so warnings/errors were visible. * **the rotated file** — saw stdout (rich banners, `print()` output, third-party libs that wrote to fd 1). This was surprising for users coming from the pre-s6 image, where `docker run … gateway run` produced a single unified stream in `docker logs`. They'd see partial output, conclude something was broken, and dig around for the missing pieces. Fix: add the `1` s6-log action directive before the file destination so each line is forwarded to s6-log's stdout — which propagates up the s6-supervise pipeline to /init's stdout = container stdout = `docker logs`. The file destination is preserved as a second destination, so the rotated log (with ISO 8601 timestamps) still exists for `hermes logs` and for survival across container restarts. Trade-off considered: timestamps. Putting `T` between `1` and the file destination (not before `1`) means: * docker logs sees raw lines — Python's logging formatter has its own timestamps, and `docker logs --timestamps` adds another layer when desired. No double-stamping in the common reading path. * The persisted file gets s6-log's ISO 8601 timestamp so even output that lacked a Python-logger timestamp (rich banners, third-party raw prints) is correlatable in `current`. Verification: * New unit-test assertion in `test_service_manager.py` locks the `s6-log 1` directive into the rendered run-script. Mutation- tested by reverting to the pre-fix script (no `1`); the assert catches it cleanly. * New docker-harness test `test_supervised_gateway_stdout_reaches_docker_logs` builds the image, runs `docker run … gateway run`, and asserts the unique `⚕` banner glyph reaches `docker logs`. Also verifies the rotated file still contains the banner (no regression on the existing file destination). Mutation-tested end-to-end: built a deliberately-broken image without the `1` directive and the test failed exactly as designed, citing the banner present in `current` but absent from `docker logs`. * `website/docs/user-guide/docker.md` gains a new `:::note Where gateway logs go` admonition documenting both destinations and the audit-log file at `${HERMES_HOME}/logs/container-boot.log`. Existing functionality preserved: every other docker-harness test still passes against the new image. Unit-test sweep across `tests/hermes_cli/` (5561 tests) is green.
2026-05-28 13:06:11 +10:00
:::
:::note Tool-loop hard stops for unattended gateways
The `tool_loop_guardrails.hard_stop_enabled` setting defaults to `false`, which is reasonable for interactive CLI and TUI sessions where a person can see repeated tool-call warnings. In unattended gateway or server deployments, warnings alone may not stop an agent that gets stuck in a repeated tool-call loop. Operators who want circuit-breaker behavior should explicitly enable hard stops in the profile's `config.yaml`:
```yaml
tool_loop_guardrails:
hard_stop_enabled: true
hard_stop_after:
exact_failure: 5
idempotent_no_progress: 5
```
:::
Note: the API server is gated on `API_SERVER_ENABLED=true`. To expose it beyond `127.0.0.1` inside the container, also set `API_SERVER_HOST=0.0.0.0` and an `API_SERVER_KEY` (minimum 8 characters — generate one with `openssl rand -hex 32`). Example:
```sh
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
-e API_SERVER_ENABLED=true \
-e API_SERVER_HOST=0.0.0.0 \
-e API_SERVER_KEY="$(openssl rand -hex 32)" \
-e API_SERVER_CORS_ORIGINS='*' \
nousresearch/hermes-agent gateway run
```
Opening any port on an internet facing machine is a security risk. You should not do it unless you understand the risks.
## Running the dashboard
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
The built-in web dashboard runs as a supervised s6-rc service alongside the gateway in the same container. Set `HERMES_DASHBOARD=1` to bring it up:
```sh
docker run -d \
feat(docker): launch dashboard as side-process via HERMES_DASHBOARD=1 Adds an optional dashboard side-process to the container entrypoint, toggled by `HERMES_DASHBOARD=1` (also accepts `true` / `yes`). When set, the entrypoint backgrounds `hermes dashboard` before `exec`-ing the main command so the user's chosen foreground process (gateway, chat, `sleep infinity`, …) remains PID-of-interest for the container runtime. docker run -d \ -v ~/.hermes:/opt/data \ -p 8642:8642 -p 9119:9119 \ -e HERMES_DASHBOARD=1 \ nousresearch/hermes-agent gateway run Defaults chosen for the container case: - Host: 0.0.0.0 (reachable through published port; can override to 127.0.0.1 via HERMES_DASHBOARD_HOST for sidecar/reverse-proxy setups) - Port: 9119 (matches `hermes dashboard`) - Auto-adds `--insecure` when binding to non-localhost, matching the dashboard's own safety gate for exposing API keys - HERMES_DASHBOARD_TUI is read by `hermes dashboard` directly — no entrypoint plumbing needed Dashboard output is prefixed with `[dashboard]` via `stdbuf`+`sed -u` so it's easy to separate from gateway logs in `docker logs`. No supervision: if the dashboard crashes it stays down until the container restarts (documented in the `:::note` panel). Other changes bundled in: - Deprecate GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT env vars in hermes_cli/web_server.py with a DEPRECATED block comment and a `.. deprecated::` note on _probe_gateway_health. The feature still works for this release; it'll be removed alongside the move to a first-class dashboard config key. - Rewrite the "Running the dashboard" doc section around the new single-container pattern. Drops the previously-documented dashboard-as-its-own-container setup — that pattern relied on the deprecated env vars for cross-container gateway-liveness detection, and without them the dashboard would permanently report the gateway as "not running". - Collapse the two-service Compose example (gateway + dashboard container) into a single service with HERMES_DASHBOARD=1. Removes the now-unnecessary bridge network and `depends_on`. - Drop the ":::warning" caveat about "Running a dashboard container alongside the gateway is safe" — that case no longer exists.
2026-05-04 15:37:27 +10:00
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
feat(docker): launch dashboard as side-process via HERMES_DASHBOARD=1 Adds an optional dashboard side-process to the container entrypoint, toggled by `HERMES_DASHBOARD=1` (also accepts `true` / `yes`). When set, the entrypoint backgrounds `hermes dashboard` before `exec`-ing the main command so the user's chosen foreground process (gateway, chat, `sleep infinity`, …) remains PID-of-interest for the container runtime. docker run -d \ -v ~/.hermes:/opt/data \ -p 8642:8642 -p 9119:9119 \ -e HERMES_DASHBOARD=1 \ nousresearch/hermes-agent gateway run Defaults chosen for the container case: - Host: 0.0.0.0 (reachable through published port; can override to 127.0.0.1 via HERMES_DASHBOARD_HOST for sidecar/reverse-proxy setups) - Port: 9119 (matches `hermes dashboard`) - Auto-adds `--insecure` when binding to non-localhost, matching the dashboard's own safety gate for exposing API keys - HERMES_DASHBOARD_TUI is read by `hermes dashboard` directly — no entrypoint plumbing needed Dashboard output is prefixed with `[dashboard]` via `stdbuf`+`sed -u` so it's easy to separate from gateway logs in `docker logs`. No supervision: if the dashboard crashes it stays down until the container restarts (documented in the `:::note` panel). Other changes bundled in: - Deprecate GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT env vars in hermes_cli/web_server.py with a DEPRECATED block comment and a `.. deprecated::` note on _probe_gateway_health. The feature still works for this release; it'll be removed alongside the move to a first-class dashboard config key. - Rewrite the "Running the dashboard" doc section around the new single-container pattern. Drops the previously-documented dashboard-as-its-own-container setup — that pattern relied on the deprecated env vars for cross-container gateway-liveness detection, and without them the dashboard would permanently report the gateway as "not running". - Collapse the two-service Compose example (gateway + dashboard container) into a single service with HERMES_DASHBOARD=1. Removes the now-unnecessary bridge network and `depends_on`. - Drop the ":::warning" caveat about "Running a dashboard container alongside the gateway is safe" — that case no longer exists.
2026-05-04 15:37:27 +10:00
-p 8642:8642 \
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
-p 9119:9119 \
feat(docker): launch dashboard as side-process via HERMES_DASHBOARD=1 Adds an optional dashboard side-process to the container entrypoint, toggled by `HERMES_DASHBOARD=1` (also accepts `true` / `yes`). When set, the entrypoint backgrounds `hermes dashboard` before `exec`-ing the main command so the user's chosen foreground process (gateway, chat, `sleep infinity`, …) remains PID-of-interest for the container runtime. docker run -d \ -v ~/.hermes:/opt/data \ -p 8642:8642 -p 9119:9119 \ -e HERMES_DASHBOARD=1 \ nousresearch/hermes-agent gateway run Defaults chosen for the container case: - Host: 0.0.0.0 (reachable through published port; can override to 127.0.0.1 via HERMES_DASHBOARD_HOST for sidecar/reverse-proxy setups) - Port: 9119 (matches `hermes dashboard`) - Auto-adds `--insecure` when binding to non-localhost, matching the dashboard's own safety gate for exposing API keys - HERMES_DASHBOARD_TUI is read by `hermes dashboard` directly — no entrypoint plumbing needed Dashboard output is prefixed with `[dashboard]` via `stdbuf`+`sed -u` so it's easy to separate from gateway logs in `docker logs`. No supervision: if the dashboard crashes it stays down until the container restarts (documented in the `:::note` panel). Other changes bundled in: - Deprecate GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT env vars in hermes_cli/web_server.py with a DEPRECATED block comment and a `.. deprecated::` note on _probe_gateway_health. The feature still works for this release; it'll be removed alongside the move to a first-class dashboard config key. - Rewrite the "Running the dashboard" doc section around the new single-container pattern. Drops the previously-documented dashboard-as-its-own-container setup — that pattern relied on the deprecated env vars for cross-container gateway-liveness detection, and without them the dashboard would permanently report the gateway as "not running". - Collapse the two-service Compose example (gateway + dashboard container) into a single service with HERMES_DASHBOARD=1. Removes the now-unnecessary bridge network and `depends_on`. - Drop the ":::warning" caveat about "Running a dashboard container alongside the gateway is safe" — that case no longer exists.
2026-05-04 15:37:27 +10:00
-e HERMES_DASHBOARD=1 \
nousresearch/hermes-agent gateway run
```
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
The dashboard is supervised by s6 — if it crashes, `s6-supervise` restarts it automatically after a short backoff. Dashboard stdout/stderr is forwarded to `docker logs <container>` (no prefix; the gateway's own output now lives in a per-profile s6-log file — see [Where the logs go](#where-the-logs-go) below — so the two streams don't clash).
| Environment variable | Description | Default |
|---------------------|-------------|---------|
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
| `HERMES_DASHBOARD` | Set to `1` (or `true` / `yes`) to enable the supervised dashboard service | *(unset — service is registered but stays down)* |
| `HERMES_DASHBOARD_HOST` | Bind address for the dashboard HTTP server | `0.0.0.0` |
feat(docker): launch dashboard as side-process via HERMES_DASHBOARD=1 Adds an optional dashboard side-process to the container entrypoint, toggled by `HERMES_DASHBOARD=1` (also accepts `true` / `yes`). When set, the entrypoint backgrounds `hermes dashboard` before `exec`-ing the main command so the user's chosen foreground process (gateway, chat, `sleep infinity`, …) remains PID-of-interest for the container runtime. docker run -d \ -v ~/.hermes:/opt/data \ -p 8642:8642 -p 9119:9119 \ -e HERMES_DASHBOARD=1 \ nousresearch/hermes-agent gateway run Defaults chosen for the container case: - Host: 0.0.0.0 (reachable through published port; can override to 127.0.0.1 via HERMES_DASHBOARD_HOST for sidecar/reverse-proxy setups) - Port: 9119 (matches `hermes dashboard`) - Auto-adds `--insecure` when binding to non-localhost, matching the dashboard's own safety gate for exposing API keys - HERMES_DASHBOARD_TUI is read by `hermes dashboard` directly — no entrypoint plumbing needed Dashboard output is prefixed with `[dashboard]` via `stdbuf`+`sed -u` so it's easy to separate from gateway logs in `docker logs`. No supervision: if the dashboard crashes it stays down until the container restarts (documented in the `:::note` panel). Other changes bundled in: - Deprecate GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT env vars in hermes_cli/web_server.py with a DEPRECATED block comment and a `.. deprecated::` note on _probe_gateway_health. The feature still works for this release; it'll be removed alongside the move to a first-class dashboard config key. - Rewrite the "Running the dashboard" doc section around the new single-container pattern. Drops the previously-documented dashboard-as-its-own-container setup — that pattern relied on the deprecated env vars for cross-container gateway-liveness detection, and without them the dashboard would permanently report the gateway as "not running". - Collapse the two-service Compose example (gateway + dashboard container) into a single service with HERMES_DASHBOARD=1. Removes the now-unnecessary bridge network and `depends_on`. - Drop the ":::warning" caveat about "Running a dashboard container alongside the gateway is safe" — that case no longer exists.
2026-05-04 15:37:27 +10:00
| `HERMES_DASHBOARD_PORT` | Port for the dashboard HTTP server | `9119` |
| `HERMES_DASHBOARD_INSECURE` | **Deprecated / no-op.** Formerly bypassed the auth gate; as of the June 2026 hardening it no longer disables authentication. A non-loopback bind always requires an auth provider | *(ignored — configure a provider instead)* |
docker: opt in to dashboard --insecure via env var, never derive from bind host The s6 dashboard run script flipped `--insecure` on whenever `HERMES_DASHBOARD_HOST` was anything other than 127.0.0.1 / localhost. That comment ("the dashboard refuses otherwise") predates the OAuth auth gate: back when it was written, `start_server` would SystemExit on any non-loopback bind, so the run script's `--insecure` was the only way to make in-container deployments work at all. The gate has since been replaced by `should_require_auth(host, allow_public)`, which engages the OAuth flow when a `DashboardAuthProvider` is registered (the bundled `dashboard_auth/nous` provider auto-registers on `HERMES_DASHBOARD_OAUTH_CLIENT_ID`) and fails closed with a specific operator-facing error when none is. The host-derived `--insecure` ran upstream of all that and silently disabled the gate on every container-deployed dashboard. Most visible under the portal's wildcard-subdomain rollout: every Fly machine binds 0.0.0.0 so the edge can reach Flycast, every machine boots with the correct `HERMES_DASHBOARD_OAUTH_CLIENT_ID`, the nous provider registers — and `/api/status` still returns `{"auth_required": false, "auth_providers": ["nous"]}` because the run script disabled the gate before `start_server` ever saw the request. The dashboard SPA was served to anyone, no `/login` redirect, no OAuth challenge. Fix: derive `--insecure` from an explicit opt-in env var, `HERMES_DASHBOARD_INSECURE` (truthy values matching the rest of the s6 boolean envs: 1, true, TRUE, True, yes, YES, Yes). Operators on trusted LANs behind a reverse proxy without the OAuth contract (the existing `docker-compose.windows.yml` use case) opt in explicitly; portal-managed agent deployments leave it unset and let the gate engage. `docker-compose.windows.yml` already passes `--insecure` on the `command:` array directly (line 38), so it doesn't depend on the s6 auto-injection. No compose-file change required. Tests: * `tests/test_docker_home_override_scripts.py` — extends the existing static-text guard with a regression assertion that the legacy host-derived case-statement is gone and the new env-var opt-in is present (locks against accidental revert). * `tests/docker/test_dashboard.py` — adds two Docker-in-Docker tests exercising the actual `/api/status` round-trip: - 0.0.0.0 bind + `HERMES_DASHBOARD_OAUTH_CLIENT_ID` → gate engaged - 0.0.0.0 bind + `HERMES_DASHBOARD_INSECURE=1` → gate disabled Docs: * `website/docs/user-guide/docker.md` + zh-Hans i18n — adds the new env var to the table, replaces the stale prose ("the entrypoint no longer auto-enables insecure mode" — which until this PR was flat-out wrong) with an accurate description of the gate's trigger conditions and the explicit opt-out. shellcheck clean. Python static-text test passes locally. Behavioural test will run against any future image build (CI's Docker harness).
2026-05-29 09:45:33 +10:00
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
The dashboard inside the container defaults to binding `0.0.0.0` — without it, the published `-p 9119:9119` port would not be reachable from the host. To restrict the bind to container loopback (for sidecar / reverse-proxy setups), set `HERMES_DASHBOARD_HOST=127.0.0.1`.
The dashboard's auth gate engages automatically when both of the following are true:
docker: opt in to dashboard --insecure via env var, never derive from bind host The s6 dashboard run script flipped `--insecure` on whenever `HERMES_DASHBOARD_HOST` was anything other than 127.0.0.1 / localhost. That comment ("the dashboard refuses otherwise") predates the OAuth auth gate: back when it was written, `start_server` would SystemExit on any non-loopback bind, so the run script's `--insecure` was the only way to make in-container deployments work at all. The gate has since been replaced by `should_require_auth(host, allow_public)`, which engages the OAuth flow when a `DashboardAuthProvider` is registered (the bundled `dashboard_auth/nous` provider auto-registers on `HERMES_DASHBOARD_OAUTH_CLIENT_ID`) and fails closed with a specific operator-facing error when none is. The host-derived `--insecure` ran upstream of all that and silently disabled the gate on every container-deployed dashboard. Most visible under the portal's wildcard-subdomain rollout: every Fly machine binds 0.0.0.0 so the edge can reach Flycast, every machine boots with the correct `HERMES_DASHBOARD_OAUTH_CLIENT_ID`, the nous provider registers — and `/api/status` still returns `{"auth_required": false, "auth_providers": ["nous"]}` because the run script disabled the gate before `start_server` ever saw the request. The dashboard SPA was served to anyone, no `/login` redirect, no OAuth challenge. Fix: derive `--insecure` from an explicit opt-in env var, `HERMES_DASHBOARD_INSECURE` (truthy values matching the rest of the s6 boolean envs: 1, true, TRUE, True, yes, YES, Yes). Operators on trusted LANs behind a reverse proxy without the OAuth contract (the existing `docker-compose.windows.yml` use case) opt in explicitly; portal-managed agent deployments leave it unset and let the gate engage. `docker-compose.windows.yml` already passes `--insecure` on the `command:` array directly (line 38), so it doesn't depend on the s6 auto-injection. No compose-file change required. Tests: * `tests/test_docker_home_override_scripts.py` — extends the existing static-text guard with a regression assertion that the legacy host-derived case-statement is gone and the new env-var opt-in is present (locks against accidental revert). * `tests/docker/test_dashboard.py` — adds two Docker-in-Docker tests exercising the actual `/api/status` round-trip: - 0.0.0.0 bind + `HERMES_DASHBOARD_OAUTH_CLIENT_ID` → gate engaged - 0.0.0.0 bind + `HERMES_DASHBOARD_INSECURE=1` → gate disabled Docs: * `website/docs/user-guide/docker.md` + zh-Hans i18n — adds the new env var to the table, replaces the stale prose ("the entrypoint no longer auto-enables insecure mode" — which until this PR was flat-out wrong) with an accurate description of the gate's trigger conditions and the explicit opt-out. shellcheck clean. Python static-text test passes locally. Behavioural test will run against any future image build (CI's Docker harness).
2026-05-29 09:45:33 +10:00
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
1. The bind host is non-loopback (e.g. the default `0.0.0.0` inside the container), **and**
docker: opt in to dashboard --insecure via env var, never derive from bind host The s6 dashboard run script flipped `--insecure` on whenever `HERMES_DASHBOARD_HOST` was anything other than 127.0.0.1 / localhost. That comment ("the dashboard refuses otherwise") predates the OAuth auth gate: back when it was written, `start_server` would SystemExit on any non-loopback bind, so the run script's `--insecure` was the only way to make in-container deployments work at all. The gate has since been replaced by `should_require_auth(host, allow_public)`, which engages the OAuth flow when a `DashboardAuthProvider` is registered (the bundled `dashboard_auth/nous` provider auto-registers on `HERMES_DASHBOARD_OAUTH_CLIENT_ID`) and fails closed with a specific operator-facing error when none is. The host-derived `--insecure` ran upstream of all that and silently disabled the gate on every container-deployed dashboard. Most visible under the portal's wildcard-subdomain rollout: every Fly machine binds 0.0.0.0 so the edge can reach Flycast, every machine boots with the correct `HERMES_DASHBOARD_OAUTH_CLIENT_ID`, the nous provider registers — and `/api/status` still returns `{"auth_required": false, "auth_providers": ["nous"]}` because the run script disabled the gate before `start_server` ever saw the request. The dashboard SPA was served to anyone, no `/login` redirect, no OAuth challenge. Fix: derive `--insecure` from an explicit opt-in env var, `HERMES_DASHBOARD_INSECURE` (truthy values matching the rest of the s6 boolean envs: 1, true, TRUE, True, yes, YES, Yes). Operators on trusted LANs behind a reverse proxy without the OAuth contract (the existing `docker-compose.windows.yml` use case) opt in explicitly; portal-managed agent deployments leave it unset and let the gate engage. `docker-compose.windows.yml` already passes `--insecure` on the `command:` array directly (line 38), so it doesn't depend on the s6 auto-injection. No compose-file change required. Tests: * `tests/test_docker_home_override_scripts.py` — extends the existing static-text guard with a regression assertion that the legacy host-derived case-statement is gone and the new env-var opt-in is present (locks against accidental revert). * `tests/docker/test_dashboard.py` — adds two Docker-in-Docker tests exercising the actual `/api/status` round-trip: - 0.0.0.0 bind + `HERMES_DASHBOARD_OAUTH_CLIENT_ID` → gate engaged - 0.0.0.0 bind + `HERMES_DASHBOARD_INSECURE=1` → gate disabled Docs: * `website/docs/user-guide/docker.md` + zh-Hans i18n — adds the new env var to the table, replaces the stale prose ("the entrypoint no longer auto-enables insecure mode" — which until this PR was flat-out wrong) with an accurate description of the gate's trigger conditions and the explicit opt-out. shellcheck clean. Python static-text test passes locally. Behavioural test will run against any future image build (CI's Docker harness).
2026-05-29 09:45:33 +10:00
2. A `DashboardAuthProvider` plugin is registered.
docs(dashboard): clarify auth provider suitability + registration across dashboard/Docker/Desktop docs (#39633) * docs(dashboard): clarify auth provider suitability + document dashboard registration - Add a 'Registering a dashboard' subsection under the Nous Research provider covering both the 'hermes dashboard register' CLI command and the Portal /local-dashboards GUI page. - Note that the Nous provider is the one suitable for public-internet exposure (logins verified against your Nous account). - Add a warning that the username/password provider is for trusted networks / VPN only and is not suitable for direct public-internet exposure; point readers to the Nous / OIDC / custom OAuth providers. - Surface the same distinction in the two-provider intro list. * docs(dashboard): count three bundled auth providers, add self-hosted OIDC to intro 'Two providers ship in the box' undercounted — the bundled plugins/dashboard_auth/self_hosted (generic OpenID Connect) is a third. List all three in the gated-mode intro and link each to its section. * docs(dashboard): extend auth provider updates to Docker and Desktop pages - docker.md: list all three bundled gate providers (was username/password + OAuth only), adding the self-hosted OIDC provider and its env vars, and note username/password is not for public-internet exposure. - desktop.md: reframe the remote-backend connection so OAuth (Nous Portal) is the preferred option for any backend reachable beyond the local machine, with username/password positioned for local / trusted-network use only. Cover the 'Sign in with <provider>' OAuth flow in the in-app steps and scope the VPN warning to the password path. * docs(dashboard): align env-var, CLI, and remote-Desktop recipe with provider changes - environment-variables.md: reframe the Web Dashboard & Hermes Desktop intro (OAuth preferred for remote/public, username/password for trusted networks), add the self-hosted OIDC env vars (HERMES_DASHBOARD_OIDC_*) that were missing from the table, and note hermes dashboard register provisions the OAuth client_id. - cli-commands.md: document the 'hermes dashboard register' subcommand (flags, behavior, /local-dashboards GUI alternative). - web-dashboard.md: apply the OAuth-preferred reframe to the bottom 'Connecting Hermes Desktop to a remote backend' recipe and scope its VPN warning to the username/password path, matching desktop.md. * docs(dashboard): move 'recommended remote Desktop path' framing from username/password to OAuth The gated-mode intro list claimed the username/password provider was the recommended path for a remote Hermes Desktop connection, contradicting the OAuth-preferred framing established elsewhere. Move that recommendation onto the OAuth (Nous Portal) item so the docs are consistent: OAuth is the recommended provider for any remote/internet-facing backend; username/password is for trusted networks only. * docs(dashboard): drop unreleased managed/hosted-install provisioning notes Remove the 'not available in managed/hosted installs, where the client id is provisioned by the hosting platform' line from the dashboard register docs (web-dashboard.md, cli-commands.md) and the 'provisioned by the Nous Portal for hosted deploys' clause from the HERMES_DASHBOARD_OAUTH_CLIENT_ID env-var row — that platform-provisioning path is unreleased. * docs(dashboard): drop --portal-url / HERMES_DASHBOARD_PORTAL_URL from user docs The portal-URL override targets a non-production Nous Portal and only works for internal Nous usage — it won't function for end users (the access token must be issued by the same portal). Remove it from the register CLI flags, the Nous-provider config/env tables, and the verify-the-gate example so users aren't pointed at an option that can't work for them. * docs(dashboard): add worked examples for Nous and username/password providers The self-hosted OIDC provider already had a full 'Worked example: Keycloak' walkthrough; the Nous and username/password providers only had scattered config snippets. Add parallel '#### Worked example' sections for both (register/run/login + /api/status verification), mirroring the Keycloak example's structure so all three bundled providers read consistently. * docs(env): move HERMES_DESKTOP_REMOTE_URL to end of the dashboard auth table It was sitting between the HERMES_DASHBOARD_BASIC_AUTH_* block and the HERMES_DASHBOARD_OAUTH/OIDC block, splitting the dashboard-side vars. As the only desktop-side var in the table, it belongs at the end so the dashboard provider vars (basic, OAuth, OIDC) stay grouped together. * docs(dashboard): remove Fly.io references from dashboard auth docs Fly.io is the internal hosting implementation for hosted Hermes — it shouldn't leak into user-facing dashboard auth docs. Reword the OAuth provider intro, the env-var-path rationale, the public-URL-override section, the cookie Secure note, and the verify-the-gate example to generic 'hosting platform' / 'reverse proxy' / 'TLS terminator' phrasing. Left the legitimate user-facing Fly.io mentions in telegram.md (a deliberate cloud-deployment walkthrough) and work-with-skills.md (a generic example) untouched.
2026-06-05 18:34:19 +10:00
There are three bundled ways to satisfy the second condition:
- **Username/password** — the simplest for a self-hosted / on-prem / homelab container on a trusted network or behind a VPN: set `HERMES_DASHBOARD_BASIC_AUTH_USERNAME` + `HERMES_DASHBOARD_BASIC_AUTH_PASSWORD` (and `HERMES_DASHBOARD_BASIC_AUTH_SECRET` for restart-stable sessions). Not suitable for direct public-internet exposure.
- **OAuth (Nous Portal)** — for hosted/public deploys: the `dashboard_auth/nous` provider activates whenever `HERMES_DASHBOARD_OAUTH_CLIENT_ID` is set.
- **Self-hosted OIDC** — to authenticate against your own identity provider via standard OpenID Connect: the `dashboard_auth/self_hosted` provider activates when `HERMES_DASHBOARD_OIDC_ISSUER` + `HERMES_DASHBOARD_OIDC_CLIENT_ID` are set.
Whichever you choose, the gate redirects callers to a login page before they can reach any protected route. See [Web Dashboard → Authentication](features/web-dashboard.md#authentication-gated-mode) for all three providers.
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
If no provider is registered and the bind is non-loopback, the dashboard **fails closed at startup** with a specific error pointing at the missing env var. There is no longer an escape hatch that serves the dashboard unauthenticated on a public bind: `HERMES_DASHBOARD_INSECURE=1` is now a deprecated no-op (it logs a warning and is ignored). Configure a provider, or bind `HERMES_DASHBOARD_HOST=127.0.0.1` and reach the dashboard over an SSH tunnel / Tailscale instead.
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
:::warning Why `--insecure` was removed
An unauthenticated public dashboard was the entry point for the June 2026 MCP-config persistence campaign: internet scanners reached exposed dashboards (and OpenAI API servers) and drove the agent into planting an SSH-key backdoor. The auth gate is now mandatory on every non-loopback bind. For a trusted-LAN / homelab box, the bundled username/password provider (`HERMES_DASHBOARD_BASIC_AUTH_USERNAME` + `_PASSWORD`) is the zero-infra way to satisfy it.
feat(docker): launch dashboard as side-process via HERMES_DASHBOARD=1 Adds an optional dashboard side-process to the container entrypoint, toggled by `HERMES_DASHBOARD=1` (also accepts `true` / `yes`). When set, the entrypoint backgrounds `hermes dashboard` before `exec`-ing the main command so the user's chosen foreground process (gateway, chat, `sleep infinity`, …) remains PID-of-interest for the container runtime. docker run -d \ -v ~/.hermes:/opt/data \ -p 8642:8642 -p 9119:9119 \ -e HERMES_DASHBOARD=1 \ nousresearch/hermes-agent gateway run Defaults chosen for the container case: - Host: 0.0.0.0 (reachable through published port; can override to 127.0.0.1 via HERMES_DASHBOARD_HOST for sidecar/reverse-proxy setups) - Port: 9119 (matches `hermes dashboard`) - Auto-adds `--insecure` when binding to non-localhost, matching the dashboard's own safety gate for exposing API keys - HERMES_DASHBOARD_TUI is read by `hermes dashboard` directly — no entrypoint plumbing needed Dashboard output is prefixed with `[dashboard]` via `stdbuf`+`sed -u` so it's easy to separate from gateway logs in `docker logs`. No supervision: if the dashboard crashes it stays down until the container restarts (documented in the `:::note` panel). Other changes bundled in: - Deprecate GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT env vars in hermes_cli/web_server.py with a DEPRECATED block comment and a `.. deprecated::` note on _probe_gateway_health. The feature still works for this release; it'll be removed alongside the move to a first-class dashboard config key. - Rewrite the "Running the dashboard" doc section around the new single-container pattern. Drops the previously-documented dashboard-as-its-own-container setup — that pattern relied on the deprecated env vars for cross-container gateway-liveness detection, and without them the dashboard would permanently report the gateway as "not running". - Collapse the two-service Compose example (gateway + dashboard container) into a single service with HERMES_DASHBOARD=1. Removes the now-unnecessary bridge network and `depends_on`. - Drop the ":::warning" caveat about "Running a dashboard container alongside the gateway is safe" — that case no longer exists.
2026-05-04 15:37:27 +10:00
:::
docs: deep audit — registry drift, stale claims, 2-week PR coverage, dashboard screenshot (#40952) Full-corpus correctness audit of the hand-written docs against the codebase, plus a 2-week merged-PR coverage sweep and one live dashboard screenshot. Correctness (verified against COMMAND_REGISTRY / PROVIDER_REGISTRY / TOOLSETS / tools.registry / DEFAULT_CONFIG / source): - reference: add /version slash command, context_engine toolset, openai-api + novita-ai to --provider; fix tool count 64->71; model_catalog ttl 24->1; add profile describe to summary table; add real provider env vars (LM_API_KEY/LM_BASE_URL, KIMI_CODING_API_KEY, ALIBABA_CODING_PLAN_*, ANTHROPIC_BASE_URL, COPILOT_API_BASE_URL); fix faq "Windows: not natively". - user-guide: fix broken `hermes -w -q` (->-z) and `hermes logs --tail` (->-f); language list 8->16; aux slots 8->11; docker separate-dashboard claim; _SECURITY_ARGS -> _BASE_SECURITY_ARGS. - features: curator prune_builtins truth + missing CLI verbs; codex-runtime aux keys (context_compression->compression, vision_detect->vision); kanban terminate endpoint + promote/reassign/schedule/diagnostics/edit + per-profile cap; mcp mTLS (client_cert/client_key); built-in-plugins nemo_relay + teams_pipeline; api-server run approval endpoint; computer-use frontmatter. - features N-Z + integrations: StepFun step-3-mini->step-3.5-flash; web-search backends 4->8; tool-gateway image-model IDs; voice-mode STT/TTS enums; remove phantom `rl` toolset; nous-portal status subcommand. - messaging: WeCom typing/streaming cols; telegram transport default edit->auto; sms host default; simplex/ntfy `gateway setup` + pairing approve; line smart-chunking; matrix MATRIX_DM_AUTO_THREAD. - developer-guide: build-a-plugin code examples (register_command signature, ContextEngine/ImageGenProvider/MemoryProvider ABCs); model-provider-plugin entry-point group hermes.plugins->hermes_agent.plugins; PLUGIN.yaml->plugin.yaml; agent-loop stale LOC; web-search-provider phantom crawl(). PR coverage (2-week window, 149 feat PRs): - desktop.md refreshed for ~15 shipped features (zh-Hans switcher, rebindable shortcuts + zoom + Cmd+K, status-bar model picker + YOLO toggle, session-by-id + archive, multi-profile concurrent + cross-profile @session, composer history, Providers pane, per-profile remote hosts, Grok OAuth, aux-pin warning). - configuration.md gateway-streaming default corrected to per-platform. - tool-gateway.md free tool pool entitlement note. Media: - New /img/dashboard/admin-config.png — live dashboard Config admin page (captured from a clean profile, no secrets/personalization).
2026-06-07 01:39:06 -07:00
Running the dashboard as a separate container **is** supported when that container shares the host PID and network namespace (e.g. `network_mode: host`, as the repo's own `docker-compose.yml` does — see its `dashboard` service). Its gateway-liveness detection requires a shared PID namespace with the gateway process, so the limitation only applies to dashboards run in isolated bridge-network containers without a shared PID namespace.
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
## Running interactively (CLI chat)
To open an interactive chat session against a running data directory:
```sh
docker run -it --rm \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent
```
Or if you have already opened a terminal in your running container (via Docker Desktop for instance), just run:
```sh
/opt/hermes/.venv/bin/hermes
```
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
## Persistent volumes
The `/opt/data` volume is the single source of truth for all Hermes state. It maps to your host's `~/.hermes/` directory and contains:
| Path | Contents |
|------|----------|
| `.env` | API keys and secrets |
| `config.yaml` | All Hermes configuration |
| `SOUL.md` | Agent personality/identity |
| `sessions/` | Conversation history |
| `memories/` | Persistent memory store |
| `skills/` | Installed skills |
2026-05-24 23:50:31 +08:00
| `home/` | Per-profile HOME for Hermes tool subprocesses (`git`, `ssh`, `gh`, `npm`, and skill CLIs) |
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
| `cron/` | Scheduled job definitions |
| `hooks/` | Event hooks |
| `logs/` | Runtime logs |
| `skins/` | Custom CLI skins |
### Immutable install tree
In hosted and published Docker images, `/opt/hermes` is the installed application tree. It is root-owned and read-only to the runtime `hermes` user, so agent turns, gateway sessions, dashboard actions, and normal `docker exec hermes hermes ...` commands cannot edit the core source, bundled `.venv`, `node_modules`, or TUI bundle in place.
All mutable Hermes state belongs under `/opt/data`: config, `.env`, profiles, skills, memories, sessions, logs, dashboard uploads, plugins, and other user-managed files. The image also disables runtime `.pyc` writes and Hermes lazy dependency installs into `/opt/hermes`; optional platform dependencies needed by the published image should be baked into the image or installed through a new image build.
On hosted/published images, agent self-improvement is scoped to skills, memory, plugins, and config under `/opt/data`. The installed core source under `/opt/hermes` is immutable; core changes are made via PRs to the repo and shipped by updating the image, not by live-editing the running install.
If an operator needs to repair or inspect files outside `/opt/data`, use a root shell intentionally. The `hermes` shim normally drops `docker exec hermes hermes ...` back to the runtime user; set `HERMES_DOCKER_EXEC_AS_ROOT=1` for a one-off root invocation when you explicitly need root semantics.
2026-05-24 23:50:31 +08:00
Skill CLIs that store credentials under `~` must be initialized against the subprocess HOME, not just the data-volume root. For example, the [xurl skill](./skills/bundled/social-media/social-media-xurl.md) stores OAuth state in `~/.xurl`; in the official Docker layout, Hermes tool calls read that as `/opt/data/home/.xurl`, so run manual xurl auth with `HOME=/opt/data/home` and verify with `HOME=/opt/data/home xurl auth status`.
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
:::warning
feat(docker): launch dashboard as side-process via HERMES_DASHBOARD=1 Adds an optional dashboard side-process to the container entrypoint, toggled by `HERMES_DASHBOARD=1` (also accepts `true` / `yes`). When set, the entrypoint backgrounds `hermes dashboard` before `exec`-ing the main command so the user's chosen foreground process (gateway, chat, `sleep infinity`, …) remains PID-of-interest for the container runtime. docker run -d \ -v ~/.hermes:/opt/data \ -p 8642:8642 -p 9119:9119 \ -e HERMES_DASHBOARD=1 \ nousresearch/hermes-agent gateway run Defaults chosen for the container case: - Host: 0.0.0.0 (reachable through published port; can override to 127.0.0.1 via HERMES_DASHBOARD_HOST for sidecar/reverse-proxy setups) - Port: 9119 (matches `hermes dashboard`) - Auto-adds `--insecure` when binding to non-localhost, matching the dashboard's own safety gate for exposing API keys - HERMES_DASHBOARD_TUI is read by `hermes dashboard` directly — no entrypoint plumbing needed Dashboard output is prefixed with `[dashboard]` via `stdbuf`+`sed -u` so it's easy to separate from gateway logs in `docker logs`. No supervision: if the dashboard crashes it stays down until the container restarts (documented in the `:::note` panel). Other changes bundled in: - Deprecate GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT env vars in hermes_cli/web_server.py with a DEPRECATED block comment and a `.. deprecated::` note on _probe_gateway_health. The feature still works for this release; it'll be removed alongside the move to a first-class dashboard config key. - Rewrite the "Running the dashboard" doc section around the new single-container pattern. Drops the previously-documented dashboard-as-its-own-container setup — that pattern relied on the deprecated env vars for cross-container gateway-liveness detection, and without them the dashboard would permanently report the gateway as "not running". - Collapse the two-service Compose example (gateway + dashboard container) into a single service with HERMES_DASHBOARD=1. Removes the now-unnecessary bridge network and `depends_on`. - Drop the ":::warning" caveat about "Running a dashboard container alongside the gateway is safe" — that case no longer exists.
2026-05-04 15:37:27 +10:00
Never run two Hermes **gateway** containers against the same data directory simultaneously — session files and memory stores are not designed for concurrent write access.
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
:::
## Multi-profile support
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
Hermes supports [multiple profiles](../reference/profile-commands.md) — separate `~/.hermes/` subdirectories that let you run independent agents (different SOUL, skills, memory, sessions, credentials) from a single installation. **Inside the official Docker image, the s6 supervision tree treats each profile as a first-class supervised service**, so the recommended deployment is **one container hosting all profiles**.
Each profile created with `hermes profile create <name>` gets:
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
- A dedicated s6 service slot at `/run/service/gateway-<name>/`, registered dynamically by the runtime — no container rebuild required.
- Auto-restart on crash, backoff-managed by `s6-supervise`.
- Per-profile rotated logs at `${HERMES_HOME}/logs/gateways/<name>/current` (10 archives × 1 MB each).
fix(gateway): auto-start after container restart via planned-stop marker (#42675) (#43236) * fix(gateway): auto-start after container restart via planned-stop marker On Docker (s6-overlay), the gateway runs as a dynamically-registered s6 service. When the container stops/restarts/upgrades, s6 sends the gateway a plain SIGTERM. The shutdown path (_stop_impl) ended with an unconditional _update_runtime_status("stopped"), persisting gateway_state=stopped to the volume. container_boot.py reads that on the next boot and only auto-starts gateways whose last state was "running" (_AUTOSTART_STATES) — so after a routine `docker compose up --force-recreate` the gateway stays down and messaging channels silently go dark, with no error surfaced (issue #42675). The codebase already distinguishes intentional stops from unexpected signals via the planned-stop marker (write_planned_stop_marker / consume_planned_stop_marker_for_self): `hermes gateway stop`, systemd/launchd ExecStop, and Ctrl+C write a marker before signalling, so the handler classifies them as planned. An unmarked SIGTERM (container/s6 restart, OOM, bare kill) is signal-initiated. This wires that existing classification through to the state persist, rather than adding unreliable signal-source inference: - run.py: GatewayRunner._signal_initiated_shutdown, set in shutdown_signal_handler's unmarked-signal branch. In _stop_impl, a signal-initiated (non-restart) teardown now persists "running" instead of "stopped" — preserving the operator's run-intent and overwriting the mid-shutdown "draining" marker so _AUTOSTART_STATES matches on reboot. Operator stops and restarts persist "stopped" as before. - service_manager.py: S6ServiceManager.stop() now writes the planned-stop marker for the supervised PID (read from s6-svstat) before `s6-svc -d`, so an in-container `hermes gateway stop` is correctly classified as intentional (parity with the systemd/launchd/host stop paths, which already mark). Best-effort: a marker-write failure falls back to the safe signal-initiated path. Tests: shutdown persist-decision table (signal→running, operator→stopped, restart→stopped), s6 stop marker write + svstat PID parse + failure tolerance. The signal→running and s6-marker tests fail without the respective source change. Verified end-to-end against a container built from this branch: an unmarked SIGTERM to the live gateway leaves gateway_state=running (shutdown-context log confirms signal path); existing real container-restart suite still green. * docs(docker): clarify gateway autostart distinguishes operator-stop from container-kill The per-profile-supervision section described the autostart-across-restart contract as "running gateways come back, stopped stay stopped" without spelling out what records 'stopped'. That contract was the source of #42675 confusion: users expected a restart to bring the gateway back and it didn't. With the write-side fix, only an explicit `hermes gateway stop` records 'stopped'; container/s6 restart SIGTERMs (incl. image upgrades and unexpected exits) leave the state 'running' so the gateway auto-starts. Make that distinction explicit in both the multi-profile and per-profile-supervision sections. * test(docker): real-restart autostart E2E for #42675 Adds test_live_gateway_autostarts_after_real_restart_without_manual_state_stamp: a live s6-supervised gateway is killed by an actual `docker restart` SIGTERM (no manual gateway_state stamp, no planned-stop marker) and must auto-start on the next boot. Exercises the WRITE side of the fix that the existing stamp-based tests bypass. Verified to FAIL against an origin/main image (reconciler logs prior_state=stopped action=registered — the #42675 bug) and PASS against the fixed image (prior_state=running action=started).
2026-06-10 14:01:34 +10:00
- State persistence across container restarts: the boot-time reconciler reads `gateway_state.json` from each profile directory and brings the slot back up only for profiles whose last recorded state was `running`. Only a gateway you explicitly stopped (`hermes gateway stop`) stays down across a restart — a container restart, image upgrade, or unexpected exit leaves the recorded state as `running`, so the gateway auto-starts on the next boot.
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
The lifecycle commands you'd run on the host work the same way from inside the container:
```sh
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
# Create a profile — registers the gateway-<name> s6 slot.
docker exec hermes hermes profile create coder
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
# Start / stop / restart — dispatches s6-svc; the gateway lifecycle survives docker restart.
docker exec hermes hermes -p coder gateway start
docker exec hermes hermes -p coder gateway stop
docker exec hermes hermes -p coder gateway restart
# Status — reports `Manager: s6 (container supervisor)` inside the container.
docker exec hermes hermes -p coder gateway status
# Remove a profile — tears down the s6 slot too.
docker exec hermes hermes profile delete coder
```
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
Under the hood, `hermes gateway start/stop/restart` inside the container is intercepted and routed to `s6-svc` against the right service directory; you don't need to learn the s6 commands directly. For raw supervisor state, use `/command/s6-svstat /run/service/gateway-<name>` (note `/command/` is on PATH only for processes spawned by the supervision tree — when calling from `docker exec`, pass the absolute path).
### Reaching more than one profile from outside the container
Two different surfaces reach a profile's gateway from outside, and they behave differently — don't conflate them:
**Hermes Desktop (and the web dashboard).** The Desktop app's **Remote Gateway** connection talks to a `hermes dashboard` backend (default **port 9119**, enabled by `HERMES_DASHBOARD=1`) — *not* the OpenAI API server. One dashboard backend serves **every** co-located profile: the app's profile switcher sends the target profile with each request and the backend opens that profile's `HERMES_HOME` on disk. So you do **not** need a second port — or a second connection — per profile for Desktop; one `:9119` connection covers them all through the switcher.
**OpenAI-compatible API clients (Open WebUI, LobeChat, `/v1/...`).** These talk to each profile's **API server**, which binds **port 8642 for every profile** (resolved from `API_SERVER_PORT` / `platforms.api_server.extra.port` — there is no auto-allocation and no `config.yaml`/`gateway.port` key). If you want a client to reach a *specific* second profile, give that profile a distinct `API_SERVER_PORT` in **its own** `.env`, otherwise its gateway tries to bind 8642 too and conflicts with the default profile:
```sh
# Create the profile (registers its gateway-<name> s6 slot)
docker exec hermes hermes profile create work
# Point its API server at a free port (write to the profile's own .env)
cat >> /opt/data/profiles/work/.env <<'EOF'
API_SERVER_ENABLED=true
API_SERVER_PORT=8643
EOF
docker exec hermes hermes -p work gateway restart
```
Keep `API_SERVER_PORT` in each profile's **own** `.env`, never in the container-wide `environment:` block — a global value would force every profile onto the same port and they would collide. With bridge networking, publish the extra port in `docker-compose.yml` (`- "8643:8643"`); with `network_mode: host` it is already reachable on the host. The default profile's 8642 connection is untouched.
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
### Why one container with many profiles, not many containers
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
Before the s6 migration, "one container per profile" was the recommended pattern because there was no in-container supervisor to manage multiple gateways. With s6 as PID 1, that's no longer necessary, and the single-container layout is simpler in almost every dimension:
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
| | One container, many profiles | One container per profile |
|---|---|---|
| Disk overhead | One image, one bundled venv, one Playwright cache | N images / N caches |
| Memory overhead | Shared Python interpreter cache, shared node_modules | Duplicated per container |
| Profile creation | `docker exec ... hermes profile create <name>` (seconds) | New `docker run` invocation + port allocation + bind-mount config |
| Per-profile crash recovery | `s6-supervise` auto-restart | Docker's `--restart unless-stopped` (slower, kills sibling work) |
| Logs | Per-profile rotated file via `s6-log`, plus container-boot audit log | `docker logs <name>` per container — no built-in rotation |
| Backup | One `~/.hermes` directory | N directories to coordinate |
The default profile (`default`) is always registered on first boot, so a fresh container ships with one supervised gateway out of the box. Additional profiles are pure runtime adds.
### When you DO want a separate container
Profile-in-container is the default. Run a separate container per profile only when you have a specific reason:
- **Resource isolation per workload** — e.g. a runaway browser-tool session in profile A shouldn't be able to OOM profile B. Containers give you `--memory` / `--cpus` per profile.
- **Independent image pinning** — different upstream image tags per workload.
- **Network segmentation** — distinct Docker networks per profile (e.g. one customer-facing, one internal).
- **Compliance / blast radius** — distinct credentials never share an OS-level process tree.
In those cases, declare one service per profile with distinct `container_name`, `volumes`, and `ports`:
```yaml
services:
hermes-work:
image: nousresearch/hermes-agent:latest
container_name: hermes-work
restart: unless-stopped
command: gateway run
ports:
- "8642:8642"
volumes:
- ~/.hermes-work:/opt/data
hermes-personal:
image: nousresearch/hermes-agent:latest
container_name: hermes-personal
restart: unless-stopped
command: gateway run
ports:
- "8643:8642"
volumes:
- ~/.hermes-personal:/opt/data
```
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
The warning from [Persistent volumes](#persistent-volumes) still applies: never point two containers at the same `~/.hermes` directory simultaneously. The s6 supervisor inside each container manages its own profile set; cross-container sharing of a data volume corrupts session files and memory stores.
## Where the logs go
The s6 container has four distinct log surfaces, and "why isn't my gateway showing anything in `docker logs`" is a common surprise. Cheatsheet:
| Source | Where it lands | How to read it |
|---|---|---|
| **Per-profile gateway** (`hermes gateway run` and per-profile gateways under s6) | Tee'd to two places: `docker logs <container>` (real time, no extra prefix) **and** `${HERMES_HOME}/logs/gateways/<profile>/current` (rotated, ISO-8601 timestamped, 10 archives × 1 MB each) | `docker logs -f hermes` or `tail -F ~/.hermes/logs/gateways/default/current` on the host |
| **Dashboard** (when `HERMES_DASHBOARD=1`) | `docker logs <container>` (no prefix) | `docker logs -f hermes` — interleaved with gateway lines |
| **Boot reconciler** (records which profile gateways were restored on each container start) | `${HERMES_HOME}/logs/container-boot.log` (append-only audit log) | `tail -F ~/.hermes/logs/container-boot.log` |
| **Generic Hermes logs** (`agent.log`, `errors.log`) | `${HERMES_HOME}/logs/` (profile-aware) | `docker exec hermes hermes logs --follow [--level WARNING] [--session <id>]` |
Two practical consequences worth knowing:
- The file copy at `logs/gateways/<profile>/current` is what survives container restarts. `docker logs` only retains output from the current container's lifetime (and is wiped on `docker rm`); the rotated files persist on the bind-mounted volume.
- The boot reconciler's audit line shape is `<iso-timestamp> profile=<name> prior_state=<state> action=<registered|started>`, so a quick `grep profile=coder ~/.hermes/logs/container-boot.log` reveals when a given profile was last restored and whether s6 auto-started it.
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
## Environment variable forwarding
API keys are read from `/opt/data/.env` inside the container. You can also pass environment variables directly:
```sh
docker run -it --rm \
-v ~/.hermes:/opt/data \
-e ANTHROPIC_API_KEY="sk-ant-..." \
-e OPENAI_API_KEY="sk-..." \
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
nousresearch/hermes-agent
```
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.
docs: comprehensive 2-week sweep of feature/PR coverage gaps (#28497) Catch the website docs up to two weeks of merged work (May 4 – May 18, 2026, roughly 1,080 PRs). The audit found ~50 user-visible features that had landed in code with no docs footprint, plus a handful of stale pages. This PR closes every gap the scan turned up. New pages - user-guide/features/deliverable-mode.md — extension list, agent triggers, kanban_complete artifacts pattern, [[as_document]] override (PR #27813). - developer-guide/web-search-provider-plugin.md — authoring guide modeled on image-gen-provider-plugin, covering brave_free / ddgs / etc. (PR #25448). Providers / auth - Rename "Alibaba Cloud" → "Qwen Cloud (Alibaba DashScope)" everywhere the display label shows up; provider id stays `alibaba` (PR #24835). - Document OAuth refresh-token quarantine for xAI / MiniMax / Codex (PRs #28116 / #28118 / #28119). - Document Nous JWT minting from refresh token + invalid-refresh quarantine + cross-profile shared token store (PRs #27663 / #19712). - Add `## Microsoft Entra ID authentication (keyless)` section to azure-foundry guide — DefaultAzureCredential, RBAC, OpenAI + Anthropic routing details (PR #28101 / #9df9816da). - Custom providers `api_mode` is now prompted-and-persisted, not just URL autodetected (PR #25068). - Delegation honours `api_mode` + auto-detects anthropic_messages base URLs (PR #26824). - `x_search` auto-enables when xAI credentials are present (PR #27376). - Add `xAI Grok OAuth (SuperGrok)` row to providers headline table (PR #26534). - NVIDIA NIM billing-origin header is set automatically (PR #26585). Windows / installer - `install.ps1`: document `-Commit <sha>` and `-Tag <v>` pin params plus the BOM-strip / git-retry hardening (PR #28169). - Document Hermes Desktop thin installer + first-launch bootstrap (PR #27822). - Document `dep_ensure` Windows bootstrap (PR #27845). - Document install-method auto-detection (pip / git / homebrew / nixos) and the matching update command (PR #27843). Gateway / messaging - `/platform list|pause|resume` full description + circuit-breaker semantics (PR #26600). - Slack / Matrix / Mattermost get parallel `allowed_channels` / `allowed_rooms` allowlist sections matching Telegram/Discord/DingTalk (PR #21251). - Discord `allow_any_attachment` + `max_attachment_bytes` (config and env vars) (PR #27245). - Discord clarify-choice button rendering (PR #25485). - Telegram `guest_mode` @mention bypass for allowlisted groups (PR #22759). - Telegram `notifications` mode (`important` vs `all`) (PR #22793). - `[[as_document]]` skill / response directive for forcing document-style media delivery (PR #21210). CLI / TUI - `/new [name]` argument (PR #19637). - `/subgoal` user-supplied criteria appended to `/goal` (PR #25449). - `/exit --delete` flag confirmation prompts for destructive slash commands (PR #22687). - Status-bar additions: ▶ N background indicator (PR #27175), context compression count (PR #21218), YOLO mode banner+statusbar warning (PR #26238). - `display.timestamps` + `docker_extra_args` config keys (PR #23599). - TUI collapsible startup banner sections (PR #20625). - `HERMES_SESSION_ID` exported to tool subprocesses (PR #23847). i18n - Refresh display.language locale list from 8 → 16 (en, zh, zh-hant, ja, de, es, fr, tr, uk, af, ko, it, ga, pt, ru, hu) — matches `agent/i18n.py:SUPPORTED_LANGUAGES`. Tools / features - `vision_analyze` native-pixel passthrough for vision-capable callers, with auxiliary text-describer fallback (PR #22955). - `session_search` rewrite to the single-shape tool (discovery / scroll / browse modes) (PRs #27590 / #27840). - Clarify MCP transport scope: client supports stdio + SSE; embedded `hermes mcp serve` is stdio-only (PR #21227). - Web search backends table: add Brave Search (free tier) and DDGS rows (PR #21337). - ACP session-scoped edit auto-approval modes (PR #27862). - Curator rename map in the user-visible per-run summary (PR #22910). - Prompt caching feature page reference in features/overview.md — Claude cross-session 1-hour prefix cache on native Anthropic / OpenRouter / Nous Portal (PR #23828). - Cron per-job profile parameter (PR #28124). - `--no-skills` flag for `hermes profile create` (PR #20986). Build - Verified with `npm run build` in `website/`; both `en` and `zh-Hans` locales compile. Remaining broken-link/anchor warnings are pre-existing (`rl-training.md` from learning-path / overview; the zh-Hans translation lag the docs skill already calls out).
2026-05-18 23:55:25 -07:00
:::note Looking for Docker as the **terminal backend**?
docs(docker): document new persist-across-processes contract and orphan reaper (#20561) Updates the Docker Backend section of the user-guide configuration page to match the actual behavior shipped in PR #33645. Pre-PR the docs claimed "container is stopped and removed on shutdown," which was never quite true for the documented happy path and is now actively wrong: in default mode the container survives across Hermes processes so background processes (npm watchers, dev servers, long-running pytest) carry over the way the "ONE long-lived container shared across sessions" promise requires. Changes to `website/docs/user-guide/configuration.md`: * Reworked the intro paragraph at the top of the Docker Backend section to describe the actual cross-process reuse contract. * Expanded the YAML example with the new keys `docker_persist_across_processes` and `docker_orphan_reaper`, plus the pre-existing-but-undocumented `docker_env`, `timeout`, and `lifetime_seconds`. Clarified the `container_persistent` comment to disambiguate from `docker_persist_across_processes`. * Added a `docker_env` vs `docker_forward_env` explainer (one injects literal KEY=value, the other forwards values from the host/.env — easy to confuse). * Replaced the one-line "Container lifecycle" paragraph with a full subsection covering: - the three labels Hermes tags every container with (hermes-agent, hermes-task-id, hermes-profile) - the label-probe reuse mechanism on startup - a teardown-trigger table with four rows for every situation that destroys the container in default mode - edge cases (OOM kill, profile switching) * Added an "Environment variable overrides" table covering all TERMINAL_* env vars relevant to the Docker backend, including the previously-undocumented `TERMINAL_DOCKER_ENV` and `HERMES_DOCKER_BINARY`. Changes to `website/docs/user-guide/docker.md`: * Extended the cross-link admonition (around l.227) so the Hermes-in-Docker page points at the new terminal-backend keys (`docker_env`, `docker_persist_across_processes`, `docker_orphan_reaper`) alongside the ones already mentioned. No code changes. Behavior already covered by tests added in earlier commits on this branch (#33645 commits 1-5). Refs #20561
2026-05-29 10:48:32 +10:00
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 long-lived container shared across Hermes processes — see issue #20561), that's a separate config block — `terminal.backend: docker` plus `terminal.docker_image`, `terminal.docker_volumes`, `terminal.docker_forward_env`, `terminal.docker_env`, `terminal.docker_run_as_host_user`, `terminal.docker_extra_args`, `terminal.docker_persist_across_processes`, and `terminal.docker_orphan_reaper`. See [Configuration → Docker Backend](configuration.md#docker-backend) for the full set including container-lifecycle rules.
docs: comprehensive 2-week sweep of feature/PR coverage gaps (#28497) Catch the website docs up to two weeks of merged work (May 4 – May 18, 2026, roughly 1,080 PRs). The audit found ~50 user-visible features that had landed in code with no docs footprint, plus a handful of stale pages. This PR closes every gap the scan turned up. New pages - user-guide/features/deliverable-mode.md — extension list, agent triggers, kanban_complete artifacts pattern, [[as_document]] override (PR #27813). - developer-guide/web-search-provider-plugin.md — authoring guide modeled on image-gen-provider-plugin, covering brave_free / ddgs / etc. (PR #25448). Providers / auth - Rename "Alibaba Cloud" → "Qwen Cloud (Alibaba DashScope)" everywhere the display label shows up; provider id stays `alibaba` (PR #24835). - Document OAuth refresh-token quarantine for xAI / MiniMax / Codex (PRs #28116 / #28118 / #28119). - Document Nous JWT minting from refresh token + invalid-refresh quarantine + cross-profile shared token store (PRs #27663 / #19712). - Add `## Microsoft Entra ID authentication (keyless)` section to azure-foundry guide — DefaultAzureCredential, RBAC, OpenAI + Anthropic routing details (PR #28101 / #9df9816da). - Custom providers `api_mode` is now prompted-and-persisted, not just URL autodetected (PR #25068). - Delegation honours `api_mode` + auto-detects anthropic_messages base URLs (PR #26824). - `x_search` auto-enables when xAI credentials are present (PR #27376). - Add `xAI Grok OAuth (SuperGrok)` row to providers headline table (PR #26534). - NVIDIA NIM billing-origin header is set automatically (PR #26585). Windows / installer - `install.ps1`: document `-Commit <sha>` and `-Tag <v>` pin params plus the BOM-strip / git-retry hardening (PR #28169). - Document Hermes Desktop thin installer + first-launch bootstrap (PR #27822). - Document `dep_ensure` Windows bootstrap (PR #27845). - Document install-method auto-detection (pip / git / homebrew / nixos) and the matching update command (PR #27843). Gateway / messaging - `/platform list|pause|resume` full description + circuit-breaker semantics (PR #26600). - Slack / Matrix / Mattermost get parallel `allowed_channels` / `allowed_rooms` allowlist sections matching Telegram/Discord/DingTalk (PR #21251). - Discord `allow_any_attachment` + `max_attachment_bytes` (config and env vars) (PR #27245). - Discord clarify-choice button rendering (PR #25485). - Telegram `guest_mode` @mention bypass for allowlisted groups (PR #22759). - Telegram `notifications` mode (`important` vs `all`) (PR #22793). - `[[as_document]]` skill / response directive for forcing document-style media delivery (PR #21210). CLI / TUI - `/new [name]` argument (PR #19637). - `/subgoal` user-supplied criteria appended to `/goal` (PR #25449). - `/exit --delete` flag confirmation prompts for destructive slash commands (PR #22687). - Status-bar additions: ▶ N background indicator (PR #27175), context compression count (PR #21218), YOLO mode banner+statusbar warning (PR #26238). - `display.timestamps` + `docker_extra_args` config keys (PR #23599). - TUI collapsible startup banner sections (PR #20625). - `HERMES_SESSION_ID` exported to tool subprocesses (PR #23847). i18n - Refresh display.language locale list from 8 → 16 (en, zh, zh-hant, ja, de, es, fr, tr, uk, af, ko, it, ga, pt, ru, hu) — matches `agent/i18n.py:SUPPORTED_LANGUAGES`. Tools / features - `vision_analyze` native-pixel passthrough for vision-capable callers, with auxiliary text-describer fallback (PR #22955). - `session_search` rewrite to the single-shape tool (discovery / scroll / browse modes) (PRs #27590 / #27840). - Clarify MCP transport scope: client supports stdio + SSE; embedded `hermes mcp serve` is stdio-only (PR #21227). - Web search backends table: add Brave Search (free tier) and DDGS rows (PR #21337). - ACP session-scoped edit auto-approval modes (PR #27862). - Curator rename map in the user-visible per-run summary (PR #22910). - Prompt caching feature page reference in features/overview.md — Claude cross-session 1-hour prefix cache on native Anthropic / OpenRouter / Nous Portal (PR #23828). - Cron per-job profile parameter (PR #28124). - `--no-skills` flag for `hermes profile create` (PR #20986). Build - Verified with `npm run build` in `website/`; both `en` and `zh-Hans` locales compile. Remaining broken-link/anchor warnings are pre-existing (`rl-training.md` from learning-path / overview; the zh-Hans translation lag the docs skill already calls out).
2026-05-18 23:55:25 -07:00
:::
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
## Docker Compose example
For persistent deployment with both the gateway and dashboard, a `docker-compose.yaml` is convenient:
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
```yaml
services:
hermes:
image: nousresearch/hermes-agent:latest
container_name: hermes
restart: unless-stopped
command: gateway run
ports:
feat(docker): launch dashboard as side-process via HERMES_DASHBOARD=1 Adds an optional dashboard side-process to the container entrypoint, toggled by `HERMES_DASHBOARD=1` (also accepts `true` / `yes`). When set, the entrypoint backgrounds `hermes dashboard` before `exec`-ing the main command so the user's chosen foreground process (gateway, chat, `sleep infinity`, …) remains PID-of-interest for the container runtime. docker run -d \ -v ~/.hermes:/opt/data \ -p 8642:8642 -p 9119:9119 \ -e HERMES_DASHBOARD=1 \ nousresearch/hermes-agent gateway run Defaults chosen for the container case: - Host: 0.0.0.0 (reachable through published port; can override to 127.0.0.1 via HERMES_DASHBOARD_HOST for sidecar/reverse-proxy setups) - Port: 9119 (matches `hermes dashboard`) - Auto-adds `--insecure` when binding to non-localhost, matching the dashboard's own safety gate for exposing API keys - HERMES_DASHBOARD_TUI is read by `hermes dashboard` directly — no entrypoint plumbing needed Dashboard output is prefixed with `[dashboard]` via `stdbuf`+`sed -u` so it's easy to separate from gateway logs in `docker logs`. No supervision: if the dashboard crashes it stays down until the container restarts (documented in the `:::note` panel). Other changes bundled in: - Deprecate GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT env vars in hermes_cli/web_server.py with a DEPRECATED block comment and a `.. deprecated::` note on _probe_gateway_health. The feature still works for this release; it'll be removed alongside the move to a first-class dashboard config key. - Rewrite the "Running the dashboard" doc section around the new single-container pattern. Drops the previously-documented dashboard-as-its-own-container setup — that pattern relied on the deprecated env vars for cross-container gateway-liveness detection, and without them the dashboard would permanently report the gateway as "not running". - Collapse the two-service Compose example (gateway + dashboard container) into a single service with HERMES_DASHBOARD=1. Removes the now-unnecessary bridge network and `depends_on`. - Drop the ":::warning" caveat about "Running a dashboard container alongside the gateway is safe" — that case no longer exists.
2026-05-04 15:37:27 +10:00
- "8642:8642" # gateway API
- "9119:9119" # dashboard (only reached when HERMES_DASHBOARD=1)
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
volumes:
- ~/.hermes:/opt/data
feat(docker): launch dashboard as side-process via HERMES_DASHBOARD=1 Adds an optional dashboard side-process to the container entrypoint, toggled by `HERMES_DASHBOARD=1` (also accepts `true` / `yes`). When set, the entrypoint backgrounds `hermes dashboard` before `exec`-ing the main command so the user's chosen foreground process (gateway, chat, `sleep infinity`, …) remains PID-of-interest for the container runtime. docker run -d \ -v ~/.hermes:/opt/data \ -p 8642:8642 -p 9119:9119 \ -e HERMES_DASHBOARD=1 \ nousresearch/hermes-agent gateway run Defaults chosen for the container case: - Host: 0.0.0.0 (reachable through published port; can override to 127.0.0.1 via HERMES_DASHBOARD_HOST for sidecar/reverse-proxy setups) - Port: 9119 (matches `hermes dashboard`) - Auto-adds `--insecure` when binding to non-localhost, matching the dashboard's own safety gate for exposing API keys - HERMES_DASHBOARD_TUI is read by `hermes dashboard` directly — no entrypoint plumbing needed Dashboard output is prefixed with `[dashboard]` via `stdbuf`+`sed -u` so it's easy to separate from gateway logs in `docker logs`. No supervision: if the dashboard crashes it stays down until the container restarts (documented in the `:::note` panel). Other changes bundled in: - Deprecate GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT env vars in hermes_cli/web_server.py with a DEPRECATED block comment and a `.. deprecated::` note on _probe_gateway_health. The feature still works for this release; it'll be removed alongside the move to a first-class dashboard config key. - Rewrite the "Running the dashboard" doc section around the new single-container pattern. Drops the previously-documented dashboard-as-its-own-container setup — that pattern relied on the deprecated env vars for cross-container gateway-liveness detection, and without them the dashboard would permanently report the gateway as "not running". - Collapse the two-service Compose example (gateway + dashboard container) into a single service with HERMES_DASHBOARD=1. Removes the now-unnecessary bridge network and `depends_on`. - Drop the ":::warning" caveat about "Running a dashboard container alongside the gateway is safe" — that case no longer exists.
2026-05-04 15:37:27 +10:00
environment:
- HERMES_DASHBOARD=1
# Uncomment to forward specific env vars instead of using .env file:
# - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
# - OPENAI_API_KEY=${OPENAI_API_KEY}
# - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
deploy:
resources:
limits:
memory: 4G
cpus: "2.0"
```
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
Start with `docker compose up -d` and view logs with `docker compose logs -f`. The supervised gateway's stdout is also tee'd to `${HERMES_HOME}/logs/gateways/<profile>/current` on the volume — see [Where the logs go](#where-the-logs-go) for the full routing map.
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
2026-05-25 22:45:12 +08:00
## Optional: Linux desktop audio bridge
Voice mode in Docker needs two separate things to work: Hermes must be allowed to probe audio devices inside the container, and the container must be able to reach your host audio server. The setup below covers the host audio plumbing for Linux desktops that expose a PulseAudio-compatible socket, including many PipeWire setups.
:::caution
This is a Linux desktop workaround, not a general Docker Desktop feature. It is useful when you already have host audio working and want CLI voice mode inside the Hermes container. If Hermes still reports `Running inside Docker container -- no audio devices`, use a build that includes Docker audio probing support for `PULSE_SERVER` / `PIPEWIRE_REMOTE`.
:::
First, create an ALSA config next to your Compose file:
```conf title="asound.conf"
pcm.!default {
type pulse
hint {
show on
description "Default ALSA Output (PulseAudio)"
}
}
pcm.pulse {
type pulse
}
ctl.!default {
type pulse
}
```
Then build a small derived image with the ALSA PulseAudio plugin installed:
```dockerfile title="Dockerfile.audio"
FROM nousresearch/hermes-agent:latest
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends libasound2-plugins \
&& rm -rf /var/lib/apt/lists/*
```
Use that image in Compose and pass through the host user's PulseAudio socket and cookie:
```yaml
services:
hermes:
build:
context: .
dockerfile: Dockerfile.audio
image: hermes-agent-audio
container_name: hermes
restart: unless-stopped
command: gateway run
volumes:
- ~/.hermes:/opt/data
- /run/user/${HERMES_UID}/pulse:/run/user/${HERMES_UID}/pulse
- ~/.config/pulse/cookie:/tmp/pulse-cookie:ro
- ./asound.conf:/etc/asound.conf:ro
environment:
- HERMES_UID=${HERMES_UID}
- HERMES_GID=${HERMES_GID}
- XDG_RUNTIME_DIR=/run/user/${HERMES_UID}
- PULSE_SERVER=unix:/run/user/${HERMES_UID}/pulse/native
- PULSE_COOKIE=/tmp/pulse-cookie
```
Start it with your host UID/GID so the container process can access the per-user audio socket:
```sh
export HERMES_UID="$(id -u)"
export HERMES_GID="$(id -g)"
docker compose up -d --build
```
To verify what PortAudio sees inside the container:
```sh
docker exec hermes /opt/hermes/.venv/bin/python -c "import sounddevice as sd; print(sd.query_devices())"
```
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
## Resource limits
The Hermes container needs moderate resources. Recommended minimums:
| Resource | Minimum | Recommended |
|----------|---------|-------------|
| Memory | 1 GB | 24 GB |
| CPU | 1 core | 2 cores |
| Disk (data volume) | 500 MB | 2+ GB (grows with sessions/skills) |
Browser automation (Playwright/Chromium) is the most memory-hungry feature. If you don't need browser tools, 1 GB is sufficient. With browser tools active, allocate at least 2 GB.
Set limits in Docker:
```sh
docker run -d \
--name hermes \
--restart unless-stopped \
--memory=4g --cpus=2 \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway run
```
## What the Dockerfile does
The official image is based on `debian:13.4` and includes:
- Python 3.13 with dependencies synced from the lockfile via `uv sync --frozen --no-install-project` for the baked extras (`all`, `messaging`, Anthropic/Bedrock/Azure identity, Hindsight, Matrix), followed by a no-dependency editable install of Hermes itself.
- Node.js 22 + npm (for browser automation, WhatsApp bridge, TUI/Desktop bundles, and workspace build tooling)
docs: resync reference, user-guide, developer-guide, and messaging pages against code (#17738) Broad drift audit against origin/main (b52b63396). Reference pages (most user-visible drift): - slash-commands: add /busy, /curator, /footer, /indicator, /redraw, /steer that were missing; drop non-existent /terminal-setup; fix /q footnote (resolves to /queue, not /quit); extend CLI-only list with all 24 CLI-only commands in the registry - cli-commands: add dedicated sections for hermes curator / fallback / hooks (new subcommands not previously documented); remove stale hermes honcho standalone section (the plugin registers dynamically via hermes memory); list curator/fallback/hooks in top-level table; fix completion to include fish - toolsets-reference: document the real 52-toolset count; split browser vs browser-cdp; add discord / discord_admin / spotify / yuanbao; correct hermes-cli tool count from 36 to 38; fix misleading claim that hermes-homeassistant adds tools (it's identical to hermes-cli) - tools-reference: bump tool count 55 -> 68; add 7 Spotify, 5 Yuanbao, 2 Discord toolsets; move browser_cdp/browser_dialog to their own browser-cdp toolset section - environment-variables: add 40+ user-facing HERMES_* vars that were undocumented (--yolo, --accept-hooks, --ignore-*, inference model override, agent/stream/checkpoint timeouts, OAuth trace, per-platform batch tuning for Telegram/Discord/Matrix/Feishu/WeCom, cron knobs, gateway restart/connect timeouts); dedupe the Cron Scheduler section; replace stale QQ_SANDBOX with QQ_PORTAL_HOST User-guide (top level): - cli.md: compression preserves last 20 turns, not 4 (protect_last_n: 20) - configuration.md: display.platforms is the canonical per-platform override key; tool_progress_overrides is deprecated and auto-migrated - profiles.md: model.default is the config key, not model.model - sessions.md: CLI/TUI session IDs use 6-char hex, gateway uses 8 - checkpoints-and-rollback.md: destructive-command list now matches _DESTRUCTIVE_PATTERNS (adds rmdir, cp, install, dd) - docker.md: the container runs as non-root hermes (UID 10000) via gosu; fix install command (uv pip); add missing --insecure on the dashboard compose example (required for non-loopback bind) - security.md: systemctl danger pattern also matches 'restart' - index.md: built-in tool count 47 -> 68 - integrations/index.md: 6 STT providers, 8 memory providers - integrations/providers.md: drop fictional dashscope/qwen aliases Features: - overview.md: 9 image models (not 8), 9 TTS providers (not 5), 8 memory providers (Supermemory was missing) - tool-gateway.md: 9 image models - tools.md: extend common-toolsets list with search / messaging / spotify / discord / debugging / safe - fallback-providers.md: add 6 real providers from PROVIDER_REGISTRY (lmstudio, kimi-coding-cn, stepfun, alibaba-coding-plan, tencent-tokenhub, azure-foundry) - plugins.md: Available Hooks table now includes on_session_finalize, on_session_reset, subagent_stop - built-in-plugins.md: add the 7 bundled plugins the page didn't mention (spotify, google_meet, three image_gen providers, two dashboard examples) - web-dashboard.md: add --insecure and --tui flags - cron.md: hermes cron create takes positional schedule/prompt, not flags Messaging: - telegram.md: TELEGRAM_WEBHOOK_SECRET is now REQUIRED when TELEGRAM_WEBHOOK_URL is set (gateway refuses to start without it per GHSA-3vpc-7q5r-276h). Biggest user-visible drift in the batch. - discord.md: HERMES_DISCORD_TEXT_BATCH_SPLIT_DELAY_SECONDS default is 2.0, not 0.1 - dingtalk.md: document DINGTALK_REQUIRE_MENTION / FREE_RESPONSE_CHATS / MENTION_PATTERNS / HOME_CHANNEL / ALLOW_ALL_USERS that the adapter supports - bluebubbles.md: drop fictional BLUEBUBBLES_SEND_READ_RECEIPTS env var; the setting lives in platforms.bluebubbles.extra only - qqbot.md: drop dead QQ_SANDBOX; add real QQ_PORTAL_HOST and QQ_GROUP_ALLOWED_USERS - wecom-callback.md: replace 'hermes gateway start' (service-only) with 'hermes gateway' for first-time setup Developer-guide: - architecture.md: refresh tool/toolset counts (61/52), terminal backend count (7), line counts for run_agent.py (~13.7k), cli.py (~11.5k), main.py (~10.4k), setup.py (~3.5k), gateway/run.py (~12.2k), mcp_tool.py (~3.1k); add yuanbao adapter, bump platform adapter count 18 -> 20 - agent-loop.md: run_agent.py line count 10.7k -> 13.7k - tools-runtime.md: add vercel_sandbox backend - adding-tools.md: remove stale 'Discovery import added to model_tools.py' checklist item (registry auto-discovery) - adding-platform-adapters.md: mark send_typing / get_chat_info as concrete base methods; only connect/disconnect/send are abstract - acp-internals.md: ACP sessions now persist to SessionDB (~/.hermes/state.db); acp.run_agent call uses use_unstable_protocol=True - cron-internals.md: gateway runs scheduler in a dedicated background thread via _start_cron_ticker, not on a maintenance cycle; locking is cross-process via fcntl.flock (Unix) / msvcrt.locking (Windows) - gateway-internals.md: gateway/run.py ~12k lines - provider-runtime.md: cron DOES support fallback (run_job reads fallback_providers from config) - session-storage.md: SCHEMA_VERSION = 11 (not 9); add migrations 10 and 11 (trigram FTS, inline-mode FTS5 re-index); add api_call_count column to Sessions DDL; document messages_fts_trigram and state_meta in the architecture tree - context-compression-and-caching.md: remove the obsolete 'context pressure warnings' section (warnings were removed for causing models to give up early) - context-engine-plugin.md: compress() signature now includes focus_topic param - extending-the-cli.md: _build_tui_layout_children signature now includes model_picker_widget; add to default layout Also fixed three pre-existing broken links/anchors the build warned about (docker.md -> api-server.md, yuanbao.md -> cron-jobs.md and tips#background-tasks, nix-setup.md -> #container-aware-cli). Regenerated per-skill pages via website/scripts/generate-skill-docs.py so catalog tables and sidebar are consistent with current SKILL.md frontmatter. docusaurus build: clean, no broken links or anchors.
2026-04-29 20:55:59 -07:00
- Playwright with Chromium (`npx playwright install --with-deps chromium --only-shell`)
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
- ripgrep, ffmpeg, git, and `xz-utils` as system utilities
docs: two-week gap sweep — platforms, CLI, config, TUI, hooks, providers (#17727) Covers ~60 merged PRs from Apr 15–29 that shipped user-visible behavior without docs coverage. No functional code changes; docs + static manifest regeneration only. Highlights: Stale / incorrect: - configuration.md: auxiliary auto-routing line was wrong since #11900; now correctly states auto routes to the main model, with a note on the cost trade-off and per-task override pattern. - integrations/providers.md + configuration.md compression intro: removed stale 'Gemini Flash via OpenRouter' claim. - website/static/api/model-catalog.json: rebuilt from hermes_cli/models.py so the live manifest picks up tencent/hy3-preview (and remains in sync for future model-catalog PRs). Platform messaging (#17417 #16997 #16193 #14315 #13151 #11794 #10610 #10283 #10246 #11564 #13178): - Signal: native formatting (bodyRanges), reply quotes, reactions. - Telegram: table rendering (bullets + code-block fallback), disable_link_previews, group_allowed_chats. - Slack: strict_mention config. - Discord: slash_commands disable, send_animation GIF, send_message native media attachments. - DingTalk: require_mention + allowed_users. CLI (#16052 #16539 #16566 #15841 #14798 #10043): - New 'hermes fallback' interactive manager. - New 'hermes update --check', '--backup' flag, and pre-update pairing snapshot behavior. - 'hermes gateway start/restart --all' multi-profile flag. - cron.md: 'hermes tools' as a platform, per-job enabled_toolsets, wakeAgent gate, context_from chaining. Config keys / env vars (#17305 #17026 #17000 #15077 #14557 #14227 #14166 #14730 #17008): - terminal.docker_run_as_host_user, display.runtime_metadata_footer, compression.hygiene_hard_message_limit, HINDSIGHT_TIMEOUT, skills.guard_agent_created, TAVILY_BASE_URL, security.allow_private_urls, agent.api_max_retries, gateway hot-reload of compression/context_length config edits. TUI / CLI UX (#17130 #17113 #17175 #17150 #16707 #12312 #12305 #12934 #14810 #14045 #17286 #17126): - HERMES_TUI_RESUME, HERMES_TUI_THEME, LaTeX rendering, busy-indicator styles, ctrl-x queued-message delete, git branch in status bar, per- prompt elapsed stopwatch, external-editor keybind, markdown stripping, TUI voice-mode parity, /agents overlay, /reload + /mouse. Gateway features (#16506 #15027 #13428 #12116): - Native multimodal image routing based on vision capability. - /usage account-limits section. - /steer slash command (added to reference + explanation in CLI). Plugins / hooks (#12929 #12972 #10763 #16364): - transform_tool_result, transform_terminal_output plugin hooks. - PluginContext.dispatch_tool() documented with slash-command example. - google_meet bundled plugin entry under built-in-plugins.md. Other (#16576 #16572 #16383 #15878 #15608 #15606 #14809 #14767 #14231 #14232 #14307 #13683 #12373 #11891 #11291 #10066): - hermes backup exclusions (WAL/SHM/journal + checkpoints/). - security.md hardline blocklist (floor below --yolo). - FHS install layout for root installs. - openssh-client + docker-cli baked into the Docker image. - MEDIA: tag supported extensions table (docs/office/archives/pdf). - Remote-to-host file sync on SSH/Modal/Daytona teardown. - 'hermes model' -> Configure Auxiliary Models interactive picker. - Podman support via HERMES_DOCKER_BINARY. Providers / STT / one-shot (#15045 #14473 #15704): - alibaba-coding-plan first-class provider entry. - xAI Grok STT as a 6th transcription option. - 'hermes -z' scripted one-shot mode + HERMES_INFERENCE_MODEL. Build: 'docusaurus build' succeeds. No new broken links/anchors; pre-existing warnings unchanged.
2026-04-29 20:32:37 -07:00
- **`docker-cli`** — so agents running inside the container can drive the host's Docker daemon (bind-mount `/var/run/docker.sock` to opt in) for `docker build`, `docker run`, container inspection, etc.
- **`openssh-client`** — enables the [SSH terminal backend](/user-guide/configuration#ssh-backend) from inside the container. The SSH backend shells out to the system `ssh` binary; without this, it failed silently in containerized installs.
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
- The WhatsApp bridge (`scripts/whatsapp-bridge/`)
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
- **[`s6-overlay`](https://github.com/just-containers/s6-overlay) v3** as PID 1 (replaces the older `tini`) — supervises the dashboard and per-profile gateways with auto-restart on crash, reaps zombie subprocesses, and forwards signals.
The image treats `/opt/hermes` as an immutable install tree at runtime. Optional Python extras, Node workspaces, and TUI assets that must be available inside Docker need to be baked during the image build; runtime lazy installs are disabled so supervised gateways and `docker exec hermes …` commands do not try to write dependency artifacts back into the read-only source tree.
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
The container's `ENTRYPOINT` is s6-overlay's `/init`. On boot it:
fix(docker): run config migrations during container boot (salvage #35508) (#36627) Salvage of #35508 (@dchenk), rebased onto current main. Resolved the tests/tools/test_stage2_hook_puid_pgid.py conflict (kept both the envdir-creation regression test on main and the new config-migration tests). Docker image upgrades replace code under $INSTALL_DIR but preserve $HERMES_HOME on the mounted volume, so the persisted config.yaml never received the schema migrations that non-Docker `hermes update` runs (#35406). This adds scripts/docker_config_migrate.py, invoked from stage2-hook after first-boot seeding and before gateway services start: it backs up config.yaml + .env, runs migrate_config(interactive=False), and honors HERMES_SKIP_CONFIG_MIGRATION=1 for manual control. Also fixes a latent bug in check_config_version(): it called load_config() which deep-merges DEFAULT_CONFIG, so a legacy config with no raw _config_version falsely reported as already-current. It now reads the raw on-disk file so legacy configs are correctly detected for migration. Differs from #35508 as submitted (Option B cleanup): dropped the `_config_version` line added to cli-config.yaml.example and removed the accompanying test_cli_config_example_declares_latest_version change-detector test. The example is a copy-template and has no business asserting a schema version; check_config_version() reads the user's real config.yaml, not the example. This removes a second sync point that drifts on every version bump. Closes #35508. Fixes #35406. Co-authored-by: Dmitriy Cherchenko <17372886+dchenk@users.noreply.github.com>
2026-06-04 11:11:27 +10:00
1. Runs `/etc/cont-init.d/01-hermes-setup` (= `docker/stage2-hook.sh`) as root: optional UID/GID remap, fixes volume ownership, seeds `.env` / `config.yaml` / `SOUL.md` on first boot, runs non-interactive config-schema migrations unless `HERMES_SKIP_CONFIG_MIGRATION=1`, syncs bundled skills.
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
2. Runs `/etc/cont-init.d/02-reconcile-profiles` (= `hermes_cli.container_boot`): walks `$HERMES_HOME/profiles/<name>/`, recreates the per-profile gateway s6 service slot under `/run/service/gateway-<profile>/`, and auto-starts only those whose last recorded state was `running` (see [Per-profile gateway supervision](#per-profile-gateway-supervision)).
3. Starts the static `main-hermes` and `dashboard` s6-rc services.
4. Exec's the container's CMD as the main program (`/opt/hermes/docker/main-wrapper.sh`), which routes the arguments the user passed to `docker run`:
- no args → `hermes` (the default)
- first arg is an executable on PATH (e.g. `sleep`, `bash`) → exec it directly
- anything else → `hermes <args>` (subcommand passthrough)
The container exits when this main program exits, with its exit code.
:::warning Breaking change vs. pre-s6 images
The container ENTRYPOINT is now `/init` (s6-overlay), not `/usr/bin/tini`. All five documented `docker run` invocation patterns (no args, `chat -q "…"`, `sleep infinity`, `bash`, `--tui`) behave identically to the tini-based image. If you have a downstream wrapper that depended on tini-specific signal behavior or hard-coded `/usr/bin/tini --` invocation, pin to the previous image tag.
:::
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
:::warning Privilege model
Do not override the image entrypoint unless you keep `/init` (or, equivalently, the legacy `docker/entrypoint.sh` shim that forwards to the stage2 hook) in the command chain. s6-overlay's `/init` runs as root so it can chown the volume on first boot, then drops to the `hermes` user via `s6-setuidgid` for every supervised service AND for the main program. Starting `hermes gateway run` as root inside the official image is refused by default because it can leave root-owned files in `/opt/data` and break later dashboard or gateway starts. Set `HERMES_ALLOW_ROOT_GATEWAY=1` only when you intentionally accept that risk.
:::
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
### `docker exec` automatically drops to the `hermes` user
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
`docker exec hermes <cmd>` defaults to running as root inside the container, but the image ships a thin shim at `/opt/hermes/bin/hermes` (earliest on PATH) that detects root callers and transparently re-execs through `s6-setuidgid hermes`. So `docker exec hermes login`, `docker exec hermes profile create …`, `docker exec hermes setup`, etc. all write files owned by UID 10000 — i.e. readable by the supervised gateway — with no extra `--user` flag needed. Non-root callers (the supervised processes themselves, `docker exec --user hermes`, kanban subagents inside the container) hit a short-circuit that exec's the venv binary directly, so there's no overhead on the hot paths.
If you specifically need a `docker exec` that retains root semantics (diagnostic sessions, inspecting root-only state, files outside `/opt/data` that root happens to own), opt out per invocation:
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
```sh
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
docker exec -e HERMES_DOCKER_EXEC_AS_ROOT=1 hermes <cmd>
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
```
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
The shim accepts `1` / `true` / `yes` (case-insensitive). Anything else — including typos like `=0` — falls through to the drop, so silent opt-outs aren't possible. If `s6-setuidgid` isn't available (custom builds that stripped s6-overlay), the shim refuses to run as root and exits 126 instead, surfacing the broken privilege model loudly rather than regressing to the historical footgun where `docker exec hermes login` would write `auth.json` as `root:root` and break the supervised gateway's auth on every chat platform message.
### Per-profile gateway supervision
Each profile created with `hermes profile create <name>` automatically gets an s6-supervised gateway service registered at `/run/service/gateway-<name>/`, with state-persistent auto-restart across container restarts. See [Multi-profile support](#multi-profile-support) above for the user-facing workflow and the lifecycle commands.
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
**Supervision benefits over the pre-s6 image:**
- Gateway crashes are auto-restarted by `s6-supervise` after a ~1s backoff.
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
- Dashboard, when enabled with `HERMES_DASHBOARD=1`, is supervised on the same supervision tree and gets the same auto-restart treatment.
fix(gateway): auto-start after container restart via planned-stop marker (#42675) (#43236) * fix(gateway): auto-start after container restart via planned-stop marker On Docker (s6-overlay), the gateway runs as a dynamically-registered s6 service. When the container stops/restarts/upgrades, s6 sends the gateway a plain SIGTERM. The shutdown path (_stop_impl) ended with an unconditional _update_runtime_status("stopped"), persisting gateway_state=stopped to the volume. container_boot.py reads that on the next boot and only auto-starts gateways whose last state was "running" (_AUTOSTART_STATES) — so after a routine `docker compose up --force-recreate` the gateway stays down and messaging channels silently go dark, with no error surfaced (issue #42675). The codebase already distinguishes intentional stops from unexpected signals via the planned-stop marker (write_planned_stop_marker / consume_planned_stop_marker_for_self): `hermes gateway stop`, systemd/launchd ExecStop, and Ctrl+C write a marker before signalling, so the handler classifies them as planned. An unmarked SIGTERM (container/s6 restart, OOM, bare kill) is signal-initiated. This wires that existing classification through to the state persist, rather than adding unreliable signal-source inference: - run.py: GatewayRunner._signal_initiated_shutdown, set in shutdown_signal_handler's unmarked-signal branch. In _stop_impl, a signal-initiated (non-restart) teardown now persists "running" instead of "stopped" — preserving the operator's run-intent and overwriting the mid-shutdown "draining" marker so _AUTOSTART_STATES matches on reboot. Operator stops and restarts persist "stopped" as before. - service_manager.py: S6ServiceManager.stop() now writes the planned-stop marker for the supervised PID (read from s6-svstat) before `s6-svc -d`, so an in-container `hermes gateway stop` is correctly classified as intentional (parity with the systemd/launchd/host stop paths, which already mark). Best-effort: a marker-write failure falls back to the safe signal-initiated path. Tests: shutdown persist-decision table (signal→running, operator→stopped, restart→stopped), s6 stop marker write + svstat PID parse + failure tolerance. The signal→running and s6-marker tests fail without the respective source change. Verified end-to-end against a container built from this branch: an unmarked SIGTERM to the live gateway leaves gateway_state=running (shutdown-context log confirms signal path); existing real container-restart suite still green. * docs(docker): clarify gateway autostart distinguishes operator-stop from container-kill The per-profile-supervision section described the autostart-across-restart contract as "running gateways come back, stopped stay stopped" without spelling out what records 'stopped'. That contract was the source of #42675 confusion: users expected a restart to bring the gateway back and it didn't. With the write-side fix, only an explicit `hermes gateway stop` records 'stopped'; container/s6 restart SIGTERMs (incl. image upgrades and unexpected exits) leave the state 'running' so the gateway auto-starts. Make that distinction explicit in both the multi-profile and per-profile-supervision sections. * test(docker): real-restart autostart E2E for #42675 Adds test_live_gateway_autostarts_after_real_restart_without_manual_state_stamp: a live s6-supervised gateway is killed by an actual `docker restart` SIGTERM (no manual gateway_state stamp, no planned-stop marker) and must auto-start on the next boot. Exercises the WRITE side of the fix that the existing stamp-based tests bypass. Verified to FAIL against an origin/main image (reconciler logs prior_state=stopped action=registered — the #42675 bug) and PASS against the fixed image (prior_state=running action=started).
2026-06-10 14:01:34 +10:00
- `docker restart`, image upgrades (`docker compose up -d --force-recreate`), and unexpected exits preserve running gateways: the cont-init reconciler reads `$HERMES_HOME/profiles/<name>/gateway_state.json` and brings the slot back up if the last recorded state was `running`. Only an explicit `hermes gateway stop` records `stopped` and keeps the gateway down across the restart; the container/s6 SIGTERM sent on a restart or upgrade is treated as "still running" and auto-starts.
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
- Per-profile gateway logs persist under `$HERMES_HOME/logs/gateways/<profile>/current` (rotated by `s6-log`), and the reconciler's actions are appended to `$HERMES_HOME/logs/container-boot.log` per boot. See [Where the logs go](#where-the-logs-go) for the full routing map.
docs(s6): document container supervision; doctor + skill + user-guide updates Phase 5 of the s6-overlay supervision plan. Documentation + small diagnostic cleanups; no behavior changes. website/docs/user-guide/docker.md: - Replace the old 'entrypoint script does the bootstrap' section with the s6-overlay boot flow (cont-init.d/01-hermes-setup, cont-init.d/02-reconcile-profiles, static main-hermes + dashboard services, ENTRYPOINT-as-main-program pattern). - Add a 'Per-profile gateway supervision' subsection covering the new lifecycle commands, restart semantics, log persistence, and 'Manager: s6 (container supervisor)' status reporting. - Add 'Breaking change vs. pre-s6 images' callout naming the /init ENTRYPOINT and pointing affected wrappers at the pin workaround. website/docs/user-guide/profiles.md: - Add a note under 'Persistent services' pointing container users at the docker.md section explaining s6 supervision inside the image. Host-side systemd/launchd documentation is unchanged. skills/software-development/hermes-s6-container-supervision/SKILL.md: - New maintainer skill covering the supervision-tree map, file layout, the Architecture B rationale (cont-init.d args + halt exit-code propagation), quick recipes, and the 8 pitfalls we hit while implementing the plan (PATH-without-/command, root-owned profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.). hermes_cli/doctor.py: - _check_gateway_service_linger skips on s6 (the linger concept doesn't apply inside the container). - New _check_s6_supervision section reports main-hermes/dashboard state and per-profile-gateway count (registered vs supervised up), only inside the s6 container. Host doctor output unchanged. - External Tools / Docker check no longer emits a 'docker not found' warning inside the container; prints an explanatory info line instead. Still respects an explicit TERMINAL_ENV=docker (in case the user mounted /var/run/docker.sock). hermes_cli/gateway.py: - Document _container_systemd_operational more precisely: it's NOT for our Hermes Docker image (s6-overlay handles that via detect_service_manager() == 's6'). It still covers systemd-nspawn / k8s-with-systemd-init cases, so leaving it in place is correct; the docstring just makes that explicit. Test harness (verification, no test changes in this commit): 19 passed, 0 xfailed. 66 service-manager / container-boot / profiles-s6-hooks / gateway-s6-dispatch unit tests still green. 61 doctor tests still green. Hadolint + shellcheck clean. Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
2026-05-21 17:05:32 +10:00
`hermes status` inside the container reports `Manager: s6 (container supervisor)`. Use `/command/s6-svstat /run/service/gateway-<name>` for the raw supervisor view (note `/command/` is on PATH for supervision-tree processes only; pass the absolute path when calling from `docker exec`).
## Upgrading
fix(docker): run config migrations during container boot (salvage #35508) (#36627) Salvage of #35508 (@dchenk), rebased onto current main. Resolved the tests/tools/test_stage2_hook_puid_pgid.py conflict (kept both the envdir-creation regression test on main and the new config-migration tests). Docker image upgrades replace code under $INSTALL_DIR but preserve $HERMES_HOME on the mounted volume, so the persisted config.yaml never received the schema migrations that non-Docker `hermes update` runs (#35406). This adds scripts/docker_config_migrate.py, invoked from stage2-hook after first-boot seeding and before gateway services start: it backs up config.yaml + .env, runs migrate_config(interactive=False), and honors HERMES_SKIP_CONFIG_MIGRATION=1 for manual control. Also fixes a latent bug in check_config_version(): it called load_config() which deep-merges DEFAULT_CONFIG, so a legacy config with no raw _config_version falsely reported as already-current. It now reads the raw on-disk file so legacy configs are correctly detected for migration. Differs from #35508 as submitted (Option B cleanup): dropped the `_config_version` line added to cli-config.yaml.example and removed the accompanying test_cli_config_example_declares_latest_version change-detector test. The example is a copy-template and has no business asserting a schema version; check_config_version() reads the user's real config.yaml, not the example. This removes a second sync point that drifts on every version bump. Closes #35508. Fixes #35406. Co-authored-by: Dmitriy Cherchenko <17372886+dchenk@users.noreply.github.com>
2026-06-04 11:11:27 +10:00
Pull the latest image and recreate the container. Your data directory is
preserved, and the container runs non-interactive config-schema migrations
against the mounted `$HERMES_HOME/config.yaml` before starting the gateway.
When a migration is needed, Hermes writes timestamped backups next to
`config.yaml` and `.env` first.
```sh
docker pull nousresearch/hermes-agent:latest
docker rm -f hermes
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
nousresearch/hermes-agent gateway run
```
Or with Docker Compose:
```sh
docker compose pull
docker compose up -d
```
docs: comprehensive documentation audit — fix 9 HIGH, 20+ MEDIUM gaps (#4087) Reference docs fixes: - cli-commands.md: remove non-existent --provider alibaba, add hermes profile/completion/plugins/mcp to top-level table, add --profile/-p global flag, add --source chat option - slash-commands.md: add /yolo and /commands, fix /q alias conflict (resolves to /queue not /quit), add missing aliases (/bg, /set-home, /reload_mcp, /gateway) - toolsets-reference.md: fix hermes-api-server (not same as hermes-cli, omits clarify/send_message/text_to_speech) - profile-commands.md: fix show name required not optional, --clone-from not --from, add --remove/--name to alias, fix alias path, fix export/ import arg types, remove non-existent fish completion - tools-reference.md: add EXA_API_KEY to web tools requires_env - mcp-config-reference.md: add auth key for OAuth, tool name sanitization - environment-variables.md: add EXA_API_KEY, update provider values - plugins.md: remove non-existent ctx.register_command(), add ctx.inject_message() Feature docs additions: - security.md: add /yolo mode, approval modes (manual/smart/off), configurable timeout, expanded dangerous patterns table - cron.md: add wrap_response config, [SILENT] suppression - mcp.md: add dynamic tool discovery, MCP sampling support - cli.md: add Ctrl+Z suspend, busy_input_mode, tool_preview_length - docker.md: add skills/credential file mounting Messaging platform docs: - telegram.md: add webhook mode, DoH fallback IPs - slack.md: add multi-workspace OAuth support - discord.md: add DISCORD_IGNORE_NO_MENTION - matrix.md: add MSC3245 native voice messages - feishu.md: expand from 129 to 365 lines (encrypt key, verification token, group policy, card actions, media, rate limiting, markdown, troubleshooting) - wecom.md: expand from 86 to 264 lines (per-group allowlists, media, AES decryption, stream replies, reconnection, troubleshooting) Configuration docs: - quickstart.md: add DeepSeek, Copilot, Copilot ACP providers - configuration.md: add DeepSeek provider, Exa web backend, terminal env_passthrough/images, browser.command_timeout, compression params, discord config, security/tirith config, timezone, auxiliary models 21 files changed, ~1000 lines added
2026-03-30 17:15:21 -07:00
fix(docker): run config migrations during container boot (salvage #35508) (#36627) Salvage of #35508 (@dchenk), rebased onto current main. Resolved the tests/tools/test_stage2_hook_puid_pgid.py conflict (kept both the envdir-creation regression test on main and the new config-migration tests). Docker image upgrades replace code under $INSTALL_DIR but preserve $HERMES_HOME on the mounted volume, so the persisted config.yaml never received the schema migrations that non-Docker `hermes update` runs (#35406). This adds scripts/docker_config_migrate.py, invoked from stage2-hook after first-boot seeding and before gateway services start: it backs up config.yaml + .env, runs migrate_config(interactive=False), and honors HERMES_SKIP_CONFIG_MIGRATION=1 for manual control. Also fixes a latent bug in check_config_version(): it called load_config() which deep-merges DEFAULT_CONFIG, so a legacy config with no raw _config_version falsely reported as already-current. It now reads the raw on-disk file so legacy configs are correctly detected for migration. Differs from #35508 as submitted (Option B cleanup): dropped the `_config_version` line added to cli-config.yaml.example and removed the accompanying test_cli_config_example_declares_latest_version change-detector test. The example is a copy-template and has no business asserting a schema version; check_config_version() reads the user's real config.yaml, not the example. This removes a second sync point that drifts on every version bump. Closes #35508. Fixes #35406. Co-authored-by: Dmitriy Cherchenko <17372886+dchenk@users.noreply.github.com>
2026-06-04 11:11:27 +10:00
Set `HERMES_SKIP_CONFIG_MIGRATION=1` only if you need to inspect or migrate the
persisted config manually before letting the new image rewrite it.
docs: comprehensive documentation audit — fix 9 HIGH, 20+ MEDIUM gaps (#4087) Reference docs fixes: - cli-commands.md: remove non-existent --provider alibaba, add hermes profile/completion/plugins/mcp to top-level table, add --profile/-p global flag, add --source chat option - slash-commands.md: add /yolo and /commands, fix /q alias conflict (resolves to /queue not /quit), add missing aliases (/bg, /set-home, /reload_mcp, /gateway) - toolsets-reference.md: fix hermes-api-server (not same as hermes-cli, omits clarify/send_message/text_to_speech) - profile-commands.md: fix show name required not optional, --clone-from not --from, add --remove/--name to alias, fix alias path, fix export/ import arg types, remove non-existent fish completion - tools-reference.md: add EXA_API_KEY to web tools requires_env - mcp-config-reference.md: add auth key for OAuth, tool name sanitization - environment-variables.md: add EXA_API_KEY, update provider values - plugins.md: remove non-existent ctx.register_command(), add ctx.inject_message() Feature docs additions: - security.md: add /yolo mode, approval modes (manual/smart/off), configurable timeout, expanded dangerous patterns table - cron.md: add wrap_response config, [SILENT] suppression - mcp.md: add dynamic tool discovery, MCP sampling support - cli.md: add Ctrl+Z suspend, busy_input_mode, tool_preview_length - docker.md: add skills/credential file mounting Messaging platform docs: - telegram.md: add webhook mode, DoH fallback IPs - slack.md: add multi-workspace OAuth support - discord.md: add DISCORD_IGNORE_NO_MENTION - matrix.md: add MSC3245 native voice messages - feishu.md: expand from 129 to 365 lines (encrypt key, verification token, group policy, card actions, media, rate limiting, markdown, troubleshooting) - wecom.md: expand from 86 to 264 lines (per-group allowlists, media, AES decryption, stream replies, reconnection, troubleshooting) Configuration docs: - quickstart.md: add DeepSeek, Copilot, Copilot ACP providers - configuration.md: add DeepSeek provider, Exa web backend, terminal env_passthrough/images, browser.command_timeout, compression params, discord config, security/tirith config, timezone, auxiliary models 21 files changed, ~1000 lines added
2026-03-30 17:15:21 -07:00
## Skills and credential files
When using Docker as the execution environment (not the methods above, but when the agent runs commands inside a Docker sandbox — see [Configuration → Docker Backend](./configuration.md#docker-backend)), Hermes reuses a single long-lived container for all tool calls and automatically bind-mounts the skills directory (`~/.hermes/skills/`) and any credential files declared by skills into that container as read-only volumes. Skill scripts, templates, and references are available inside the sandbox without manual configuration, and because the container persists for the life of the Hermes process, any dependencies you install or files you write stay around for the next tool call.
docs: comprehensive documentation audit — fix 9 HIGH, 20+ MEDIUM gaps (#4087) Reference docs fixes: - cli-commands.md: remove non-existent --provider alibaba, add hermes profile/completion/plugins/mcp to top-level table, add --profile/-p global flag, add --source chat option - slash-commands.md: add /yolo and /commands, fix /q alias conflict (resolves to /queue not /quit), add missing aliases (/bg, /set-home, /reload_mcp, /gateway) - toolsets-reference.md: fix hermes-api-server (not same as hermes-cli, omits clarify/send_message/text_to_speech) - profile-commands.md: fix show name required not optional, --clone-from not --from, add --remove/--name to alias, fix alias path, fix export/ import arg types, remove non-existent fish completion - tools-reference.md: add EXA_API_KEY to web tools requires_env - mcp-config-reference.md: add auth key for OAuth, tool name sanitization - environment-variables.md: add EXA_API_KEY, update provider values - plugins.md: remove non-existent ctx.register_command(), add ctx.inject_message() Feature docs additions: - security.md: add /yolo mode, approval modes (manual/smart/off), configurable timeout, expanded dangerous patterns table - cron.md: add wrap_response config, [SILENT] suppression - mcp.md: add dynamic tool discovery, MCP sampling support - cli.md: add Ctrl+Z suspend, busy_input_mode, tool_preview_length - docker.md: add skills/credential file mounting Messaging platform docs: - telegram.md: add webhook mode, DoH fallback IPs - slack.md: add multi-workspace OAuth support - discord.md: add DISCORD_IGNORE_NO_MENTION - matrix.md: add MSC3245 native voice messages - feishu.md: expand from 129 to 365 lines (encrypt key, verification token, group policy, card actions, media, rate limiting, markdown, troubleshooting) - wecom.md: expand from 86 to 264 lines (per-group allowlists, media, AES decryption, stream replies, reconnection, troubleshooting) Configuration docs: - quickstart.md: add DeepSeek, Copilot, Copilot ACP providers - configuration.md: add DeepSeek provider, Exa web backend, terminal env_passthrough/images, browser.command_timeout, compression params, discord config, security/tirith config, timezone, auxiliary models 21 files changed, ~1000 lines added
2026-03-30 17:15:21 -07:00
The same syncing happens for SSH and Modal backends — skills and credential files are uploaded via rsync or the Modal mount API before each command.
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
## Installing more tools in the container
The official image ships with a curated set of utilities (see [What the Dockerfile does](#what-the-dockerfile-does)), but not every tool an agent might want is preinstalled. There are five recommended approaches, in increasing order of effort and durability.
### npm or Python tools — use `npx` or `uvx`
For any tool published to npm or PyPI, instruct Hermes to run it via `npx` (npm) or `uvx` (Python) and to remember that command in its persistent memory. If the tool needs a config file or credentials, instruct it to drop those under `/opt/data` (e.g. `/opt/data/<tool>/config.yaml`).
Dependencies are fetched on demand and cached for the life of the container. Configuration written under `/opt/data` survives container restarts because it lives on the bind-mounted host directory. The package cache itself is rebuilt after a `docker rm`, but `npx` and `uvx` re-fetch transparently the next time the tool runs.
### Other tools (apt packages, binaries) — install and remember
For anything outside npm or PyPI — `apt` packages, prebuilt binaries, language runtimes not already in the image — instruct Hermes how to install it (e.g. `apt-get update && apt-get install -y <package>`) and tell it to remember the install command. The tool persists for the rest of the container's lifetime, and Hermes will re-run the install command after a container restart when it next needs the tool.
This is a good fit for tools that are quick to install and used occasionally. For tools used constantly, prefer the next approach.
### Durable installs — build a derived image
When a tool must be available immediately on every container start with no re-install delay, build a new image that inherits from `nousresearch/hermes-agent` and installs the tool in a layer:
```dockerfile
FROM nousresearch/hermes-agent:latest
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends <your-package> \
&& rm -rf /var/lib/apt/lists/*
USER hermes
```
Build it and use it in place of the official image:
```sh
docker build -t my-hermes:latest .
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
my-hermes:latest gateway run
```
The entrypoint script and `/opt/data` semantics are inherited unchanged, so the rest of this page still applies. Remember to rebuild the image when pulling a newer upstream `nousresearch/hermes-agent`.
### Complex tools or multi-service stacks — run a sidecar container
For tools that bring their own service (a database, a web server, a queue, a headless browser farm) or that are too heavy to live inside the Hermes container, run them as a separate container on a shared Docker network. Hermes reaches the sidecar by container name, the same way it reaches a local inference server (see [Connecting to local inference servers](#connecting-to-local-inference-servers-vllm-ollama-etc)).
```yaml
services:
hermes:
image: nousresearch/hermes-agent:latest
container_name: hermes
restart: unless-stopped
command: gateway run
ports:
- "8642:8642"
volumes:
- ~/.hermes:/opt/data
networks:
- hermes-net
my-tool:
image: example/my-tool:latest
container_name: my-tool
restart: unless-stopped
networks:
- hermes-net
networks:
hermes-net:
driver: bridge
```
From inside the Hermes container, the sidecar is reachable at `http://my-tool:<port>` (or whatever protocol it serves). This pattern keeps each service's lifecycle, resource limits, and upgrade cadence independent, and avoids bloating the Hermes image with dependencies that are only needed by one tool.
### Broadly useful tools — open an issue or pull request
If a tool is likely to be useful to most Hermes Agent users, consider contributing it upstream rather than carrying it in a private derived image. Open an issue or pull request on the [hermes-agent repository](https://github.com/NousResearch/hermes-agent) describing the tool and its use case. Tools that get bundled into the official image benefit every user and avoid the maintenance overhead of a downstream fork.
## Connecting to local inference servers (vLLM, Ollama, etc.)
When running Hermes in Docker and your inference server (vLLM, Ollama, text-generation-inference, etc.) is also running on the host or in another container, networking requires extra attention.
### Docker Compose (recommended)
Put both services on the same Docker network. This is the most reliable approach:
```yaml
services:
vllm:
image: vllm/vllm-openai:latest
container_name: vllm
command: >
--model Qwen/Qwen2.5-7B-Instruct
--served-model-name my-model
--host 0.0.0.0
--port 8000
ports:
- "8000:8000"
networks:
- hermes-net
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
hermes:
image: nousresearch/hermes-agent:latest
container_name: hermes
restart: unless-stopped
command: gateway run
ports:
- "8642:8642"
volumes:
- ~/.hermes:/opt/data
networks:
- hermes-net
networks:
hermes-net:
driver: bridge
```
Then in your `~/.hermes/config.yaml`, use the **container name** as the hostname:
```yaml
model:
provider: custom
model: my-model
base_url: http://vllm:8000/v1
api_key: "none"
```
:::tip Key points
- Use the **container name** (`vllm`) as the hostname — not `localhost` or `127.0.0.1`, which refer to the Hermes container itself.
- The `model` value must match the `--served-model-name` you passed to vLLM.
- Set `api_key` to any non-empty string (vLLM requires the header but doesn't validate it by default).
- Do **not** include a trailing slash in `base_url`.
:::
### Standalone Docker run (no Compose)
If your inference server runs directly on the host (not in Docker), use `host.docker.internal` on macOS/Windows, or `--network host` on Linux:
**macOS / Windows:**
```sh
docker run -d \
--name hermes \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
nousresearch/hermes-agent gateway run
```
```yaml
# config.yaml
model:
provider: custom
model: my-model
base_url: http://host.docker.internal:8000/v1
api_key: "none"
```
**Linux (host networking):**
```sh
docker run -d \
--name hermes \
--network host \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway run
```
```yaml
# config.yaml
model:
provider: custom
model: my-model
base_url: http://127.0.0.1:8000/v1
api_key: "none"
```
:::warning With `--network host`, the `-p` flag is ignored — all container ports are directly exposed on the host.
:::
### Verifying connectivity
From inside the Hermes container, confirm the inference server is reachable:
```sh
docker exec hermes curl -s http://vllm:8000/v1/models
```
You should see a JSON response listing your served model. If this fails, check:
1. Both containers are on the same Docker network (`docker network inspect hermes-net`)
2. The inference server is listening on `0.0.0.0`, not `127.0.0.1`
3. The port number matches
### Ollama
Ollama works the same way. If Ollama runs on the host, use `host.docker.internal:11434` (macOS/Windows) or `127.0.0.1:11434` (Linux with `--network host`). If Ollama runs in its own container on the same Docker network:
```yaml
model:
provider: custom
model: llama3
base_url: http://ollama:11434/v1
api_key: "none"
```
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
## Troubleshooting
### Container exits immediately
Check logs: `docker logs hermes`. Common causes:
- Missing or invalid `.env` file — run interactively first to complete setup
- Port conflicts if running with exposed ports
### "Permission denied" errors
fix(docker): accept PUID/PGID as aliases for HERMES_UID/HERMES_GID (#25872) (#34401) Salvages #25872 by @konsisumer against current main. NAS users (UGOS, Synology, unRAID) expect the LinuxServer.io PUID/PGID convention and bind-mount /opt/data from a host directory owned by their own UID. Without this alias those vars are silently ignored and the s6-setuidgid drop to UID 10000 leaves the runtime unable to read the volume. HERMES_UID/HERMES_GID still take precedence when both are set. The original PR targeted docker/entrypoint.sh, which is now a 27-line deprecation shim under s6-overlay (the May 2026 rework moved all bootstrap logic to docker/stage2-hook.sh, installed as /etc/cont-init.d/01-hermes-setup). Re-applied the same 2-line alias resolution at the equivalent spot in stage2-hook.sh just before the existing UID/GID remap block. Test was retargeted at docker/stage2-hook.sh; docs hunk adapted to current main's wording ("stage2 hook" + s6-setuidgid, not the obsolete "entrypoint drops via gosu") with the NAS bind-mount example preserved verbatim. Test-first regression verification: reverted just docker/stage2-hook.sh to origin/main and re-ran the new tests. Result: FAILED test_stage2_hook_resolves_puid_pgid_aliases FAILED test_puid_pgid_populate_hermes_uid_gid AssertionError: assert ':' == '1000:10' That's the exact bug shape — PUID=1000 PGID=10 silently ignored, HERMES_UID/HERMES_GID stay empty. With the salvage applied, all 4 tests pass. Closes #25872 Co-authored-by: konsisumer <11262660+konsisumer@users.noreply.github.com>
2026-05-29 16:07:15 +10:00
The container's stage2 hook drops privileges to the non-root `hermes` user (UID 10000) via `s6-setuidgid` inside each supervised service. If your host `~/.hermes/` is owned by a different UID, set `HERMES_UID`/`HERMES_GID` — or their `PUID`/`PGID` aliases, for parity with LinuxServer.io and NAS images — to match your host user, or ensure the data directory is writable:
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
```sh
chmod -R 755 ~/.hermes
```
fix(docker): accept PUID/PGID as aliases for HERMES_UID/HERMES_GID (#25872) (#34401) Salvages #25872 by @konsisumer against current main. NAS users (UGOS, Synology, unRAID) expect the LinuxServer.io PUID/PGID convention and bind-mount /opt/data from a host directory owned by their own UID. Without this alias those vars are silently ignored and the s6-setuidgid drop to UID 10000 leaves the runtime unable to read the volume. HERMES_UID/HERMES_GID still take precedence when both are set. The original PR targeted docker/entrypoint.sh, which is now a 27-line deprecation shim under s6-overlay (the May 2026 rework moved all bootstrap logic to docker/stage2-hook.sh, installed as /etc/cont-init.d/01-hermes-setup). Re-applied the same 2-line alias resolution at the equivalent spot in stage2-hook.sh just before the existing UID/GID remap block. Test was retargeted at docker/stage2-hook.sh; docs hunk adapted to current main's wording ("stage2 hook" + s6-setuidgid, not the obsolete "entrypoint drops via gosu") with the NAS bind-mount example preserved verbatim. Test-first regression verification: reverted just docker/stage2-hook.sh to origin/main and re-ran the new tests. Result: FAILED test_stage2_hook_resolves_puid_pgid_aliases FAILED test_puid_pgid_populate_hermes_uid_gid AssertionError: assert ':' == '1000:10' That's the exact bug shape — PUID=1000 PGID=10 silently ignored, HERMES_UID/HERMES_GID stay empty. With the salvage applied, all 4 tests pass. Closes #25872 Co-authored-by: konsisumer <11262660+konsisumer@users.noreply.github.com>
2026-05-29 16:07:15 +10:00
On a NAS (UGOS, Synology, unRAID) the data directory is typically a **bind mount** owned by a host UID the container cannot `chown`. Set `PUID`/`PGID` (or `HERMES_UID`/`HERMES_GID`) to that host user so the runtime runs as the owner of the mount rather than UID 10000:
```sh
docker run -d \
--name hermes \
-e PUID=1000 -e PGID=10 \
-v /volume1/docker/hermes:/opt/data \
nousresearch/hermes-agent gateway run
```
docs(docker): refresh user-guide page for s6-overlay reality The page was last meaningfully rewritten in the pre-s6 (tini) era and had drifted on five points that no longer matched the image: 1. "Running the dashboard" claimed the entrypoint backgrounds `hermes dashboard` and prefixes its output with `[dashboard]`. That was the pre-s6 entrypoint.sh path; under s6 the dashboard is a supervised s6-rc service (`docker/s6-rc.d/dashboard/run`) with no sed-prefix pipeline. Rewrote the section accordingly. 2. The default for `HERMES_DASHBOARD_HOST` was documented as `127.0.0.1`. The s6 run script defaults it to `0.0.0.0` (`dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}"`). Fixed the table and the surrounding prose. 3. Multi-profile was documented as "not recommended in Docker — run one container per profile." That advice was load-bearing when there was no in-container supervisor, but the s6 architecture explicitly adds per-profile gateway supervision: each profile created via `hermes profile create <name>` gets a slot under `/run/service/gateway-<name>/`, the `02-reconcile-profiles` cont-init script restores them across `docker restart` from `gateway_state.json`, and `hermes gateway start/stop/restart` is intercepted by `_dispatch_via_service_manager_if_s6` to route through `s6-svc`. Pivoted the section to "one container, many supervised profile gateways" as the default, with a comparison table and a "When you DO want a separate container" escape hatch for the genuine resource-isolation / network-segmentation cases. 4. The Compose example trailer also claimed `[dashboard]` log prefixing. Replaced with the actual log routing. 5. Added a new "Where the logs go" section covering all four log surfaces: per-profile gateways (tee'd to `docker logs` AND `${HERMES_HOME}/logs/gateways/<profile>/current` since PR b34532319), dashboard (`docker logs`, no prefix), boot reconciler (`container-boot.log`), and `hermes logs`. The gateway-mode and Compose sections cross-reference this rather than each carrying their own routing prose. Added a new "docker exec automatically drops to the hermes user" subsection under "What the Dockerfile does", next to the existing Privilege model warning. Documents the `/opt/hermes/bin/hermes` shim (landed via the docker-exec privilege-drop work) — operators don't need to remember `--user hermes` for `docker exec hermes login`, `docker exec hermes profile create …`, etc. The historical footgun (`auth.json` written as `root:root`, supervised gateway then can't read its own auth file) is mentioned only as context for what the fail-loud `exit 126` is protecting against, not as a problem the reader needs to solve. The `HERMES_DOCKER_EXEC_AS_ROOT=1` opt-out is documented for diagnostic sessions. The "Permission denied" troubleshooting subsection now carries a single-line pointer to the new section instead of duplicating it. The `--insecure` framing reflects PR #fb5125362 (opt-in via `HERMES_DASHBOARD_INSECURE`, not derived from bind host): the OAuth gate is the authority, the bind host alone never implies `--insecure`, and opting out is an explicit security trade-off. Anchors verified resolve. i18n zh-Hans mirror left for the translation flow to catch up.
2026-05-29 11:33:32 +10:00
`docker exec hermes <cmd>` automatically drops to UID 10000 too — see [`docker exec` automatically drops to the `hermes` user](#docker-exec-automatically-drops-to-the-hermes-user) for details and the per-invocation opt-out.
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
### Browser tools not working
Playwright needs shared memory. Add `--shm-size=1g` to your Docker run command:
```sh
docker run -d \
--name hermes \
--shm-size=1g \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway run
```
### Gateway not reconnecting after network issues
The `--restart unless-stopped` flag handles most transient failures. If the gateway is stuck, restart the container:
```sh
docker restart hermes
```
### Checking container health
```sh
docker logs --tail 50 hermes # Recent logs
docker run -it --rm nousresearch/hermes-agent:latest version # Verify version
docs: deep quality pass — expand 10 thin pages, fix specific issues (#4134) Developer guide stubs expanded to full documentation: - trajectory-format.md: 56→233 lines (JSONL format, ShareGPT example, normalization rules, reasoning markup, replay code) - session-storage.md: 66→388 lines (SQLite schema, migration table, FTS5 search syntax, lineage queries, Python API examples) - context-compression-and-caching.md: 72→321 lines (dual compression system, config defaults, 4-phase algorithm, before/after example, prompt caching mechanics, cache-aware patterns) - tools-runtime.md: 65→246 lines (registry API, dispatch flow, availability checking, error wrapping, approval flow) - prompt-assembly.md: 89→246 lines (concrete assembled prompt example, SOUL.md injection, context file discovery table) User-facing pages expanded: - docker.md: 62→224 lines (volumes, env forwarding, docker-compose, resource limits, troubleshooting) - updating.md: 79→167 lines (update behavior, version checking, rollback instructions, Nix users) - skins.md: 80→206 lines (all color/spinner/branding keys, built-in skin descriptions, full custom skin YAML template) Hub pages improved: - integrations/index.md: 25→82 lines (web search backends table, TTS/browser providers, quick config example) - features/overview.md: added Integrations section with 6 missing links Specific fixes: - configuration.md: removed duplicate Gateway Streaming section - mcp.md: removed internal "PR work" language - plugins.md: added inline minimal plugin example (self-contained) 13 files changed, ~1700 lines added. Docusaurus build verified clean.
2026-03-30 20:30:11 -07:00
docker stats hermes # Resource usage
```