Hermes Agent
Overview
Hermes Agent by Nous Research — self-hosted AI agent with persistent memory, multi-platform messaging gateway, and automated skill creation. Runs inside a Kata VM for isolation.
Deployment
| Component | Value |
|---|---|
| Namespace | hermes |
| Image | nousresearch/hermes-agent (Docker Hub) |
| Runtime | kata (VM isolation) |
| Node | prusik |
| User | root (required by s6-overlay stage2 hook for volume chown) |
| Data volume | /opt/data on PVC hermes-data (100Gi, openebs-zfspv) |
| Backups | Daily ZFS snapshots (maxCount=5, 01:00 UTC), quarterly Velero |
| Resources | 500m-8 CPU, 1-4Gi RAM |
| Dashboard | hermes.internal.grigri.cloud (port 9119, OIDC via Kanidm) |
| Inference | Custom provider isidoro (Kiais) at isidoro.grigri.cloud/v1 |
Key Files
| File | Purpose |
|---|---|
apps/hermes/values.yaml |
Helm values (image, runtime, resources, PVC mount, OIDC env) |
apps/hermes/templates/pvc.yaml |
PVC with Prune=false protection |
apps/hermes/templates/snapshots.yaml |
Daily ZFS snapshot schedule |
apps/hermes/templates/kanidm-oauth2-client.yaml |
Kaniop OAuth2 client for dashboard |
apps/hermes/templates/kanidm-group.yaml |
KanidmGroup hermes-users |
/opt/data/config.yaml |
Runtime config (model, skills, plugins) — on PVC, not GitOps |
Architecture Notes
- s6-overlay is PID 1 (
/init). It supervises the gateway, dashboard, and per-profile services. - The ENTRYPOINT is
["/init", "/opt/hermes/docker/main-wrapper.sh"]. Container args are routed through the wrapper — do not setcommand(which overrides ENTRYPOINT), useargsinstead. Settingcommand: ["gateway", "run"]breaks s6-overlay init and crashes with exit code 2. - The stage2 hook runs as root to chown
/opt/data, then drops to thehermesuser (UID 10000) vias6-setuidgidfor supervised services. - Multi-profile support:
kubectl exec -n hermes hermes-0 -- hermes profile create <name>. Each profile gets its own supervised gateway with independent state.
Dashboard OIDC Auth
The dashboard uses the self-hosted OIDC provider (Kanidm) for authentication. Two layers protect it: oauth2-proxy at the ingress (group-gated) and the dashboard's own OIDC session.
Dashboard refuses to bind (no auth provider)
Problem: Dashboard logs Refusing to bind dashboard to 0.0.0.0 and never listens on 9119.
Ingress returns 502 Bad Gateway.
Root Cause: HERMES_DASHBOARD_INSECURE no longer disables the auth gate. The dashboard
requires a registered auth provider (basic auth or OIDC) before binding to a non-loopback address.
Fix: Configure OIDC via env vars (done in values.yaml):
- name: HERMES_DASHBOARD_OIDC_ISSUER
value: https://idm.grigri.cloud/oauth2/openid/hermes
- name: HERMES_DASHBOARD_OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: hermes-kanidm-oauth2-credentials
key: CLIENT_ID
- name: HERMES_DASHBOARD_OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: hermes-kanidm-oauth2-credentials
key: CLIENT_SECRET
Kanidm rejects redirect_uri (invalid_origin)
Problem: Login redirects to Kanidm but shows an error. Kanidm logs:
Invalid OAuth2 redirect_uri (must be an exact match) - got http://hermes.internal.grigri.cloud/auth/callback.
Root Cause: Behind a reverse proxy, the dashboard sees http:// (the proxy terminates TLS)
and constructs the redirect_uri with the wrong scheme. Kanidm requires an exact match against the
registered https:// URL.
Fix: Set the public URL so the dashboard knows its external origin:
- name: HERMES_DASHBOARD_PUBLIC_URL
value: https://hermes.internal.grigri.cloud
Or in /opt/data/config.yaml:
dashboard:
public_url: https://hermes.internal.grigri.cloud
Custom Inference Provider (Isidoro/Kiais)
Configured in /opt/data/config.yaml (PVC, not GitOps):
model:
default: balanced
provider: custom
base_url: https://isidoro.grigri.cloud/v1
custom_providers:
- name: isidoro
base_url: https://isidoro.grigri.cloud/v1
API key stored in /opt/data/auth.json under pool key custom:isidoro.
Plugins and Skills
- Plugins are opt-in via
plugins.enabledlist in config.yaml. Empty = nothing bundled loads. Do not bulk-enable all plugins — only enable what's needed. - Skills are controlled by
skills.disabledlist. Removed from disabled = enabled. - Changes to
/opt/data/config.yamlsurvive pod restarts (PVC) but are not GitOps-managed.
Persistent CLI Tools
The container root filesystem is ephemeral. Install standalone tools and their configuration on
the /opt/data PVC instead of /usr/local/bin or /root. See
Persistent CLI Tools in Hermes for the persistent fj layout,
token scopes, and verification steps.
Useful Commands
# Check pod status
kubectl --context=grigri get pod/hermes-0 -n hermes -o wide
# View gateway logs
kubectl --context=grigri logs -n hermes hermes-0 --tail=50 -f
# Open interactive session
kubectl --context=grigri exec -n hermes -it hermes-0 -- /opt/hermes/.venv/bin/hermes
# Check config
kubectl --context=grigri exec -n hermes hermes-0 -- /opt/hermes/.venv/bin/hermes config show
# Health check
kubectl --context=grigri exec -n hermes hermes-0 -- /opt/hermes/.venv/bin/hermes doctor
# Restart gateway (inside pod)
kubectl --context=grigri exec -n hermes hermes-0 -- /opt/hermes/.venv/bin/hermes gateway run --replace
# Update image tag in values.yaml, then push — ArgoCD auto-syncs