Files
light-heart-labs--dreamserver/ods/docs/HERMES.md
T
wehub-resource-sync 9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:33 +08:00

17 KiB

Hermes Agent

ODS ships Hermes Agent — the Nous Research open-source agent packaged as a ODS service. Hermes is a self-improving generalist agent with persistent memory, autonomous skill creation, and 70+ tools built in.

When enabled, Hermes runs in a container alongside the rest of the stack, serves its own browser dashboard on internal port 9119, and talks to the selected model provider through an OpenAI-compatible API. End users should enter through hermes-proxy on port 9120; direct host access to 9119 is intentionally not bound in the default stack.

What you get

Hermes ships its own complete web UI — ODS is just packaging it. After ods enable hermes + ods enable hermes-proxy, you can browse to http://<device>:9120 (or hermes.<device>.local:9120 once mDNS announcement lands — see "Roadmap" below). The proxy is the LAN-facing entry; it gates access on ODS's magic-link cookie before forwarding to Hermes's internal port 9119. See docs/HERMES-SSO.md for the full auth flow. Once past the proxy you find pages for:

  • Chat — conversational interface with streaming responses + inline tool calls
  • Sessions — list, switch between, prune past conversations
  • Skills — view skills Hermes has autonomously created from your interactions; edit or delete
  • Memories — persistent facts Hermes has learned about you
  • Profiles — per-user agent contexts (a built-in alternative to running multiple Hermes containers)
  • Cron — schedule recurring agent tasks
  • Models — pick which LLM Hermes uses (defaults to your llama-server)
  • Config / Env — Hermes's own settings
  • Logs / Analytics — operational visibility

Dashboard feature cards

The ODS dashboard exposes two separate Hermes entry points:

  • Hermes Agent opens the authenticated Hermes runtime through hermes-proxy on port 9120. It must never link to a raw inference endpoint such as llama-server or LiteLLM.
  • Hermes Single Sign-On opens the dashboard's Setup / Owner page at /invites, where operators manage owner cards and temporary support magic links. It is an access-management surface, not a second link to the Hermes runtime.

Hermes readiness is provider-neutral. A healthy local llama-server or a healthy LiteLLM route can satisfy the inference dependency, so the same feature contract works for local, cloud, and external-provider installations on Linux, macOS, Windows, and WSL.

Architecture

  Browser
     │  http://<device>:9120
     ▼
  ┌─────────────────────────────────────┐
  │  ods-hermes-proxy                 │
  │    forward_auth → dashboard-api     │
  │    reverse_proxy → ods-hermes     │
  └─────────────────────────────────────┘
                 │
                 │  Docker network: ods-hermes:9119
                 ▼
  ┌─────────────────────────────────────┐
  │  ods-hermes container             │
  │                                     │
  │   hermes gateway run                │
  │     - HERMES_DASHBOARD=1 →          │
  │         React SPA + /api endpoints  │
  │     - scheduler tick() every 60s →  │
  │         fires cron jobs             │
  │     - no messaging adapters         │
  │                                     │
  │   State: /opt/data (HERMES_HOME)    │
  │     mounted from data/hermes/       │
  │                                     │
  │   Tool sandbox: local (in-container)│
  └─────────────────────────────────────┘
                  │
                  │  OpenAI-compatible API
                  ▼
  ┌─────────────────────────────────────┐
  │  llama-server (existing)            │
  │     llama.cpp at :8080/v1           │
  └─────────────────────────────────────┘

State layout under data/hermes/:

data/hermes/
├── config.yaml      # Bootstrapped from our cli-config.yaml.template on first start
├── .env             # Bootstrapped from upstream's .env.example
├── SOUL.md          # Bootstrapped from our SOUL.md.template
├── sessions/        # Per-session chat history
├── memories/        # Persistent agent memories
├── skills/          # Agent-authored skills
├── cron/            # Scheduled tasks
├── plans/           # Active multi-step plans
├── workspace/       # Sandboxed workspace for file ops
├── hooks/           # Custom lifecycle hooks
├── home/            # Per-profile $HOME for subprocesses (git, ssh, npm…)
└── logs/            # Hermes's own logs (separate from Docker logs)

Setup

# 1. (One-time) Verify ODS's llama-server is running:
ods status llama-server

# 2. Pull + start Hermes and its auth proxy:
ods enable hermes
ods enable hermes-proxy

# 3. Open the auth-gated dashboard:
xdg-open http://localhost:9120

The first start takes a minute — image is ~3GB, Hermes runs its skills_sync.py bootstrap, and llama-server may cold-load the model on Hermes's first request. Subsequent starts are fast.

Defaults ODS applies

  • Provider: custom (OpenAI-compatible) pointing at llama-server:8080/v1
  • Context: Hermes requires at least 64K tokens. Local installers run the bootstrap model at 64K so the agent works immediately, then move the full model and config to the model selector's chosen context after the background model upgrade completes. Large-context tiers still use 128K when they select a 128K-capable model; constrained tiers can remain at a smaller context.
  • Compression: enabled at compression.threshold: 0.50 with target_ratio: 0.20 so long sessions compact before the backend hard-rejects an over-window request.
  • Model name: qwen3.5-9b (ODS's default LLM — to switch models, edit model.default in data/hermes/config.yaml after first start; there is no env-var hook for this)
  • Persona (SOUL.md): a generalist ODS-aware persona (see extensions/services/hermes/SOUL.md.template)
  • Messaging gateways DISABLED: Telegram / Discord / Slack / WhatsApp / Signal / Teams / Google Chat / Matrix / Mattermost / SMS — all off by default. ODS owner-card users reach Hermes through the ODS Talk mobile portal, while advanced users can still open the full Hermes web dashboard. WhatsApp is pre-seeded as disabled with bridge_port: 3010 so upstream's default 3000 bridge does not collide with Open WebUI when users intentionally enable it. To enable any platform, see upstream messaging docs.
  • Network exposure: Hermes is not directly LAN-reachable. Once the hermes-proxy extension is enabled (see docs/HERMES-SSO.md), the proxy at port 9120 fronts Hermes and gates access on ODS's magic-link cookie. Hermes's own port 9119 is internal-only. To restore direct access (e.g. for testing without auth), re-add a ports: binding to extensions/services/hermes/compose.yaml.
  • Resource caps: 4 CPUs / 4GB RAM hard limit, 0.5 CPU / 1GB reservation. Hermes's playwright + ML deps can be hungry; adjust in extensions/services/hermes/compose.yaml if needed.

Configuration

Three layers, highest to lowest precedence:

  1. Edit data/hermes/config.yaml directly — Hermes's own config file, copied from our template on first start. Survives container restarts. Reset by deleting and restarting. The model name lives here, not in env.
  2. Set env vars in ODS's .envHERMES_LLM_BASE_URL, HERMES_LLM_API_KEY, HERMES_LANGUAGE, optional WHATSAPP_* gateway settings, and the HERMES_PROXY_* proxy settings. Hermes itself has no host-port env knob in the auth-gated stack; the LAN-facing port is HERMES_PROXY_PORT.
  3. Fall back to ODS's defaults — defined in extensions/services/hermes/cli-config.yaml.template.

To bring up Hermes pointing at a different LLM (e.g. OpenRouter, OpenAI, Anthropic), edit data/hermes/config.yaml's model.provider and model.base_url and restart. The whole gamut of provider options is listed in the upstream config — Hermes supports OpenRouter / Anthropic / OpenAI / Hugging Face / NVIDIA NIM / z.ai / Kimi / Gemini / Ollama Cloud / LM Studio / etc. out of the box.

For local backends, keep model.context_length and auxiliary.compression.context_length aligned with .env's CTX_SIZE / MAX_CONTEXT. Values below 64000 will make Hermes reject prompts; values above the server's real context can produce context length exceeded / max compression attempts reached loops.

Security posture

  • --insecure is enabled inside the container. Hermes's dashboard refuses non-loopback binds without it. ODS accepts that trade-off only because port 9119 is not host-bound in the default stack; the LAN-facing entry is the magic-link-gated proxy on port 9120. Do not add a public 9119 host binding.
  • The container runs as a non-root user (UID 10000 by default, remappable via HERMES_UID). The entrypoint drops privileges via gosu before any agent code runs.
  • The container has full network access within ODS's bridge net — Hermes can make outbound HTTP requests for tools like web_search. If you want to restrict this, add an iptables firewall rule on the host or run Hermes behind a forward proxy.
  • No APE policy enforcement yet. Hermes's 70+ tools include shell + file write. The base config defaults toward less-risky tools, but Hermes can still execute shell commands inside its sandbox container. APE policy wrapping is a planned follow-up; until then, the trust model is "the user authenticated to Hermes is trusted to use the local container."

How to bump the image pin

Hermes is a young, fast-moving project. ODS pins a reviewed upstream image tag in compose.yaml instead of auto-tracking :latest. Operators can temporarily override it with HERMES_AGENT_IMAGE, and can provide HERMES_AGENT_IMAGE_FALLBACK for registry hotfixes, but changing the shipped default is a deliberate review-and-smoke-test pass:

# 1. Pick a published upstream image tag.
curl -s 'https://hub.docker.com/v2/repositories/nousresearch/hermes-agent/tags?page_size=25' \
  | jq -r '.results[] | [.name, .last_updated] | @tsv'

# 2. Verify Docker can resolve the tag on a clean machine.
docker manifest inspect nousresearch/hermes-agent:<new-tag> >/dev/null

# 3. Review upstream release notes / commits. Skim breaking changes,
#    config-format migrations, removed env vars, and dashboard changes.

# 4. Update:
#    - extensions/services/hermes/compose.yaml
#    - installers/phases/08-images.sh
#    - config/dependency-lock.json
#    - this bump-history table

# 5. Smoke test:
#    ods restart hermes
#    docker inspect --format '{{.State.Health.Status}}' ods-hermes
#    curl http://localhost:9120/health
#    # NOTE: most /api/* routes are auth-gated; /api/status is the public
#    # JSON-backed endpoint used by ODS's health metadata and Docker probe.
#    open http://localhost:9120, sign in, send a chat, verify tool call

# 5. If it works, commit. If config.yaml format has changed, document the
#    migration in this file's "Bump history" section below.

Roadmap (deferred from v1)

These were in the original integration plan but cut once we discovered Hermes ships a complete browser surface:

  • mDNS announcement — register hermes.<device>.local in the ODS mDNS announcer. shipped as #1167 (stacked on #1152).
  • Magic-link SSO — magic-link cookie gates access to Hermes via the new hermes-proxy Caddy sidecar. shipped — see docs/HERMES-SSO.md. Known limitation: single shared Hermes for all users; real per-user isolation would require per-user containers.
  • APE policy integration — route Hermes's tool calls through APE for allow/deny + audit. APE is already in the stack; needs a small adapter inside or in front of Hermes.
  • Voice in/out from ODS's whisper + kokoro — Hermes has its own audio pipeline (the image bundles ffmpeg + playwright); verify whether it already proxies to local TTS/STT services or whether we need to wire that ourselves.
  • ODS-side status panel — surface Hermes's session count + skill inventory in the ODS dashboard. Lower priority since Hermes has its own AnalyticsPage.
  • Per-user Hermes containers — if true multi-user becomes a felt need, spawn one Hermes per magic-link target_username and have the proxy route based on the redeemed identity. See docs/HERMES-SSO.md.

Troubleshooting

docker inspect ods-hermes is not healthy, or localhost:9120 returns 502

Hermes hasn't finished bootstrapping. Watch the logs:

docker logs -f ods-hermes

First start does a ~30s skills sync; subsequent starts are fast.

Chat slows down after many ODS Talk or Hermes sessions

Some upstream Hermes builds can leave tui_gateway.slash_worker child processes running after sessions that use /slash commands such as /no_think. Each worker can hold model memory, so a long day of owner-card or fleet-test sessions may create memory pressure even though the main Hermes container looks healthy.

Check with:

ods doctor
docker exec ods-hermes sh -c "ps -eo pid=,etimes=,args= | grep '[t]ui_gateway[.]slash_worker'"

Clean up workers that exceed the age/count policy:

ods repair hermes-workers

Policy knobs:

HERMES_SLASH_WORKER_MAX_COUNT=8
HERMES_SLASH_WORKER_MAX_AGE_SECONDS=3600

ods doctor reports high worker counts. ods repair hermes-workers is explicit and manual; ODS does not run an automatic process killer for active Hermes sessions.

Hermes can't reach the LLM

Inside the container, llama-server:8080 should resolve to the llama-server container. Test with:

docker exec ods-hermes curl -fs http://llama-server:8080/v1/models

If that fails, the most likely cause is that the Hermes container isn't on ODS's docker network. Check docker network inspect ods_default.

WhatsApp reports port 3000 already in use

ODS's bundled Hermes config uses platforms.whatsapp.extra.bridge_port: 3010, and the Hermes container does not bind that bridge to the host. If you see a WhatsApp bridge conflict on port 3000, you are probably running Hermes Agent natively, running upstream Hermes with host networking, or using an older data/hermes/config.yaml copied before this default existed.

Set the WhatsApp bridge port in data/hermes/config.yaml and restart Hermes:

platforms:
  whatsapp:
    enabled: true
    extra:
      bridge_port: 3010

Hermes says context length exceeded or max compression attempts reached

Check that the running backend and Hermes agree on context:

grep -E "^(CTX_SIZE|MAX_CONTEXT)=" .env
grep -n "context_length\|threshold\|target_ratio" data/hermes/config.yaml

For Hermes, CTX_SIZE / MAX_CONTEXT, model.context_length, and auxiliary.compression.context_length should be at least 65536. Fresh local installs use 65536 during bootstrap, then keep the model selector's chosen full-model context after the swap. On larger tiers that may be 131072; on constrained tiers it can stay lower.

Sessions / memories / skills disappeared after upgrade

The image pin protects you from accidental version drift, but if you DID bump and lose data, it's almost certainly because Hermes's config format changed. Check data/hermes/config.yaml against upstream's current cli-config.yaml.example. The container's first start regenerates config.yaml from our template only if it doesn't exist — your old config sticks around.

"I don't want Hermes anymore"

ods disable hermes              # stops the container
rm -rf data/hermes                # wipes all sessions / memories / skills

The container image stays cached — docker image prune removes it.

Upstream attribution

Hermes Agent is © 2026 Nous Research, MIT-licensed. ODS's contribution is the packaging layer (extensions/services/hermes/) — no code is forked from upstream. The pinned image is pulled directly from docker.io/nousresearch/hermes-agent.

When promoting / talking about this extension, the convention is: "Hermes Agent (from Nous Research) — packaged for ODS."

Bump history

Date Pinned image Notes
2026-06-01 nousresearch/hermes-agent:v2026.5.16 Replace removed upstream sha-* tag with a published version tag; add HERMES_AGENT_IMAGE override/fallback path.
2026-05-12 dd0923bb89ed2dd56f82cb63656a1323f6f42e6f Initial integration.