# Hermes Agent — Claude-Max-Proxy + Hermes selbst, alles auf einer Maschine Repliziert den Proxy-Trick, den wir fuer ARIA nutzen: `claude-max-api-proxy` (npm) wrappt die Claude Code CLI — angemeldet mit deiner Claude-Max- Subscription — als OpenAI-kompatiblen `/v1/chat/completions`-Endpoint. [Hermes Agent](https://github.com/NousResearch/hermes-agent) (Nous Research, CLI/Desktop-Agent) redet darueber mit Claude, ohne API-Kosten — laeuft ueber die Subscription, nicht ueber Anthropic-API-Billing. **Entscheidung (Stefan):** Hermes Agent selbst laeuft auf DERSELBEN Maschine wie dieser Proxy-Stack — alles was zu Hermes gehoert auf eine Box, minimale Latenz, nur ein Rechner zum Pflegen. ARIA bleibt komplett unangetastet (eigenes Docker-Netz `hermes-net`, eigenes Claude-CLI-Login-Volume, kein gemeinsamer Container, laeuft ja ohnehin auf einer anderen Maschine als Hermes). ## Architektur ``` hermes-agent (Nous Research, network_mode: host) | HTTP, Authorization: Bearer , http://localhost:8447/v1 v hermes-gateway (Port 8447, NUR auf 127.0.0.1 gebunden) | prueft Bearer-Token, sonst 401 v hermes-proxy (Port 3456, NUR docker-intern im hermes-net erreichbar) | claude-max-api-proxy -> Claude Code CLI (Claude-Max-Login) v Anthropic (ueber deine Claude-Max-Subscription) hermes-agent-dashboard (0.0.0.0:9119, Hermes' eigenes Web-UI/Setup, per Basic-Auth gesichert seit 19.07.2026 — Stefans Entscheidung, siehe Sicherheitshinweis) ``` Warum das Gateway trotzdem noch da ist, obwohl alles auf einer Maschine laeuft: `hermes-proxy` selbst hat keine Authentifizierung (genau wie ARIAs Proxy) — jeder andere Container im selben Docker-Host koennte sonst unbemerkt Deine Claude-Max-Subscription mitbenutzen. Das Gateway (`proxy-patches/gateway.js`, pure Node, keine Extra-Deps) prueft den `Authorization: Bearer `-Header gegen ein Shared Secret, bevor irgendwas an `hermes-proxy` weitergeleitet wird. Seit "alles auf einer Maschine" bindet das Gateway nur noch auf `127.0.0.1`, nicht mehr nach aussen — kein anderer Rechner kommt mehr direkt ran. ## Was uebernommen wurde (1:1 aus ARIAs Proxy-Setup) - **Sed-Patches** an der npm-CLI-Bridge (host-binding, `--dangerously-skip-permissions`, echter `--system-prompt`-Kanal, 24h-Timeout, Prompt via stdin) — generische Infra-Fixes, nicht ARIA-spezifisch. - **Adapter-Patches** (`proxy-patches/openai-to-cli.js`, `cli-to-openai.js`) — die OpenAI-`tools` <-> ``-Text-Bruecke. Generisch, 1:1 uebernommen. - **NICHT** uebernommen: ARIAs `routes.js`-Patch (Live-Tool-Stream/Not-Aus fuers Diagnostic-UI, ARIA-Bridge-spezifisch). Stattdessen gibt's hier das eigene `gateway.js` fuer die Auth, die ARIA in der Form nicht braucht. ## Setup 1. **Hermes-Agent-Sourcecode daneben klonen** (das offizielle Docker-Image liegt nicht auf Docker Hub, sondern wird per `build:` aus dem Repo gebaut): ```bash cd /pfad/zum/deploy-ziel git clone https://github.com/NousResearch/hermes-agent.git hermes-agent-src ``` (dieser Clone ist in `.gitignore` — bleibt lokal, wandert nicht in unser Repo) 2. **`.env` anlegen:** ```bash cp .env.example .env openssl rand -hex 32 # Ausgabe in .env bei HERMES_GATEWAY_TOKEN eintragen ``` 3. **Model-Config fuer Hermes seeden** (einmalig, VOR dem ersten Start — danach fasst Hermes die Datei nicht mehr an): ```bash mkdir -p hermes-data/agent-home cp hermes-agent-config/config.yaml.example hermes-data/agent-home/config.yaml ``` Die Datei zeigt Hermes per `provider: custom` auf unseren `hermes-gateway` (`http://localhost:${HERMES_GATEWAY_PORT}/v1`) — `${HERMES_GATEWAY_PORT}`/`${HERMES_GATEWAY_TOKEN}` werden von Hermes selbst beim Config-Laden aus den Container-Env-Vars expandiert, die kommen aus Deiner `.env`. Kein manuelles Ausfuellen noetig. 4. **Dashboard-Auth einrichten** (Pflicht seit 19.07.2026 — `hermes-agent-dashboard` laeuft mit `--host 0.0.0.0` und Hermes verweigert diesen Bind hart, wenn kein Auth-Provider registriert ist: `Refusing to bind dashboard to 0.0.0.0 — the auth gate engages on non-loopback binds, but no auth providers are registered`. Ohne diesen Schritt kommt der Dashboard- Container beim naechsten Schritt gar nicht hoch): ```bash # HERMES_DASHBOARD_USER in .env ist Klartext, HERMES_DASHBOARD_PASSWORD_HASH # ist NIEMALS das Klartext-Passwort, sondern der Hash daraus. Erzeugen kannst # Du den erst NACHDEM Schritt 5 unten (docker compose up -d) mindestens # hermes-agent hochgefahren hat -- gegen "hermes-agent", NICHT # "hermes-agent-dashboard" (der braucht den Hash schon zum Start, Henne-Ei): docker exec -it hermes-agent python -c \ "from plugins.dashboard_auth.basic import hash_password; print(hash_password('DEIN-PASSWORT'))" ``` **ACHTUNG DOLLARZEICHEN-FALLE:** der Hash hat das Format `pbkdf2:sha256:$$` — zwei rohe `$`-Zeichen drin. Docker Compose interpretiert `$xyz` in `.env`-Werten selbst als Variablen- Referenz und ersetzt nicht aufloesbare Teile stillschweigend durch Leerstring (sichtbar als `WARN The "xyz" variable is not set` beim `docker-compose up`). Ohne Escaping kommt im Container ein verstuemmelter Hash an und das Dashboard bleibt bei "no auth providers are registered" haengen, obwohl alles "richtig" eingetragen aussieht. **Jedes `$` im Hash durch `$$` ersetzen**, bevor Du ihn in `.env` eintraegst (z.B. `260000$abc$def` → `260000$$abc$$def`). Anfuehrungszeichen um den Wert helfen dabei NICHT — Compose expandiert `$` in `.env`-Werten unabhaengig davon ob sie gequotet sind. Am einfachsten den Hash direkt per `sed` in der Zeile escapen, statt ihn von Hand nochmal abzutippen (verdoppelt NUR die `$` in genau dieser einen Zeile): ```bash sed -i '/^HERMES_DASHBOARD_PASSWORD_HASH=/ s/\$/$$/g' .env ``` Zur Kontrolle, ob noch unverdoppelte `$` uebrig sind (sollte leer sein): ```bash grep -o '\$[^$]' .env ``` Output (den escapeten Hash, nicht das Passwort) in `.env` bei `HERMES_DASHBOARD_PASSWORD_HASH` eintragen, `HERMES_DASHBOARD_USER` nach Belieben setzen (Default `admin`). Der `dashboard.basic_auth`-Block dazu liegt schon fertig in `hermes-agent-config/config.yaml.example` (Platzhalter, die Hermes selbst aus den Container-Env-Vars expandiert — kein manuelles Ausfuellen noetig, siehe Schritt 3). **WICHTIG, wenn Du Schritt 3 schon VOR dem 19.07.2026 gemacht hast:** Schritt 3 kopiert `config.yaml.example` nur EINMALIG nach `hermes-data/agent-home/config.yaml` — kein automatischer Re-Sync danach. Der `dashboard:`-Block wurde erst nachtraeglich in die `.example`-Datei ergaenzt, landet also NICHT von selbst in einer bereits existierenden `config.yaml`. Ohne ihn bleibt Hermes bei "no auth providers are registered" haengen, egal wie sauber `.env` escaped ist (weil Hermes den Block ja gar nicht sieht). Pruefen: ```bash grep -A3 '^dashboard:' hermes-data/agent-home/config.yaml ``` Kommt nichts zurueck → Block manuell anhaengen: ```bash cat >> hermes-data/agent-home/config.yaml <<'EOF' dashboard: basic_auth: username: "${HERMES_DASHBOARD_USER}" password_hash: "${HERMES_DASHBOARD_PASSWORD_HASH}" EOF ``` Danach neu hochziehen: `docker compose up -d --build hermes-agent-dashboard` 5. **TLS-Reverse-Proxy (Caddy) einrichten — macht Port 443 von aussen erreichbar, Dashboard bleibt intern auf `127.0.0.1`:** a. Selbstsigniertes Zertifikat, 100 Jahre gueltig (bewusst kein Let's Encrypt — kein Domain-/Port-80-Zwang, kein Renewal je wieder noetig). **Erzeugt sich automatisch** — kein manueller Schritt mehr noetig: der `caddy`-Container prueft beim Start, ob in `hermes-data/caddy-certs/` schon ein Zertifikat liegt, und wenn nicht, installiert er sich kurz `openssl` (per `apk`) und erzeugt es selbst (siehe `command:`-Block im `caddy`-Service in `docker-compose.yml`). Einmalig generiert, danach persistent im Volume — ueberlebt `docker compose up`/Neustarts, wird nicht bei jedem Start neu gebaut. Falls Du's doch manuell/mit eigenem `/CN` (z.B. Server-IP) erzeugen willst, geht das weiterhin von Hand — dann einfach VOR dem ersten Hochziehen selbst nach `hermes-data/caddy-certs/dashboard.{crt,key}` legen, der Auto-Generate-Check ueberspringt die Erzeugung dann: ```bash mkdir -p hermes-data/caddy-certs openssl req -x509 -nodes -newkey rsa:4096 \ -keyout hermes-data/caddy-certs/dashboard.key \ -out hermes-data/caddy-certs/dashboard.crt \ -days 36500 \ -subj "/CN=hermes-dashboard" ``` b. Caddy-Basic-Auth-Hash erzeugen (EIGENER Hash, nicht derselbe wie bei Schritt 4 — Caddy will bcrypt, Hermes' Hash ist pbkdf2): ```bash docker run --rm caddy:2-alpine caddy hash-password --plaintext 'DEIN-PASSWORT' ``` Output in `.env` bei `CADDY_BASIC_AUTH_HASH` eintragen, danach — **gleiche Dollarzeichen-Falle wie bei Schritt 4** (bcrypt-Hashes haben auch rohe `$`-Zeichen drin) — escapen: ```bash sed -i '/^CADDY_BASIC_AUTH_HASH=/ s/\$/$$/g' .env ``` `HERMES_DASHBOARD_USER` aus Schritt 4 wird wiederverwendet (dasselbe Login gilt fuer beide Auth-Schichten). 6. **Stack starten:** ```bash docker compose up -d ``` Baut beim ersten Mal das Hermes-Image aus `hermes-agent-src` (dauert ein paar Minuten), startet dann `hermes-proxy`, `hermes-gateway`, `hermes-agent` (Gateway/Messaging-Prozess), `hermes-agent-dashboard` und `caddy`. Falls `HERMES_DASHBOARD_PASSWORD_HASH` oder `CADDY_BASIC_AUTH_HASH` noch leer sind: der jeweilige Service bricht beim Start ab, die anderen laufen trotzdem — Werte nachtragen, dann `docker compose up -d hermes-agent-dashboard caddy`. 7. **Einmalig Claude-CLI-Login** (oeffnet Browser-OAuth mit deiner Claude-Max-Subscription): ```bash docker exec -it hermes-proxy claude ``` Danach liegen die Credentials persistent in `hermes-data/claude-config/` — ueberlebt Container-Restarts. 8. **Testen, dass der Proxy laeuft:** ```bash curl -s http://127.0.0.1:${HERMES_GATEWAY_PORT:-8447}/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{"model":"sonnet","messages":[{"role":"user","content":"Sag Hallo"}]}' ``` Ohne oder mit falschem Bearer-Token gibt's `401 unauthorized`. 9. **Mit Hermes chatten:** ```bash docker exec -it hermes-agent hermes chat ``` oder das Dashboard im Browser oeffnen: `https://` (Port 443, kein `:9119` mehr — der ist jetzt nur noch intern). Browser meldet beim ersten Aufruf "Zertifikat nicht vertrauenswuerdig" (weil selbstsigniert, nicht von einer oeffentlichen CA) — einmalig als Ausnahme akzeptieren ("Trotzdem fortfahren" o.ae.), danach fragt Caddy den Basic-Auth-Login aus Schritt 5b ab. Traffic ist ab hier durchgehend TLS-verschluesselt, kein SSH-Tunnel mehr noetig. ## Mobiler Client (Android)? Zwei GRUNDVERSCHIEDENE Richtungen, nicht verwechseln: - **Unser Proxy oben** (`hermes-gateway` -> `hermes-proxy`): Claude ALS Backend FUER Hermes. Kein Client redet direkt damit ausser Hermes selbst. - **Hermes' EIGENER OpenAI-kompatibler API-Server** (`API_SERVER_HOST` / `API_SERVER_KEY`, auskommentiert im `hermes-agent`-Service): Hermes ALS Server FUER einen Chat-Client. Aktivieren + `API_SERVER_KEY` in `.env` setzen, dann kann ein Client wie **gpt_mobile** oder **Maid** (Open Source, Android, freier `base_url`+Key, kein Messenger — siehe Chat-Verlauf) sich direkt gegen Hermes verbinden. Fuer sowas wie die ARIA-App (Wake-Word, Sprachsteuerung, TTS) braeuchte man dagegen einen eigenen App-Build — kein fertiges Produkt zum Runterladen. ## Sicherheitshinweis `hermes-gateway` bindet nur auf `127.0.0.1` — von aussen kommt niemand mehr ran, solange Du nicht selbst was aendertst (z.B. fuer den mobilen API-Server oben, oder falls Hermes doch mal auf eine zweite Maschine zieht). `hermes-agent-dashboard` bindet seit dem 19.07.2026, spaeter Abend, wieder auf `127.0.0.1` — von aussen nicht mehr direkt erreichbar. Davor steht seitdem `caddy` (Service in `docker-compose.yml`, Config in `Caddyfile`): terminiert TLS auf Port 443 mit einem selbstsignierten 100-Jahre-Zertifikat (Setup-Schritt 5a) und erzwingt eine EIGENE Basic-Auth-Schicht (Setup-Schritt 5b, eigener bcrypt-Hash in `CADDY_BASIC_AUTH_HASH` — nicht derselbe wie Hermes' pbkdf2-Hash). Zwei Gruende fuer die doppelte Auth statt nur auf Hermes' eigene zu vertrauen: 1. Hermes' Auth-Gate "engagiert" laut eigener Fehlermeldung nur bei einem 0.0.0.0-Bind — auf 127.0.0.1 (das Caddy anspricht) ist nicht garantiert, dass `dashboard.basic_auth` noch durchgesetzt wird. 2. Selbst wenn doch: zwei unabhaengige Schichten sind ohnehin robuster als eine, falls in Zukunft mal eine davon falsch konfiguriert wird. Das war vorher (kurzzeitig, testweise) `0.0.0.0` OHNE TLS — Passwort ging als Basic-Auth im Klartext raus. Ist jetzt behoben: einziger von aussen offene Port ist 443, durchgehend verschluesselt. Das Zertifikat ist selbstsigniert (keine oeffentliche CA dahinter) — Browser zeigen beim ersten Aufruf eine Warnung, die Du einmalig als Ausnahme akzeptierst (Details siehe Setup-Schritt 9). Wer eine "echte", warnungsfreie CA-Kette will, braucht dafuer eine oeffentliche Domain + Let's Encrypt — war hier bewusst nicht der Plan. Generell gilt: wenn Du `HERMES_GATEWAY_PORT`, `API_SERVER_HOST` oder den Dashboard-Port (`9119`) doch mal direkt nach aussen exposest (statt ueber Caddy), ist der Traffic unverschluesseltes HTTP, kein TLS. Nicht direkt ins offene Internet haengen — SSH-Tunnel, WireGuard/Tailscale oder eben den Caddy-Reverse-Proxy davorschalten. ## Verzeichnisse (nicht committet, siehe .gitignore) - `hermes-agent-src/` — geklonter Hermes-Agent-Sourcecode (Docker-Build-Context) - `hermes-data/claude-config/` — Claude-CLI-Login (Browser-OAuth-Session) - `hermes-data/agent-home/` — Hermes' `~/.hermes`-Aequivalent: Config (`config.yaml`, aus `hermes-agent-config/config.yaml.example` geseedet), Sessions, Memory, Skills, `.env` - `hermes-data/caddy-certs/` — selbstsigniertes 100-Jahre-Zertifikat (`dashboard.crt`/`dashboard.key`, Setup-Schritt 5a) — Private Key, niemals committen - `hermes-data/caddy-data/`, `hermes-data/caddy-config/` — Caddys eigener Runtime-State (nicht von Dir angefasst) - `.env` — Secrets (Token, ggf. API_SERVER_KEY, Basic-Auth-Hashes) ## Wo deployen? Diese Compose-Datei ist ortsunabhaengig. Stefan deployt sie selbst auf der Ziel-Maschine (nicht aria-wohnung — komplett getrennt von ARIA). Repo klonen, die drei Setup-Schritte oben (Hermes-Source klonen, `.env`, Config seeden), dann `docker compose up -d`.