Files
containers/images/hermes/readme.md
grenade d53e06d784
All checks were successful
images / hermes (push) Successful in 1m55s
hermes: two-stage build, make /opt/hermes writable by uid 10000
Upstream ships /opt/hermes (app + .venv + scripts) read-only root, which
blocks the agent self-modifying and the gateway auto-installing the
WhatsApp bridge's node_modules in place. Add a derived Containerfile layer
(FROM the upstream build) that chowns/chmods /opt/hermes writable by the
runtime hermes user. Done in the image, not a volume: a volume over
/opt/hermes copies-up once then freezes the app, silently defeating
AutoUpdate=registry. Persistence stays on the /opt/data volume.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011D3YeWKpjg5bT488fVanCH
2026-06-23 18:31:32 +03:00

84 lines
4.0 KiB
Markdown

# hermes
[NousResearch Hermes Agent](https://github.com/NousResearch/hermes-agent) — a
self-improving AI agent — packaged for lair infra and published to
`git.lair.cafe/lair/hermes`.
## How it's built
Upstream ships its own `Dockerfile` (debian 13 + s6-overlay), so we build in two
stages — `build.sh` and the `images` workflow both do this:
1. **Upstream** — build straight from the upstream git context at the release tag
into a local tag: `podman build github.com/NousResearch/hermes-agent.git#<tag>`.
2. **Derived** — [`Containerfile`](Containerfile) does `FROM` that and makes the
`/opt/hermes` app tree writable by the runtime `hermes` user (uid 10000), then
publishes `git.lair.cafe/lair/hermes:{<version>,latest}`.
The writable-tree layer gives the agent "untied hands" (self-modify; the gateway
can auto-install the WhatsApp bridge's `node_modules` in place) **without** the
volume trap: a volume over `/opt/hermes` would copy-up once and freeze the app,
silently defeating `AutoUpdate=registry`. Baked into the image, it ships
hands-free on every pull and refreshes cleanly on update. **Persistence belongs
on `/opt/data`** (skills, memory, sessions, the WhatsApp bridge via `bridge_script`),
never in `/opt/hermes`.
Builds are **release-triggered** (daily poll of the GitHub releases API; a build
runs only when that version isn't already in our registry) and **self-healing**
(a failed build leaves the version absent, so the next poll retries). When the
*build definition* changes (e.g. this writable layer) for an already-published
version, republish with the workflow's `force` dispatch input. Locally:
```
HERMES_REF=v0.17.0 ./build.sh
```
## How it runs (single container, gateway + dashboard)
The image is an s6-overlay supervisor. The **command selects the mode** — the
default (no command) is the *interactive CLI*, which exits without a TTY under
systemd and crash-loops. The supported headless setup (per the image's own
startup guidance) is **one container running `gateway run` with the dashboard
supervised alongside** via `HERMES_DASHBOARD`:
- `Exec=gateway run` → the agent daemon (`hermes gateway run --replace`), s6-supervised.
- `HERMES_DASHBOARD=1` → the dashboard web UI (binds `0.0.0.0:9119`) in the **same**
container — which is what lets it reach the gateway (two bridge-networked
containers could not, unlike upstream's host-networked compose).
- The image **fails closed** on a non-loopback dashboard bind: it refuses
`0.0.0.0` unless OAuth is configured *or* `HERMES_DASHBOARD_INSECURE=1` is set.
We expose on the trusted LAN without auth, so we opt in. ⚠ Anyone on the LAN
can reach the API-key-storing UI — switch to `HERMES_DASHBOARD_OAUTH_CLIENT_ID`
for real auth if that's not acceptable.
Persistent state — `config.yaml`, `.env`, sessions, memory, skills — all lives
under `/opt/data` (the single `:Z` volume). Keys/backend go in those mounted
files, never in the image or quadlet.
## Deploying on bob
See [`hermes.container`](hermes.container) — a rootful quadlet matching the
existing `agent-zero` / `open-webui` services. As deployed:
1. Publish `git.lair.cafe/lair/hermes:latest` (the `images` workflow).
2. `sudo install -d -o 10000 -g 10000 /var/lib/hermes`
3. Drop `config.yaml` into `/var/lib/hermes` (owned `10000:10000`) — **LLM backend
→ local sovereign inference** via Hermes' `custom` OpenAI-compatible provider:
```yaml
# /var/lib/hermes/config.yaml
model:
provider: "custom"
base_url: "http://hanzalova.internal:31313/v1" # same endpoint open-webui uses
api_key: "<your-inference-key>"
default: "<model-id>" # see: curl …/v1/models
# context_length / max_tokens optional
```
Other secrets (web-search/tool keys, messaging tokens) go in
`/var/lib/hermes/.env`.
4. Install `hermes.container` to `/etc/containers/systemd/`, `daemon-reload`,
`start hermes.service`. Dashboard then serves on the LAN at
`http://bob.hanzalova.internal:5100/`; `AutoUpdate=registry` enrolls it in the
host's `podman-auto-update.timer`.