From 71775891b494efa82d27dda192a358837d8427d1 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:50:23 +0200 Subject: [PATCH] Add Hermes WebUI and Athena operator skill --- compose.yaml | 65 ++++++++++ config/install.env.example | 4 + docs/COMPONENTS.md | 2 + docs/CURRENT_REFERENCE.md | 18 +++ docs/DISASTER_RECOVERY.md | 5 + docs/VPN_SERVICE_PORTS.md | 1 + install.sh | 9 ++ platform/docker/hermes-webui/Dockerfile | 9 ++ .../hermes-webui/hermes_requirements.py | 35 ++++++ .../docker/hermes-webui/patch_entrypoint.py | 22 ++++ platform/hermes/install-hermes.sh | 1 + platform/hermes/install-profiles.sh | 44 +++++++ platform/hermes/install-skills.sh | 31 +++++ platform/hermes/install-webui.sh | 85 +++++++++++++ .../hermes/skills/athena-operator/SKILL.md | 115 ++++++++++++++++++ 15 files changed, 446 insertions(+) create mode 100644 platform/docker/hermes-webui/Dockerfile create mode 100644 platform/docker/hermes-webui/hermes_requirements.py create mode 100644 platform/docker/hermes-webui/patch_entrypoint.py create mode 100755 platform/hermes/install-profiles.sh create mode 100755 platform/hermes/install-skills.sh create mode 100755 platform/hermes/install-webui.sh create mode 100644 platform/hermes/skills/athena-operator/SKILL.md diff --git a/compose.yaml b/compose.yaml index 4060d4c..2e224c8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -819,6 +819,71 @@ services: retries: 20 start_period: 45s + # Optional, fully removable community chat surface. Chat execution goes + # through the existing Hermes gateway. Upstream's container entrypoint + # requires a writable Hermes home for its ownership/init checks; UI-only + # state still remains on a separate bind mount for easy removal. + hermes-webui: + image: ${HERMES_WEBUI_IMAGE:-mike-ai/hermes-webui:0.52.113-hermes-source-v1} + container_name: mike-ai-hermes-webui + restart: unless-stopped + profiles: [hermes-webui] + env_file: + - /data/hermes-webui/.env + volumes: + - /data/hermes:/home/hermeswebui/.hermes + - /data/hermes-webui/state:/state + - /data/hermes-webui/hermes-agent:/home/hermeswebui/.hermes/hermes-agent:ro + - /data/hermes/workspace:/workspace + environment: + HERMES_HOME: /home/hermeswebui/.hermes + HERMES_WEBUI_STATE_DIR: /state + HERMES_WEBUI_HOST: 0.0.0.0 + HERMES_WEBUI_PORT: "8787" + HERMES_WEBUI_CHAT_BACKEND: gateway + HERMES_WEBUI_GATEWAY_BASE_URL: http://hermes:8642 + HERMES_API_URL: http://hermes:8642 + HERMES_WEBUI_AGENT_DIR: /home/hermeswebui/.hermes/hermes-agent + HERMES_WEBUI_GATEWAY_USE_RUNS_API: "true" + HERMES_SKIP_CHMOD: "1" + WANTED_UID: "10000" + WANTED_GID: "10000" + networks: [frontend] + depends_on: + hermes: + condition: service_healthy + security_opt: ["no-new-privileges:true"] + healthcheck: + test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8787/health', timeout=3)"] + interval: 15s + timeout: 5s + retries: 20 + start_period: 45s + + # Persistent VPN listener for the optional WebUI. Sharing the existing + # WireGuard network namespace avoids recreating the remote-access gateway + # merely to add one listener. + hermes-webui-vpn-proxy: + image: mike-ai/wireguard-gateway:local + container_name: mike-ai-hermes-webui-vpn-proxy + restart: unless-stopped + profiles: [hermes-webui] + network_mode: "service:wireguard-gateway" + entrypoint: [socat] + command: + - TCP-LISTEN:8787,bind=192.168.1.212,reuseaddr,fork + - TCP:hermes-webui:8787 + read_only: true + tmpfs: + - /tmp:size=4m,mode=1777 + cap_drop: [ALL] + security_opt: ["no-new-privileges:true"] + depends_on: + wireguard-gateway: + condition: service_healthy + hermes-webui: + condition: service_healthy + networks: frontend: internal: false diff --git a/config/install.env.example b/config/install.env.example index 42b8832..69cb9be 100644 --- a/config/install.env.example +++ b/config/install.env.example @@ -91,6 +91,10 @@ LLAMA_THREADS_BATCH=6 OPENWEBUI_IMAGE=mike-ai/openwebui:main-01f4282-tool-final-v3 OPENWEBUI_ENABLE_SIGNUP=false OPENWEBUI_ENABLE_FOLLOW_UP_GENERATION=false +# Removable community Hermes chat surface. Set false to keep only the official +# Hermes Dashboard/API and native clients. +INSTALL_HERMES_WEBUI=true +HERMES_WEBUI_IMAGE=mike-ai/hermes-webui:0.52.113-hermes-source-v1 PIPER_TTS_VERSION=1.6.0 PIPER_VOICE=de_DE-thorsten-high XTTS_IMAGE=ghcr.io/coqui-ai/xtts-streaming-server:latest-cuda121@sha256:f7fb3b1f9d4bc88af94da1b5959d8002f1e0b003c97557164034eb8a29f01b90 diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index 77d2520..2a29e04 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -7,6 +7,8 @@ | Qwen-Profile | `platform/profiles/` | vollständig, Modelle ausgenommen | Kern | | MCP-Tool-Stack | `platform/mcp/compose.yaml` | vollständig | Kern | | Hermes Agent | NousResearch Hermes Agent 0.20.5, OCI-Digest gepinnt | eigener Clientcontainer, Dashboard/API, persistente Daten unter `/data/hermes` | Kern | +| Hermes Community-WebUI | nesquena/hermes-webui 0.52.113, OCI-Digest gepinnt, kleine lokale Kompatibilitätsschicht | optionale mobile Browseroberfläche über Hermes-Gateway; installiert Abhängigkeiten aus Hermes 0.20.5 ohne dessen absichtlich gesperrten Wheel-Build; eigener Zustand unter `/data/hermes-webui` | Optional | +| Hermes Athena-Operator-Skill | `platform/hermes/skills/athena-operator/SKILL.md` | knappe, versionierte Arbeitslogik für Plattformwissen, Operator, Rollback, Verifikation, Git und Recovery; wird in alle Hermes-Profile synchronisiert | Kern | | Websuche | SearXNG + TinySearch/Crawl4AI | intern, ohne veröffentlichten Port | Kern | | Allgemeines Web | OpenWebUI native Suche; TinySearch-Upstream-MCP auf VPN-Port 8203 für andere Clients | site-unabhängig; keine neue Implementierung pro Website | Kern | | Frühere Web-MCP-Fassade | `platform/web-search/web_search_mcp.py` | nur Rollback-Profil `legacy-web` | Altbestand | diff --git a/docs/CURRENT_REFERENCE.md b/docs/CURRENT_REFERENCE.md index 6e6eecd..7c5e2de 100644 --- a/docs/CURRENT_REFERENCE.md +++ b/docs/CURRENT_REFERENCE.md @@ -101,6 +101,12 @@ Der Router übernimmt: - Standardmodell: `qwen-medium`, 160.000 Kontext, über den Profile Router - Dashboard: WireGuard-Port 9119 mit Basic-Auth - Agent-API: WireGuard-Port 8642 mit eigenem Bearer-Key +- Profile: Fast 76,8K, Medium 160K, Large 192K, Ultra 262K und Uncensored + 80K; alle verwenden dieselben MCPs, Skills, Sprach- und Sicherheitsvorgaben +- Der verwaltete Skill `athena-operator` wird aus dem Repository in das + Standardprofil und alle fünf benannten Profile synchronisiert. Er enthält + nur die verbindliche Arbeitslogik; Architektur und Ist-Zustand werden + bedarfsgerecht aus Platform-Context- und Operator-MCP gelesen. - persistenter Zustand: `/data/hermes` - lokales Terminal: ausschließlich `/data/hermes/workspace` im Container - MCPs: Athena-Plattform, Athena-Operator, allgemeines Web, GitHub, Home @@ -109,6 +115,18 @@ Der Router übernimmt: Universitätsadresse - Start verweigert, wenn die verwaltete Konfiguration nicht lesbar ist oder nicht ausdrücklich `custom` und den lokalen Router als Provider nennt +- Optionale Community-WebUI 0.52.113 auf WireGuard-Port 8787: eigener + Container und eigener Zustand unter `/data/hermes-webui`; Chats laufen über + die vorhandene Hermes-Gateway-API. Der vom Upstream-Entrypoint benötigte + gemeinsame Hermes-Home-Mount ist beschreibbar; UI-eigener Zustand bleibt + davon getrennt. Änderungen in WebUI-Einstellungen wirken daher bewusst auf + die zentrale Hermes-Konfiguration. Eine schreibgeschützte Kopie des exakt + gepinnten Hermes-Agent-Codes liegt unter `/data/hermes-webui/hermes-agent`, + damit Modell-, Skill- und Sitzungsfunktionen nicht im reduzierten Modus + laufen; der Installer erneuert sie nur bei geändertem Hermes-Image. Der + kleine Container `mike-ai-hermes-webui-vpn-proxy` teilt ausschließlich den + Netzwerk-Namespace des WireGuard-Gateways und hält Port 8787 rebootfest, + ohne das Gateway für Installation oder Entfernung neu zu erstellen. ## Vision diff --git a/docs/DISASTER_RECOVERY.md b/docs/DISASTER_RECOVERY.md index 0bd6161..d38e4ae 100644 --- a/docs/DISASTER_RECOVERY.md +++ b/docs/DISASTER_RECOVERY.md @@ -101,6 +101,11 @@ laufen, sondern alle fachlichen Funktionen geprüft wurden. - [ ] Athena Operator und rootseitiger Executor gesund; Lesen und Preview erfolgreich; falsches/abgelaufenes Ticket, Pfadausbruch, WireGuard-Stopp, freie Befehle, SSH, Reboot und Shutdown in Negativtests verweigert +- [ ] Hermes-Profile Fast, Medium, Large, Ultra und Uncensored vorhanden; + `athena-operator` liegt im Standardprofil und in allen fünf Profilen +- [ ] falls `INSTALL_HERMES_WEBUI=true`: Community-WebUI auf VPN-Port 8787 + gesund, Chat-Backend ist das bestehende Hermes-Gateway und weder Hermes + noch das aktive Qwen-Profil wurde dafür neu gestartet - [ ] lokales Dokumentations-Overlay ist auch im privaten Git enthalten und der Recovery-Koffer wurde danach neu erzeugt diff --git a/docs/VPN_SERVICE_PORTS.md b/docs/VPN_SERVICE_PORTS.md index c66d8fd..fed61b2 100644 --- a/docs/VPN_SERVICE_PORTS.md +++ b/docs/VPN_SERVICE_PORTS.md @@ -17,6 +17,7 @@ Aktuelle VPN-Adresse: `192.168.1.212` | 8092 | XTTS direkt | `http://192.168.1.212:8092` | | 9119 | Hermes Dashboard | `http://192.168.1.212:9119` | | 8642 | Hermes Agent API | `http://192.168.1.212:8642` | +| 8787 | optionale Hermes Community-WebUI | `http://192.168.1.212:8787` | | 8201 | Athena Platform Context MCP | `http://192.168.1.212:8201/mcp` | | 8202 | Athena Operator MCP einschließlich Terminal | `http://192.168.1.212:8202/mcp` | | 8203 | Allgemeiner TinySearch-MCP | `http://192.168.1.212:8203/mcp` | diff --git a/install.sh b/install.sh index 5416732..4fd7886 100755 --- a/install.sh +++ b/install.sh @@ -316,6 +316,7 @@ OPENWEBUI_IMAGE=${OPENWEBUI_IMAGE:-mike-ai/openwebui:main-01f4282-tool-final-v3} OPENWEBUI_ENABLE_SIGNUP=${OPENWEBUI_ENABLE_SIGNUP:-false} OPENWEBUI_ENABLE_FOLLOW_UP_GENERATION=${OPENWEBUI_ENABLE_FOLLOW_UP_GENERATION:-false} HERMES_IMAGE=${HERMES_IMAGE:-nousresearch/hermes-agent@sha256:143bdb9086bb2db645346179f11091e621ef6b7f4f9e5049ae7454bfeb3a0495} +HERMES_WEBUI_IMAGE=${HERMES_WEBUI_IMAGE:-mike-ai/hermes-webui:0.52.113-hermes-source-v1} PIPER_TTS_VERSION=${PIPER_TTS_VERSION:-1.6.0} PIPER_VOICE=${PIPER_VOICE:-de_DE-thorsten-high} XTTS_IMAGE=${XTTS_IMAGE:-ghcr.io/coqui-ai/xtts-streaming-server:latest-cuda121@sha256:f7fb3b1f9d4bc88af94da1b5959d8002f1e0b003c97557164034eb8a29f01b90} @@ -531,6 +532,14 @@ PY printf '%s\n' "$activation_output" grep -Fxq 'INSTALL_READINESS_OK' <<<"$activation_output" || \ die "Medium-Standardprofil lieferte keinen bestätigten Readiness-Marker" + + log "Hermes-Profile aus der Standardmatrix anlegen" + "$STACK_DIR/platform/hermes/install-profiles.sh" + + if [[ ${INSTALL_HERMES_WEBUI:-true} == true ]]; then + log "Entfernbare Hermes Community-WebUI installieren" + "$STACK_DIR/platform/hermes/install-webui.sh" + fi } hostnamectl set-hostname "$AI_HOSTNAME" diff --git a/platform/docker/hermes-webui/Dockerfile b/platform/docker/hermes-webui/Dockerfile new file mode 100644 index 0000000..3a5f4c1 --- /dev/null +++ b/platform/docker/hermes-webui/Dockerfile @@ -0,0 +1,9 @@ +FROM ghcr.io/nesquena/hermes-webui@sha256:48ba6ee4a837079955c00e997b751065cc0324ae6eaa0f2fec592c8f4b2a746e + +COPY hermes_requirements.py /usr/local/bin/hermes_requirements.py +COPY patch_entrypoint.py /tmp/patch_entrypoint.py + +RUN python3 /tmp/patch_entrypoint.py /hermeswebui_init.bash \ + && python3 /tmp/patch_entrypoint.py /apptoo/docker_init.bash \ + && rm /tmp/patch_entrypoint.py \ + && chmod 0555 /usr/local/bin/hermes_requirements.py diff --git a/platform/docker/hermes-webui/hermes_requirements.py b/platform/docker/hermes-webui/hermes_requirements.py new file mode 100644 index 0000000..488d1bf --- /dev/null +++ b/platform/docker/hermes-webui/hermes_requirements.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Export Hermes dependencies without attempting to build Hermes itself.""" + +from __future__ import annotations + +import re +import sys +import tomllib +from pathlib import Path + + +def main() -> None: + document = tomllib.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + project = document["project"] + optional = project.get("optional-dependencies", {}) + pending = list(project.get("dependencies", [])) + list(optional.get("all", [])) + emitted: set[str] = set() + + while pending: + requirement = pending.pop(0) + match = re.fullmatch(r"hermes-agent(?:\[([^]]+)\])?", requirement.strip()) + if match: + extras = [item.strip() for item in (match.group(1) or "").split(",") if item.strip()] + for extra in extras: + if extra not in optional: + raise SystemExit(f"Unknown Hermes extra: {extra}") + pending[:0] = optional[extra] + continue + if requirement not in emitted: + print(requirement) + emitted.add(requirement) + + +if __name__ == "__main__": + main() diff --git a/platform/docker/hermes-webui/patch_entrypoint.py b/platform/docker/hermes-webui/patch_entrypoint.py new file mode 100644 index 0000000..30f56fa --- /dev/null +++ b/platform/docker/hermes-webui/patch_entrypoint.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Patch WebUI dependency bootstrap for modern non-wheel Hermes releases.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +path = Path(sys.argv[1]) +content = path.read_text(encoding="utf-8") +old = ''' uv pip install "$_stage_src[all]" --trusted-host pypi.org --trusted-host files.pythonhosted.org \\ + || error_exit "Failed to install hermes-agent's requirements"''' +new = ''' python3 /usr/local/bin/hermes_requirements.py "$_stage_src/pyproject.toml" \\ + > /tmp/hermes-agent-requirements.txt \\ + || error_exit "Failed to export hermes-agent's requirements" + uv pip install -r /tmp/hermes-agent-requirements.txt \\ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \\ + || error_exit "Failed to install hermes-agent's requirements"''' +if content.count(old) != 1: + raise SystemExit(f"Expected dependency bootstrap exactly once in {path}") +path.write_text(content.replace(old, new), encoding="utf-8") diff --git a/platform/hermes/install-hermes.sh b/platform/hermes/install-hermes.sh index b1d425f..2ca2fbb 100755 --- a/platform/hermes/install-hermes.sh +++ b/platform/hermes/install-hermes.sh @@ -59,4 +59,5 @@ fi install -m 0600 "$STACK_DIR/platform/hermes/config.yaml" "$HERMES_DATA_DIR/config.yaml" install -m 0600 "$STACK_DIR/platform/hermes/SOUL.md" "$HERMES_DATA_DIR/SOUL.md" chown -R 10000:10000 "$HERMES_DATA_DIR" +"$STACK_DIR/platform/hermes/install-skills.sh" printf 'HERMES_CONFIG_OK %s\n' "$HERMES_DATA_DIR" diff --git a/platform/hermes/install-profiles.sh b/platform/hermes/install-profiles.sh new file mode 100755 index 0000000..88085ba --- /dev/null +++ b/platform/hermes/install-profiles.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +HERMES_CONTAINER=${HERMES_CONTAINER:-mike-ai-hermes} + +die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } +docker inspect "$HERMES_CONTAINER" >/dev/null 2>&1 || \ + die "Hermes-Container fehlt: $HERMES_CONTAINER" + +deadline=$((SECONDS + 180)) +until [[ $(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \ + "$HERMES_CONTAINER" 2>/dev/null || true) == healthy ]]; do + (( SECONDS < deadline )) || die "Hermes wurde nicht rechtzeitig gesund." + sleep 2 +done + +create_profile() { + local name=$1 model=$2 context=$3 description=$4 + if ! docker exec "$HERMES_CONTAINER" hermes profile show "$name" >/dev/null 2>&1; then + docker exec "$HERMES_CONTAINER" hermes profile create "$name" \ + --clone-from default --description "$description" + fi + docker exec "$HERMES_CONTAINER" hermes -p "$name" config set model.default "$model" + docker exec "$HERMES_CONTAINER" hermes -p "$name" config set model.context_length "$context" + [[ $(docker exec "$HERMES_CONTAINER" hermes -p "$name" config get model.default) == "$model" ]] || \ + die "Modellalias von Profil $name konnte nicht verifiziert werden." + [[ $(docker exec "$HERMES_CONTAINER" hermes -p "$name" config get model.context_length) == "$context" ]] || \ + die "Kontext von Profil $name konnte nicht verifiziert werden." +} + +create_profile fast qwen-fast 76800 \ + "Schnelles Qwen3.8-27B-Profil mit 76,8K Kontext fuer kurze Chats und schnelle Aufgaben." +create_profile medium qwen-medium 160000 \ + "Ausgewogenes Qwen3.8-27B-Standardprofil mit 160K Kontext fuer Alltag und agentische Aufgaben." +create_profile large qwen-large 192000 \ + "Grosses Qwen3.8-27B-Profil mit 192K Kontext fuer umfangreiche Dokumente und lange Aufgaben." +create_profile ultra qwen-ultra 262144 \ + "Maximales Qwen3.8-27B-Profil mit 262K Kontext fuer sehr grosse Kontexte; langsamer als die Standardprofile." +create_profile uncensored qwen-uncensored 80000 \ + "Unzensiertes Qwen3.8-27B-Profil mit 80K Kontext fuer spezielle Anfragen." + +"${STACK_DIR:-/opt/mike-ai/stack}/platform/hermes/install-skills.sh" +docker exec "$HERMES_CONTAINER" hermes profile list +printf 'HERMES_PROFILES_OK\n' diff --git a/platform/hermes/install-skills.sh b/platform/hermes/install-skills.sh new file mode 100755 index 0000000..829a467 --- /dev/null +++ b/platform/hermes/install-skills.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +STACK_DIR=${STACK_DIR:-/opt/mike-ai/stack} +HERMES_DATA_DIR=${HERMES_DATA_DIR:-/data/hermes} +SKILL_SOURCE=$STACK_DIR/platform/hermes/skills/athena-operator/SKILL.md +PROFILES=(fast medium large ultra uncensored) + +die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } +[[ $EUID -eq 0 ]] || die "Bitte als root ausführen." +[[ -s $SKILL_SOURCE ]] || die "Skill-Quelle fehlt: $SKILL_SOURCE" +grep -Fxq -- 'name: athena-operator' "$SKILL_SOURCE" || \ + die "Skill-Quelle hat kein gültiges Athena-Operator-Frontmatter." + +install_skill() { + local root=$1 target=$1/platform/athena-operator/SKILL.md + install -d -o 10000 -g 10000 -m 0750 "${target%/*}" + if [[ -s $target ]] && ! cmp -s "$SKILL_SOURCE" "$target"; then + cp -a "$target" "$target.before-managed-update-$(date +%Y%m%d-%H%M%S)" + fi + install -o 10000 -g 10000 -m 0640 "$SKILL_SOURCE" "$target" + cmp -s "$SKILL_SOURCE" "$target" || die "Skill-Synchronisierung fehlgeschlagen: $target" +} + +install_skill "$HERMES_DATA_DIR/skills" +for profile in "${PROFILES[@]}"; do + [[ -d $HERMES_DATA_DIR/profiles/$profile ]] || continue + install_skill "$HERMES_DATA_DIR/profiles/$profile/skills" +done + +printf 'HERMES_ATHENA_OPERATOR_SKILL_OK\n' diff --git a/platform/hermes/install-webui.sh b/platform/hermes/install-webui.sh new file mode 100755 index 0000000..84684d8 --- /dev/null +++ b/platform/hermes/install-webui.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 + +STACK_DIR=${STACK_DIR:-/opt/mike-ai/stack} +DATA_DIR=${HERMES_WEBUI_DATA_DIR:-/data/hermes-webui} +HERMES_ENV=${HERMES_ENV:-/data/hermes/.env} +SECRETS_DIR=${SECRETS_DIR:-/etc/mike-ai} +STACK_ENV=$SECRETS_DIR/stack.env +WEBUI_IMAGE_DEFAULT=mike-ai/hermes-webui:0.52.113-hermes-source-v1 + +die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } +[[ $EUID -eq 0 ]] || die "Bitte als root ausführen." +[[ -s $HERMES_ENV ]] || die "Hermes-Umgebung fehlt: $HERMES_ENV" +[[ -s $STACK_ENV ]] || die "Stack-Umgebung fehlt: $STACK_ENV" +[[ $(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \ + mike-ai-hermes 2>/dev/null || true) == healthy ]] || \ + die "Hermes-Gateway ist nicht gesund; WebUI wird nicht gestartet." +[[ $(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \ + mike-ai-wireguard-gateway 2>/dev/null || true) == healthy ]] || \ + die "WireGuard-Gateway ist nicht gesund; WebUI-VPN-Proxy wird nicht gestartet." + +read_env() { + local key=$1 + sed -n "s/^${key}=//p" "$HERMES_ENV" | head -n 1 +} + +gateway_key=$(read_env API_SERVER_KEY) +router_key=$(read_env ROUTER_API_KEY) +[[ ${#gateway_key} -ge 16 ]] || die "Hermes-Gateway-Key fehlt oder ist zu kurz." +[[ -n $router_key ]] || die "Router-Key fehlt in der Hermes-Umgebung." + +install -d -m 0700 "$DATA_DIR" "$DATA_DIR/state" + +hermes_image=$(sed -n 's/^HERMES_IMAGE=//p' "$STACK_ENV" | head -n 1) +if [[ -z $hermes_image ]]; then + hermes_image=$(docker inspect --format '{{.Config.Image}}' mike-ai-hermes 2>/dev/null || true) +fi +[[ -n $hermes_image ]] || die "Hermes-Image fehlt in $STACK_ENV und im laufenden Container." +image_id=$(docker image inspect --format '{{.Id}}' "$hermes_image" 2>/dev/null) || \ + die "Hermes-Image ist lokal nicht verfügbar: $hermes_image" +webui_image=$(sed -n 's/^HERMES_WEBUI_IMAGE=//p' "$STACK_ENV" | head -n 1) +webui_image=${webui_image:-$WEBUI_IMAGE_DEFAULT} +docker build --pull=false --tag "$webui_image" "$STACK_DIR/platform/docker/hermes-webui" + +agent_dir=$DATA_DIR/hermes-agent +agent_marker=$DATA_DIR/hermes-agent-image-id +if [[ ! -s $agent_marker || $(<"$agent_marker") != "$image_id" || ! -s $agent_dir/cli.py ]]; then + source_tmp=$(mktemp -d "$DATA_DIR/hermes-agent.new.XXXXXX") + source_container=$(docker create "$hermes_image") + cleanup_source_container() { + [[ -z ${source_container:-} ]] || docker rm -f "$source_container" >/dev/null 2>&1 || true + } + trap cleanup_source_container EXIT + docker cp "$source_container:/opt/hermes/." "$source_tmp/" + docker rm "$source_container" >/dev/null + source_container= + chown -R 10000:10000 "$source_tmp" + if [[ -e $agent_dir ]]; then + mv "$agent_dir" "$DATA_DIR/hermes-agent.previous-$(date +%Y%m%d-%H%M%S)" + fi + mv "$source_tmp" "$agent_dir" + printf '%s\n' "$image_id" >"$agent_marker" + chmod 0600 "$agent_marker" + trap - EXIT +fi + +[[ -s $SECRETS_DIR/hermes-webui-password ]] || \ + openssl rand -base64 24 >"$SECRETS_DIR/hermes-webui-password" +chmod 0600 "$SECRETS_DIR/hermes-webui-password" + +cat >"$DATA_DIR/.env" <