diff --git a/README.md b/README.md index 6c0fbac..9d085b8 100644 --- a/README.md +++ b/README.md @@ -28,15 +28,32 @@ hermes-agent (Nous Research, network_mode: host) v Anthropic (ueber deine Claude-Max-Subscription) -hermes-agent-dashboard (127.0.0.1:9119, Hermes' eigenes Web-UI/Setup, nur - intern erreichbar) +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 + | 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 @@ -94,10 +111,11 @@ aussen — kein anderer Rechner kommt mehr direkt ran. `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 der Dashboard-Container bindet fest auf `127.0.0.1` - (siehe `command:` im `hermes-agent-dashboard`-Service). Die komplette - Absicherung nach aussen laeuft ausschliesslich ueber Caddy unten — - ein Auth-Layer statt zwei, weniger Setup-Schritte, weniger Verwirrung. + `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). @@ -143,9 +161,10 @@ aussen — kein anderer Rechner kommt mehr direkt ran. ``` 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 `CADDY_BASIC_AUTH_HASH` noch leer ist: `caddy` bricht beim - Start ab, die anderen laufen trotzdem — Wert nachtragen (Schritt 4b), dann + `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): @@ -197,17 +216,19 @@ Zwei GRUNDVERSCHIEDENE Richtungen, nicht verwechseln: 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 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, der Dashboard-Container -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 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 @@ -242,38 +263,39 @@ 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 '`:** Kein Bug bei uns, -sondern eine Architektur-Inkompatibilitaet zwischen unserem Deploy-Stil und -dem Dashboard-Button. Hintergrund (im Hermes-Sourcecode nachgesehen, -`hermes_cli/service_manager.py` + `hermes_cli/container_boot.py`): +first, or pass an existing profile name via '-p '`, 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: -- Unser `hermes-agent`-Service startet mit `command: ["gateway", "run"]` - direkt als Container-Kommando. Das Image erkennt dieses Muster als - "Legacy-Start" und supervised den Prozess ueber den **statischen** - s6-Service `main-hermes` (fest im Image, siehe - `/etc/s6-overlay/s6-rc.d/`). -- Der Dashboard-Restart-Button ruft dagegen immer `hermes gateway restart` - auf (ohne `-p` = Profil `default`). Dieser Befehl sucht einen **dynamisch - registrierten** s6-Service `gateway-default` unter `/run/service/` — der - entsteht aber nur durch `hermes profile create ` (Per-Profile- - Gateway-System). Bei uns wurde das nie ausgefuehrt, `main-hermes` und - `gateway-default` sind zwei komplett getrennte Dinge im selben Image. -- Ergebnis: der Button findet "sein" `gateway-default` nicht und wirft genau - diesen Fehler — der eigentliche Gateway-Prozess (`main-hermes`) laeuft - davon voellig unbeeindruckt normal weiter. +- 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 / Workaround:** Den Dashboard-Restart-Button bei uns einfach nicht -benutzen, sondern von aussen den Container neu starten: +**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 ``` -Das genuegt fast nie: `main-hermes` laeuft unter s6-Supervision mit -Auto-Restart bei Crash (siehe Startup-Log "gateway is now running under s6 -supervision (auto-restart on crash...)"), ein manueller Restart ist im -Normalbetrieb also selten noetig. `hermes profile create default` -NICHT ausfuehren, um den Button zu "reparieren" — das wuerde einen zweiten, -parallelen Gateway-Mechanismus fuer denselben Port aufmachen und ist nicht -das, wofuer unser Single-Profile-Setup gebaut ist. ## Verzeichnisse (nicht committet, siehe .gitignore) diff --git a/docker-compose.yml b/docker-compose.yml index 87d3d06..aa0f41e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -113,12 +113,15 @@ services: - hermes-net # ─── Hermes Agent selbst (Nous Research) ─────────────────── - # Offizielle Service-Definition 1:1 aus NousResearch/hermes-agent/docker-compose.yml - # (nur umbenannt gateway->hermes-agent, dashboard->hermes-agent-dashboard, damit - # es nicht mit unserem eigenen "hermes-gateway" oben verwechselt wird — das ist - # ein GANZ ANDERES Ding: Hermes' eigener Gateway-Begriff meint die Messaging- - # Bruecke (Telegram/Discord/...) + optionalen OpenAI-API-Server, NICHT unseren - # Auth-Proxy vor Claude). + # Basiert auf der offiziellen Service-Definition aus + # NousResearch/hermes-agent/docker-compose.yml (umbenannt gateway->hermes-agent, + # damit's nicht mit unserem eigenen "hermes-gateway" oben verwechselt wird — + # das ist ein GANZ ANDERES Ding: Hermes' eigener Gateway-Begriff meint die + # Messaging-Bruecke (Telegram/Discord/...) + optionalen OpenAI-API-Server, + # NICHT unseren Auth-Proxy vor Claude). Seit 20.07.2026 EIN Container fuer + # Gateway + Dashboard zusammen (HERMES_DASHBOARD=1, siehe Kommentar unten + # direkt vorm Service) statt zwei getrennter — kein eigener + # "hermes-agent-dashboard"-Service mehr. # # VORAUSSETZUNG: der echte Hermes-Agent-Sourcecode muss danebenliegen, weil # das Image nicht auf Docker Hub existiert, sondern per `build:` aus dem Repo @@ -128,6 +131,28 @@ services: # network_mode: host wie im Original — manche Messaging-Plattformen brauchen # echte Host-Ports, und dadurch erreicht Hermes unser hermes-gateway einfach # ueber "http://localhost:${HERMES_GATEWAY_PORT}/v1" statt ueber Docker-DNS. + # Kurskorrektur 20.07.2026: KEIN separater hermes-agent-dashboard-Container + # mehr. Grund (in Hermes' eigener Docs + unserem eigenen Startup-Log + # nachgelesen -- Log-Zeile "dashboard supervised alongside if HERMES_DASHBOARD + # is set. This is the recommended setup for the s6 container image"): das + # offizielle Docker-Image kann Gateway UND Dashboard als zwei GESCHWISTER- + # Prozesse in DERSELBEN s6-Supervision, DEMSELBEN Container laufen lassen, + # gesteuert per Env-Var HERMES_DASHBOARD=1. Das ist explizit "the recommended + # setup" -- und loest genau unser strukturelles Problem: + # - Dashboard erkennt den Gateway-Status ueber LOKALE Prozess-Inspektion + # (psutil/PID-Check) im eigenen Container, NICHT ueber eine Netzwerk- + # Health-URL. Mit zwei getrennten Containern (unser alter Aufbau) sieht + # das Dashboard den Gateway-Prozess nie -> "Gateway Status: Stopped" + # obwohl der Gateway echt lief. + # - Der "Restart Gateway"-Button im Dashboard ruft intern `hermes gateway + # restart` auf, das ueber s6-svc gegen die Service-Verzeichnisse IM + # EIGENEN Container aufgeloest wird -- im getrennten Dashboard-Container + # gab's dort keinen "default"-Gateway-Service, daher der Fehler "no such + # gateway 'default'". Im kombinierten Container sollte der Button den + # echten main-hermes-Service finden. + # Kein `command:` mehr noetig (Default-CMD des Images uebernimmt "gateway + # run" + s6 kuemmert sich per HERMES_DASHBOARD=1 automatisch ums Mitstarten + # des Dashboards). hermes-agent: build: ./hermes-agent-src image: hermes-agent @@ -154,6 +179,14 @@ services: # Config-Laden aus os.environ — siehe hermes_cli/config.py _expand_env_vars). - HERMES_GATEWAY_PORT=${HERMES_GATEWAY_PORT:-8447} - HERMES_GATEWAY_TOKEN=${HERMES_GATEWAY_TOKEN:?HERMES_GATEWAY_TOKEN muss in .env gesetzt sein - siehe .env.example} + # Dashboard als s6-supervisierten Geschwister-Prozess IM SELBEN Container + # aktivieren (siehe Kommentar oben). HERMES_DASHBOARD_HOST=127.0.0.1 + # haelt den Bind lokal (Caddy ist die einzige Auth-/TLS-Schicht nach + # aussen, siehe caddy-Service unten und README) -- ohne das wuerde Hermes + # per Default auf 0.0.0.0 binden und sein eigenes Auth-Gate verlangen. + - HERMES_DASHBOARD=1 + - HERMES_DASHBOARD_HOST=127.0.0.1 + - HERMES_DASHBOARD_PORT=9119 # Optional: Hermes' EIGENER OpenAI-kompatibler API-Server, falls Du spaeter # einen mobilen Client (gpt_mobile, Maid, OpenWebUI, ...) direkt GEGEN # Hermes sprechen lassen willst (andere Richtung als unser Proxy oben! @@ -164,72 +197,34 @@ services: # - API_SERVER_KEY=${API_SERVER_KEY} command: ["gateway", "run"] - hermes-agent-dashboard: - # Gleicher Build-Kontext wie oben bei "hermes-agent" (nicht nur "image:"). - # Grund: "hermes-agent" ist kein Docker-Hub-Image, sondern wird lokal aus - # ./hermes-agent-src gebaut. Ohne eigenes build: hier versucht Compose - # beim "up" trotzdem erst einen Registry-Pull fuer dieses Image, der mit - # "pull access denied" fehlschlaegt UND (anders als beim hermes-agent- - # Service oben, der einen build:-Fallback hat) hart abbricht statt lokal - # zu bauen. Mit build: hier baut Compose denselben Kontext nochmal -- - # dank Docker-Layer-Cache praktisch instant, landet aber wieder unter - # dem Tag "hermes-agent". - build: ./hermes-agent-src - image: hermes-agent - pull_policy: build - container_name: hermes-agent-dashboard - restart: unless-stopped - network_mode: host - depends_on: - - hermes-agent - volumes: - - ./hermes-data/agent-home:/opt/data - environment: - - HERMES_UID=${HERMES_UID:-10000} - - HERMES_GID=${HERMES_GID:-10000} - # HERMES_DASHBOARD_USER wird hier NICHT fuer Hermes' eigenes Auth-Gate - # gebraucht (das Gate engagiert sich laut eigener Fehlermeldung nur bei - # einem 0.0.0.0-Bind, der Service unten bindet aber fest auf 127.0.0.1 - # per `command:`) — steht nur der Vollstaendigkeit halber mit drin, - # tatsaechlich genutzt wird die Variable von Caddy (siehe unten) fuer - # dessen Basic-Auth. Absicherung nach aussen laeuft komplett ueber Caddy. - - HERMES_DASHBOARD_USER=${HERMES_DASHBOARD_USER:-admin} - # Kurskorrektur 19.07.2026, spaeter Abend: Stefan wollte erst 0.0.0.0 - # (SSH-Tunnel machte mit SSL Probleme), jetzt aber die richtige Loesung: - # Dashboard bleibt auf 127.0.0.1 (Hermes' eigenes Auth-Gate verlangt dann - # gar keinen registrierten Provider mehr, siehe Fehlermeldung "the auth - # gate engages on non-loopback binds") und der Caddy-Reverse-Proxy unten - # macht TLS + Basic-Auth auf Port 443 nach aussen. Das ist die einzige - # Auth-Schicht die im Normalbetrieb wirklich noetig ist. - command: ["dashboard", "--host", "127.0.0.1", "--no-open"] - # ─── Caddy (TLS-Reverse-Proxy + Basic-Auth vor dem Dashboard) ────────── # Stefans Entscheidung 19.07.2026: Dashboard bleibt intern auf 127.0.0.1, # Caddy uebernimmt TLS-Termination auf Port 443 UND die Basic-Auth davor. # Caddy ist die EINZIGE Auth-/TLS-Schicht (aufgeraeumt 20.07.2026): Hermes' # eigenes Dashboard-Auth-Gate (dashboard.basic_auth) wurde komplett entfernt, # weil es sich laut eigener Fehlermeldung ohnehin nur bei einem 0.0.0.0-Bind - # engagiert — der Dashboard-Container bindet aber fest auf 127.0.0.1, das - # Gate haette hier nie gegriffen. Ein zweiter, nie greifender Auth-Layer - # haette nur Setup-Schritte gekostet, kein Sicherheitsgewinn. Nutzt - # HERMES_DASHBOARD_USER (aus .env) als Login-Name + einen eigenen bcrypt- - # Hash in CADDY_BASIC_AUTH_HASH (Caddy verlangt bcrypt, siehe .env.example - # fuer die Erzeugung). + # engagiert — der Dashboard-Bind (HERMES_DASHBOARD_HOST im hermes-agent- + # Service) bleibt aber fest auf 127.0.0.1, das Gate haette hier nie + # gegriffen. Ein zweiter, nie greifender Auth-Layer haette nur Setup- + # Schritte gekostet, kein Sicherheitsgewinn. Nutzt HERMES_DASHBOARD_USER + # (aus .env) als Login-Name + einen eigenen bcrypt-Hash in + # CADDY_BASIC_AUTH_HASH (Caddy verlangt bcrypt, siehe .env.example fuer die + # Erzeugung). # # Zertifikat: bewusst KEIN Let's Encrypt (bräuchte oeffentliche Domain + # Port-80-Challenge). Stefans Wunsch: ein selbstsigniertes Zertifikat mit # 100 Jahren Laufzeit, einmalig im Browser als Ausnahme akzeptieren, nie # wieder um Erneuerung kuemmern. Erzeugung siehe README Setup-Schritt. - # network_mode: host aus demselben Grund wie bei hermes-agent/-dashboard: - # muss 127.0.0.1:9119 (Dashboard) erreichen UND selbst auf dem Host-Port - # 443 lauschen. + # network_mode: host aus demselben Grund wie bei hermes-agent: muss + # 127.0.0.1:9119 (Dashboard, jetzt Teil des hermes-agent-Containers) + # erreichen UND selbst auf dem Host-Port 443 lauschen. caddy: image: caddy:2-alpine container_name: hermes-caddy restart: unless-stopped network_mode: host depends_on: - - hermes-agent-dashboard + - hermes-agent volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro # kein :ro mehr -- der Entrypoint unten schreibt das Zertifikat selbst