Root Cause fuers "geht trotzdem ans LLM": unser fast-paths-Plugin war auf pre_gateway_dispatch registriert -- der Hook feuert laut Sourcecode nur fuer die Gateway-Plattformen (Telegram/Discord/...), nicht fuer die TUI (tui_gateway/server.py), die komplett am Gateway-Dispatcher vorbei direkt in den Agent-Loop laeuft. Ein Patch an tui_gateway/server.py waere noetig gewesen, aber Concurrency-kritischer Kern-Code -- zu riskant. Hermes hat dafuer schon einen eigenen Mechanismus: quick_commands (type: exec) bypassen den Agent-Loop nachweislich auf JEDER Plattform inkl. TUI, kein LLM-Call, keine Tokens, offiziell dokumentiert. Trade-off: fester Slash-Befehl statt Freitext (/next statt "naechstes Lied") -- gleiches Prinzip wie Alexas Intent-Slots, ohne Slot-Fuellung. scripts/spotify_quick.py buendelt next/previous/pause/play/volume_up/ volume_down/current in einem Skript (ein Argument pro Aktion), nutzt denselben SpotifyClient wie der echte Tool-Dispatch. docker-compose.yml mountet scripts/ jetzt read-only nach /opt/data/scripts statt fast-paths/ nach /opt/data/plugins/fast-paths. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
554 lines
27 KiB
Markdown
554 lines
27 KiB
Markdown
# 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 <TOKEN>, 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 (EIN Container, s6 supervisiert Gateway + Dashboard als
|
|
Geschwister-Prozesse via HERMES_DASHBOARD=1):
|
|
- Gateway-Prozess: Messaging-Bruecke, redet mit Claude wie oben
|
|
- Dashboard-Prozess: 127.0.0.1:9119, Hermes' eigenes Web-UI, nur intern
|
|
erreichbar
|
|
^
|
|
| reverse_proxy (Dashboard-Port)
|
|
v
|
|
caddy (0.0.0.0:443, TLS + Basic-Auth — einzige Auth-/TLS-Schicht,
|
|
siehe Sicherheitshinweis)
|
|
```
|
|
|
|
**Wichtig, Kurskorrektur 20.07.2026:** Gateway und Dashboard liefen anfangs
|
|
in ZWEI getrennten Containern (`hermes-agent` + `hermes-agent-dashboard`,
|
|
jeweils dasselbe Image mit anderem `command:`). Das verursachte zwei reale
|
|
Bugs: Dashboard zeigte den Gateway-Status immer als "Stopped" (die Status-
|
|
Pruefung ist eine LOKALE Prozess-Inspektion im eigenen Container, nicht per
|
|
Netzwerk-Call — im getrennten Container sah sie den Gateway-Prozess nie),
|
|
und der "Restart Gateway"-Button im Dashboard scheiterte mit `no such
|
|
gateway 'default'` (der Button loest s6-Services im EIGENEN Container auf,
|
|
im Dashboard-Container gab's aber keinen). Der Startup-Log hatte den
|
|
Hinweis die ganze Zeit schon dabei: *"dashboard supervised alongside if
|
|
HERMES_DASHBOARD is set. This is the recommended setup for the s6 container
|
|
image."* Jetzt EIN `hermes-agent`-Container mit `HERMES_DASHBOARD=1` +
|
|
`HERMES_DASHBOARD_HOST=127.0.0.1` — kein separater Dashboard-Service mehr.
|
|
|
|
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 <TOKEN>`-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` <-> `<tool_call>`-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. **TLS-Reverse-Proxy (Caddy) einrichten — macht Port 443 von aussen
|
|
erreichbar, Dashboard bleibt intern auf `127.0.0.1`:**
|
|
|
|
Hermes' eigenes Dashboard-Auth-Gate (`dashboard.basic_auth` in
|
|
`config.yaml` + `HERMES_DASHBOARD_PASSWORD_HASH`) ist bewusst NICHT mehr
|
|
Teil des Setups (aufgeraeumt am 19.07.2026, spaeter Abend/20.07.2026) —
|
|
das Gate engagiert sich laut eigener Fehlermeldung ohnehin nur bei einem
|
|
`0.0.0.0`-Bind, und das Dashboard bindet fest auf `127.0.0.1`
|
|
(`HERMES_DASHBOARD_HOST` im `hermes-agent`-Service, siehe
|
|
`docker-compose.yml`). Die komplette Absicherung nach aussen laeuft
|
|
ausschliesslich ueber Caddy unten — ein Auth-Layer statt zwei, weniger
|
|
Setup-Schritte, weniger Verwirrung.
|
|
|
|
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 (bcrypt, das einzige Auth-Secret das es
|
|
jetzt noch braucht):
|
|
```bash
|
|
docker run --rm caddy:2-alpine caddy hash-password --plaintext 'DEIN-PASSWORT'
|
|
```
|
|
Output in `.env` bei `CADDY_BASIC_AUTH_HASH` eintragen, danach —
|
|
**Dollarzeichen-Falle** (bcrypt-Hashes haben rohe `$`-Zeichen drin,
|
|
Docker Compose interpretiert `$xyz` in `.env`-Werten sonst als eigene
|
|
Variablen-Referenz und ersetzt still durch Leerstring) — escapen:
|
|
```bash
|
|
sed -i '/^CADDY_BASIC_AUTH_HASH=/ s/\$/$$/g' .env
|
|
```
|
|
`HERMES_DASHBOARD_USER` aus Schritt 2 (`.env`) wird als Login-Name
|
|
wiederverwendet.
|
|
|
|
5. **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` (EIN Container, s6 supervisiert darin Gateway UND
|
|
Dashboard zusammen, siehe Architektur oben) und `caddy`. Falls
|
|
`CADDY_BASIC_AUTH_HASH` noch leer ist: `caddy` bricht beim Start ab, die
|
|
anderen laufen trotzdem — Wert nachtragen (Schritt 4b), dann
|
|
`docker compose up -d caddy`.
|
|
|
|
6. **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.
|
|
|
|
7. **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 <HERMES_GATEWAY_TOKEN aus .env>" \
|
|
-d '{"model":"sonnet","messages":[{"role":"user","content":"Sag Hallo"}]}'
|
|
```
|
|
Ohne oder mit falschem Bearer-Token gibt's `401 unauthorized`.
|
|
|
|
8. **Mit Hermes chatten:**
|
|
```bash
|
|
docker exec -it hermes-agent hermes chat
|
|
```
|
|
oder das Dashboard im Browser oeffnen: `https://<diese-maschine>` (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 4b ab. Traffic ist ab hier durchgehend TLS-verschluesselt, kein
|
|
SSH-Tunnel mehr noetig.
|
|
|
|
## Toolsets aktivieren (Spotify & Co.)
|
|
|
|
**Neu, sonst fragt Hermes nie nach Zugangsdaten und antwortet stattdessen mit
|
|
"ich hab keinen Zugriff auf Spotify/dein Handy/etc." — kein Bug, nur
|
|
unkonfiguriert:** Hermes' Toolsets (Spotify, Kalender, Browser, …) sind bei
|
|
einer frischen Installation ALLE deaktiviert, komplett Opt-in pro Plugin.
|
|
Solange nichts eingeschaltet ist, schickt Hermes gar kein `tools`-Feld an
|
|
unseren Proxy — Claude sieht ehrlich keine Tools und antwortet entsprechend
|
|
ehrlich "kann ich nicht", obwohl der Proxy selbst (`openai-to-cli.js`)
|
|
technisch laengst faehig ist, `<tool_call>`-Tools zu injizieren (genau
|
|
derselbe Mechanismus wie bei ARIA).
|
|
|
|
**Toolset einschalten** (interaktives TUI im Container):
|
|
```bash
|
|
docker exec -it hermes-agent hermes tools
|
|
```
|
|
Zum gewuenschten Plugin runterscrollen (z.B. `🎵 Spotify`), mit Leertaste an,
|
|
mit `s` speichern.
|
|
|
|
**Login/OAuth fuers Toolset:**
|
|
```bash
|
|
docker exec -it hermes-agent hermes auth spotify
|
|
```
|
|
Bei Spotify z.B. keine geteilte Drittanbieter-App — Hermes fuehrt Dich durch
|
|
das Anlegen einer eigenen minimalen Spotify-Dev-App (nur Client-ID, kein
|
|
Secret, PKCE-Flow) auf `developer.spotify.com/dashboard` inkl. der
|
|
einzutragenden Redirect-URI (`http://127.0.0.1:43827/spotify/callback`).
|
|
|
|
Der Login-Callback-Server laeuft dabei IM Container/auf der Remote-Maschine —
|
|
fuer den Abschluss im lokalen Browser einmalig ein SSH-Port-Forward
|
|
aufmachen:
|
|
```bash
|
|
ssh -N -L 43827:127.0.0.1:43827 <user>@<hermes-maschine>
|
|
```
|
|
|
|
**Kein SSH-Port-Forward moeglich?** Hermes validiert `redirect_uri` hart auf
|
|
`127.0.0.1`/`localhost` (kein oeffentlicher Callback moeglich), aber der
|
|
lokale Listener laesst sich umgehen: `scripts/spotify_manual_auth.py` baut
|
|
dieselbe PKCE-Autorisierungs-URL, wartet aber nicht auf den Callback,
|
|
sondern fragt Dich nach dem `code`-Parameter, den Du von Hand aus der
|
|
(fehlschlagenden) Redirect-URL in der Adressleiste kopierst. Das Ergebnis
|
|
landet 1:1 im selben Format in `~/.hermes/auth.json` wie beim normalen
|
|
Login.
|
|
```bash
|
|
docker cp scripts/spotify_manual_auth.py hermes-agent:/tmp/spotify_manual_auth.py
|
|
docker exec -it hermes-agent python3 /tmp/spotify_manual_auth.py
|
|
```
|
|
Details/Ablauf stehen im Docstring des Skripts. Du kannst dabei dieselbe
|
|
Spotify-Dev-App wie fuer ARIA wiederverwenden — dafuer im
|
|
[Spotify-Dashboard](https://developer.spotify.com/dashboard) bei der
|
|
bestehenden App unter *Settings → Redirect URIs* zusaetzlich
|
|
`http://127.0.0.1:43827/spotify/callback` eintragen (kein neues
|
|
Client-Secret noetig, PKCE braucht nur die Client-ID).
|
|
|
|
Hinweis: Free-Spotify-Account reicht fuer Suche/Playlists/Library, fuer
|
|
echte Playback-Steuerung (play/pause/skip auf einem Geraet) braucht's
|
|
Spotify Premium.
|
|
|
|
Gilt analog fuer jedes andere Toolset (Kalender, Browser, etc.) — immer
|
|
zuerst `hermes tools` (einschalten), dann `hermes auth <name>` falls das
|
|
Plugin OAuth/Login braucht.
|
|
|
|
### Toolset aktiviert, Claude sagt trotzdem "kenn ich nicht" / nennt eigene Skills/MCP-Server
|
|
|
|
Root Cause (20.07.2026, im echten Code + per Live-Test verifiziert, nicht
|
|
geraten): `hermes-proxy` laeuft mit einer **echten Claude-Code-CLI-Session**
|
|
(npm-Paket `claude-max-api-proxy` wrappt `@anthropic-ai/claude-code`
|
|
tatsaechlich als Subprocess). Diese Session hat ein eigenes Volume
|
|
(`./hermes-data/claude-config`), kann aber auf **demselben Anthropic-Account**
|
|
eingeloggt sein wie ARIA. Ein Account hat aber account-weite claude.ai-
|
|
Connectors (z.B. Gmail/Calendar/Drive) und Claude Codes eigenes natives
|
|
Skill-System — die werden in JEDER CLI-Session auf diesem Account automatisch
|
|
mitgeladen, auch hier. Wenn man Claude dann explizit nach einem "Skill" fragt
|
|
(z.B. "nutze Skill spotify_albums"), checkt Claude zuerst sein EIGENES,
|
|
natives Skill-/MCP-System (findet dort nichts zu Spotify, dafuer Gmail/
|
|
Calendar/Drive) und ignoriert dabei die per Prompt injizierten
|
|
`<tool_call>`-Tools (Hermes' eigentliche Plugins wie Spotify) komplett — es
|
|
sind zwei parallele, konkurrierende Tool-Konzepte in derselben Session.
|
|
|
|
**Fix:** `docker-compose.yml` startet die CLI seit `367ff64+` (Sed-Patch auf
|
|
`manager.js`) zusaetzlich mit `--safe-mode` — das deaktiviert fuer diese
|
|
Session ALLE Customizations (Skills, Plugins, MCP-Server, CLAUDE.md, ...),
|
|
eingebaute Tools (Bash/Read/...) bleiben zwar technisch an, werden aber nicht
|
|
gebraucht. Danach sieht Claude in der Hermes-Proxy-Session ausschliesslich die
|
|
per System-Prompt injizierten externen Tools und sollte sie auch fuer
|
|
"Skill"-artige Anfragen (Spotify etc.) nutzen.
|
|
|
|
```bash
|
|
git pull
|
|
docker-compose up -d --build hermes-proxy
|
|
```
|
|
|
|
Test: nach dem Neustart erneut nach einem aktivierten Toolset fragen (z.B.
|
|
"spiel meine Playlist Fliegen auf dem Handy ab") — Claude sollte jetzt einen
|
|
`<tool_call>`-Block ausgeben statt "kenn ich nicht" bzw. eigene Connectors zu
|
|
nennen. Falls es weiter hakt: `docker logs hermes-proxy --tail 100 -f`
|
|
waehrend der Reproduktion pruefen ob `--safe-mode` tatsaechlich im
|
|
gestarteten Prozess ankam (`docker exec -it hermes-proxy ps aux | grep claude`).
|
|
|
|
### Verbindungstest ohne Chat/Claude (`spotify_test_connection.py`)
|
|
|
|
Wenn Claude im Dashboard-Chat was sagt wie "Spotify-Tools sind zwar
|
|
gelistet, aber nicht verbunden/aktiv" ist unklar OB das stimmt oder ob
|
|
Claude nur raet — eine LLM-Textantwort ist kein verlaesslicher
|
|
Diagnosewert. `scripts/spotify_test_connection.py` prueft stattdessen
|
|
direkt in Hermes' eigenem Code (denselben Funktionen, die der echte
|
|
Tool-Dispatch benutzt), read-only, ohne irgendwas zu veraendern:
|
|
|
|
1. Ist das Toolset `spotify` ueber `hermes tools` fuer irgendeine
|
|
Plattform aktiviert? (`~/.hermes/config.yaml` -> `platform_toolsets`)
|
|
2. Ist Spotify eingeloggt? (`~/.hermes/auth.json` -> `providers.spotify`)
|
|
3. Funktioniert ein ECHTER API-Call mit dem gespeicherten Token
|
|
(`GET /me/player/devices`, inkl. Auto-Refresh falls abgelaufen)?
|
|
|
|
```bash
|
|
docker cp scripts/spotify_test_connection.py hermes-agent:/tmp/spotify_test_connection.py
|
|
docker exec -it hermes-agent python3 /tmp/spotify_test_connection.py
|
|
```
|
|
|
|
Sind alle drei Punkte gruen und der Chat sagt trotzdem "kenn ich nicht" /
|
|
"nicht verbunden", liegt's am Proxy bzw. am System-Prompt (siehe Abschnitt
|
|
oben), nicht an Spotify selbst — dann `docker logs hermes-proxy` waehrend
|
|
der Reproduktion pruefen. Gleiches Nutzungsmuster wie
|
|
`scripts/spotify_manual_auth.py` (Login-Skript weiter oben).
|
|
|
|
## Fast-Path ohne LLM (quick_commands)
|
|
|
|
Selbst mit allen Fixes oben braucht jede Nachricht mindestens einen vollen
|
|
LLM-Turn (mehrere Sekunden) — fuer "pause" oder "naechstes Lied" unnoetig
|
|
langsam.
|
|
|
|
**Erster Anlauf war ein eigenes Plugin** (`fast-paths/`, Regex-Hook auf
|
|
`pre_gateway_dispatch`) — das ist wieder raus. Root Cause (im echten
|
|
Hermes-Sourcecode verifiziert, nicht geraten): `pre_gateway_dispatch` feuert
|
|
NUR fuer die Gateway-Plattformen (Telegram/Discord/Slack/...), NICHT fuer die
|
|
TUI (`tui_gateway/server.py`) — die laeuft komplett am Gateway-Dispatcher
|
|
vorbei direkt in den Agent-Loop. Da wir hier fast ausschliesslich die TUI
|
|
nutzen, griff das Plugin praktisch nie; die vier `Spotify Playback`-Calls in
|
|
Folge, die Claude bei "nächstes" gemacht hat, kamen weil's doch beim LLM
|
|
landete. Ein Patch direkt in `tui_gateway/server.py` waere der einzige Weg
|
|
gewesen — aber Concurrency-kritischer Kern-Code (~700 Zeilen, History-Lock,
|
|
Busy-Queue), zu riskant fuer einen Sed-Patch.
|
|
|
|
**Stattdessen: Hermes' eigener, offizieller Mechanismus — `quick_commands`.**
|
|
Laut Sourcecode (`cli.py::process_command`, `tui_gateway/server.py`,
|
|
`gateway/run.py`) bypassen `type: exec`-Quick-Commands den kompletten
|
|
Agent-Loop — kein LLM-Call, keine Tokens — und funktionieren offiziell
|
|
dokumentiert auf JEDER Plattform (CLI/TUI, Telegram, Discord, Slack,
|
|
WhatsApp, Signal, Email, Home Assistant). Kein Patch an Hermes-Core-Dateien
|
|
noetig, nur Config + ein Skript.
|
|
|
|
**Der Trade-off (ehrlich):** quick_commands sind Slash-Befehle mit festem
|
|
Namen — kein Freitext, keine Argumente werden durchgereicht. `/next` statt
|
|
"naechstes Lied". Fuer feste Steuerbefehle reicht das genauso wie Alexas
|
|
Intent-Slots (siehe Chat-Verlauf), nur ohne Slot-Fuellung. Freitext-Faelle
|
|
wie "spiel Playlist Fliegen auf dem Handy ab" bleiben bewusst beim LLM (das
|
|
braucht ohnehin ein Modell, um den Playlist-Namen aus dem Satz zu holen).
|
|
|
|
Skript: `scripts/spotify_quick.py <next|previous|pause|play|volume_up|volume_down|current>`
|
|
— nutzt denselben `SpotifyClient` wie der echte Tool-Dispatch, gleiche
|
|
OAuth-Session, kein doppelter Token-Code. Wird read-only nach
|
|
`/opt/data/scripts/` gemountet (siehe `docker-compose.yml`).
|
|
|
|
**Einmalig einrichten** — in `hermes-data/agent-home/config.yaml` ergaenzen
|
|
(und falls noch vorhanden: den alten `plugins: enabled: [fast-paths]`-Block
|
|
entfernen):
|
|
```yaml
|
|
quick_commands:
|
|
next:
|
|
type: exec
|
|
command: python3 /opt/data/scripts/spotify_quick.py next
|
|
prev:
|
|
type: exec
|
|
command: python3 /opt/data/scripts/spotify_quick.py previous
|
|
pause:
|
|
type: exec
|
|
command: python3 /opt/data/scripts/spotify_quick.py pause
|
|
play:
|
|
type: exec
|
|
command: python3 /opt/data/scripts/spotify_quick.py play
|
|
louder:
|
|
type: exec
|
|
command: python3 /opt/data/scripts/spotify_quick.py volume_up
|
|
quieter:
|
|
type: exec
|
|
command: python3 /opt/data/scripts/spotify_quick.py volume_down
|
|
nowplaying:
|
|
type: exec
|
|
command: python3 /opt/data/scripts/spotify_quick.py current
|
|
```
|
|
Dann:
|
|
```bash
|
|
git pull
|
|
docker-compose up -d --build hermes-agent
|
|
```
|
|
Danach in der TUI `/next`, `/pause`, `/louder`, `/nowplaying` etc. testen —
|
|
sollte in Millisekunden reagieren, kein Modell-Aufruf, keine Tool-Call-Schleife
|
|
mehr moeglich (es gibt schlicht keinen LLM-Turn dafuer).
|
|
|
|
## 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).
|
|
|
|
Das Dashboard (s6-supervisierter Geschwister-Prozess im `hermes-agent`-
|
|
Container, `HERMES_DASHBOARD_HOST=127.0.0.1`) bindet fest auf `127.0.0.1` —
|
|
von aussen nicht mehr direkt erreichbar. Davor steht `caddy` (Service in
|
|
`docker-compose.yml`, Config in `Caddyfile`): terminiert TLS auf Port 443 mit
|
|
einem selbstsignierten 100-Jahre-Zertifikat (Setup-Schritt 4a) und erzwingt
|
|
eine Basic-Auth-Schicht (Setup-Schritt 4b, bcrypt-Hash in
|
|
`CADDY_BASIC_AUTH_HASH`). Caddy ist die EINZIGE Auth-/TLS-Schicht — Hermes'
|
|
eigenes Dashboard-Auth-Gate (`dashboard.basic_auth`/
|
|
`HERMES_DASHBOARD_PASSWORD_HASH`) wurde bewusst komplett rausgenommen
|
|
(aufgeraeumt 20.07.2026): es engagiert sich laut eigener Fehlermeldung
|
|
ohnehin nur bei einem 0.0.0.0-Bind, das Dashboard bindet aber fest auf
|
|
127.0.0.1 — ein zweiter, nie greifender Auth-Layer haette nur Setup-Schritte
|
|
und Verwirrung gekostet, ohne echten Sicherheitsgewinn.
|
|
|
|
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 8). 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.
|
|
|
|
## Troubleshooting
|
|
|
|
**`{"detail":"Invalid Host header. Dashboard requests must use the hostname
|
|
the server was bound to."}` beim Aufruf ueber Caddy:** Hermes' Dashboard-
|
|
Backend prueft den eingehenden `Host`-Header gegen den Host, an den es
|
|
gebunden ist (`127.0.0.1:9119`) — Schutz gegen DNS-Rebinding. Caddy reicht
|
|
per Default den ORIGINALEN Host-Header vom Browser durch (Deine
|
|
Server-IP/Domain), den lehnt Hermes dann ab. Fix ist im `Caddyfile` bereits
|
|
drin: der `reverse_proxy`-Block ueberschreibt den Host-Header explizit
|
|
(`header_up Host 127.0.0.1:9119`), bevor die Anfrage ans Dashboard geht.
|
|
Falls der Fehler trotzdem auftritt: pruefen ob Dein `Caddyfile` den
|
|
`header_up`-Block im `reverse_proxy 127.0.0.1:9119 { ... }` wirklich enthaelt
|
|
(`git pull` + `docker-compose up -d --build caddy`, kein Neubuild noetig,
|
|
Caddy liest die Config beim Start neu ein).
|
|
|
|
**"Restart Gateway"-Button im Dashboard zeigt
|
|
`✗ no such gateway 'default': register it with 'hermes profile create default'
|
|
first, or pass an existing profile name via '-p <name>'`, und/oder das
|
|
Dashboard zeigt "Gateway Status: Stopped" obwohl der Gateway laeuft:**
|
|
Root Cause war eine Architektur-Inkompatibilitaet: Gateway und Dashboard
|
|
liefen anfangs in ZWEI GETRENNTEN Containern (`hermes-agent` mit
|
|
`command: ["gateway", "run"]` + ein separater `hermes-agent-dashboard` mit
|
|
`command: ["dashboard", ...]`, beide aus demselben Image). Zwei Symptome,
|
|
eine Ursache:
|
|
|
|
- Der Gateway-Status im Dashboard kommt aus einer **lokalen Prozess-
|
|
Inspektion** (PID-Check) im eigenen Container, NICHT aus einem Netzwerk-
|
|
Call — im getrennten Dashboard-Container sah dieser Check den
|
|
Gateway-Prozess nie, daher immer "Stopped".
|
|
- Der Restart-Button ruft `hermes gateway restart` auf, das s6-Services
|
|
**im eigenen Container** aufloest. Im Dashboard-Container gab's dafuer
|
|
keinen registrierten Service — daher `no such gateway 'default'`. Der
|
|
echte Gateway-Prozess lief davon unbeeindruckt weiter.
|
|
|
|
**Fix (seit 20.07.2026, `docker-compose.yml`):** Gateway und Dashboard laufen
|
|
jetzt als s6-supervisierte Geschwister-Prozesse in EINEM `hermes-agent`-
|
|
Container (`HERMES_DASHBOARD=1` + `HERMES_DASHBOARD_HOST=127.0.0.1` im
|
|
`environment:`-Block) — genau der vom Image selbst empfohlene Weg, siehe
|
|
Startup-Log: *"dashboard supervised alongside if HERMES_DASHBOARD is set.
|
|
This is the recommended setup for the s6 container image."* Kein separater
|
|
`hermes-agent-dashboard`-Service mehr, kein `hermes profile create default`
|
|
noetig. Nach `git pull` + `docker-compose up -d --build` sollten Gateway-
|
|
Status UND Restart-Button im Dashboard den echten Zustand zeigen — bitte
|
|
nach dem Redeploy einmal ausprobieren.
|
|
|
|
Sollte der Restart-Button trotzdem noch spinnen: als Fallback von aussen
|
|
neu starten (bei s6-Auto-Restart selten noetig):
|
|
```bash
|
|
docker-compose restart hermes-agent
|
|
```
|
|
|
|
**Chat im Dashboard bricht dauerhaft mit "Chat connection interrupted (code
|
|
1006). Reconnecting..." + "events feed disconnected — tool calls may not
|
|
appear" ab, obwohl die Seite selbst (Login, Session-History) normal laedt
|
|
und `docker logs hermes-agent` waehrend des Reconnects RUHIG bleibt (kein
|
|
Guard-Token, kein Fehler, gar nichts):**
|
|
|
|
Root Cause im echten Hermes-Sourcecode gefunden (`hermes_cli/web_server.py`,
|
|
Funktion `_ws_host_origin_reason`), nicht geraten: der Host-Header-Fix von
|
|
weiter oben loest nur die **HTTP**-Seite. WebSocket-Upgrades
|
|
(`/api/ws`, `/api/pub`, `/api/events` — genau die Endpunkte hinter Chat +
|
|
Events-Feed) haben einen ZWEITEN, unabhaengigen Guard, der zusaetzlich den
|
|
`Origin`-Header prueft. Der Browser schickt als Origin immer die echte
|
|
aufgerufene Adresse (Server-IP/Domain), Hermes ist aber auf `127.0.0.1`
|
|
gebunden und lehnt jeden Origin ab, der nicht `127.0.0.1`/`localhost`/`::1`
|
|
ist -> `origin_mismatch` -> WS wird sofort mit Code 4403 geschlossen
|
|
(Browser zeigt das oft generisch als 1006).
|
|
|
|
Der Grund warum die Log-Suche vorher ins Leere lief: anders als
|
|
`/api/console` und `/api/pty` loggen `/api/ws`, `/api/pub` und `/api/events`
|
|
diese Ablehnung **nicht** — sie schliessen still. Gleicher Bug-Mechanismus
|
|
wie der Host-Header-Fehler, nur eine Ebene tiefer und ohne jede
|
|
Fehlermeldung.
|
|
|
|
**Fix (bereits im `Caddyfile`):** Origin-Header komplett entfernen statt ihn
|
|
umzuschreiben — fehlt der Header, ueberspringt Hermes den Origin-Check
|
|
komplett:
|
|
```
|
|
reverse_proxy 127.0.0.1:9119 {
|
|
header_up Host 127.0.0.1:9119
|
|
header_up -Origin
|
|
}
|
|
```
|
|
Nach `git pull`: `docker-compose up -d --build caddy` (Config-Reload reicht,
|
|
kein Neubuild von hermes-agent noetig).
|
|
|
|
## 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 4a) — 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`.
|