diff --git a/backend/services/maintenance.py b/backend/services/maintenance.py index 3b49526..9722ddd 100644 --- a/backend/services/maintenance.py +++ b/backend/services/maintenance.py @@ -490,12 +490,23 @@ def _summarize_hermes_commits(commits: list[dict]) -> dict: def _hermes_version(path: str) -> str | None: - """Versionsnummer aus der pyproject.toml des Hermes-Checkouts (z. B. „0.21.3“).""" + """Versionsnummer des Hermes-Checkouts: bis 0.21.x aus der pyproject.toml (z. B. „0.21.3“). Seit dem neuen + Paketmanager (Update vom 25.09.2026) steht dort „0.0.0“, und die Veröffentlichungen heißen nach dem Datum + (v2026.8.31) — dann das Datum des ausgecheckten Commits im selben Stil („2026.9.25“).""" try: text = (Path(path) / "pyproject.toml").read_text(encoding="utf-8") except OSError: return None m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE) + if m and m.group(1) != "0.0.0": + return m.group(1) + try: + datum = subprocess.run(["git", "-C", path, "log", "-1", "--format=%cs"], capture_output=True, text=True, + timeout=5).stdout.strip() + except (OSError, subprocess.SubprocessError): + datum = "" + if re.fullmatch(r"\d{4}-\d{2}-\d{2}", datum): + return ".".join(str(int(t)) for t in datum.split("-")) return m.group(1) if m else None @@ -690,16 +701,33 @@ def _hermes_update_cmd() -> str: # bei uns der Normalfall. Die &&-Kette brach ab (kein doctor, kein /hermes-ui/-Build), der # Job war rot, autoupdate.sh rollte zurück und pinnte — bei GRÜNEM Gehirn-Check (06./17.09.). # Der Neustart passiert unten ohnehin; Richter bleibt der Postcheck. + # Seit dem neuen Paketmanager von Hermes (Update vom 25.09.2026): + # • Hermes startet über seinen eigenen Starter (.hermes/bin/hermes, eigene Umgebung mit Python 3.14); das alte + # venv bleibt nur für den Rückweg. Der Updater schreibt dabei die Unit neu → daemon-reload vor dem Neustart, + # sonst lief das Gateway weiter aus dem alten venv (neuer Code, alte Pakete). + # • python-olm (Matrix-Extra) baut nur mit gcc (das mitgelieferte Python ist mit clang gebaut) und mit der + # CMake-Kompatibilität für CMake 4. + # • `doctor` endet mit 1, sobald es Hinweise gibt (fehlende optionale API-Schlüssel) — das ist kein Fehler. + # • hermes-plugin-deps.sh legt die Pakete unserer Plugins (trafilatura für mc2-web-lesen) nach. + starter = os.path.join(path, ".hermes", "bin", "hermes") + hermes = starter if os.path.exists(starter) else f"{py} -m hermes_cli.main" + bauen = "CC=gcc CXX=g++ CMAKE_POLICY_VERSION_MINIMUM=3.5" + plugin_deps = os.path.join(_REPO_ROOT, "deploy", "hermes-plugin-deps.sh") return ("RC=0; " + f"ALT=$(git -C {path} rev-parse HEAD 2>/dev/null); " f"bash {backup} || true; " f"systemctl --user stop hermes-builtin-ui || true; " - f"{{ cd {path} && {py} -m hermes_cli.main update --yes --no-gateway-restart " - f"&& {py} -m hermes_cli.main doctor " + f"{{ cd {path} && {bauen} {hermes} update --yes --no-gateway-restart " + f"&& {{ {bauen} {hermes} doctor || echo '(doctor meldet Hinweise — Richter bleibt der Gehirn-Check)'; }} " f"&& {build_cmd}; }} || RC=1; " + f"bash {plugin_deps} || true; " + f"systemctl --user daemon-reload; " f"systemctl --user reset-failed hermes-builtin-ui 2>/dev/null; " f"systemctl --user restart hermes-gateway hermes-builtin-ui || RC=1; " f"sleep 6; bash {postcheck} || RC=1; " - f"if [ $RC -ne 0 ]; then echo '✗ HERMES-UPDATE FEHLGESCHLAGEN (Dienste laufen wieder, aber alter Stand)'; fi; " + f"if [ $RC -ne 0 ]; then if [ \"$(git -C {path} rev-parse HEAD 2>/dev/null)\" != \"$ALT\" ]; then " + f"echo '✗ HERMES-UPDATE UNVOLLSTÄNDIG (neuer Code, der Rest scheiterte — Dienste laufen, Protokoll prüfen)'; " + f"else echo '✗ HERMES-UPDATE FEHLGESCHLAGEN (Dienste laufen wieder, aber alter Stand)'; fi; fi; " f"exit $RC") diff --git a/backend/tests/test_wartung.py b/backend/tests/test_wartung.py index 22aac5b..508e33b 100644 --- a/backend/tests/test_wartung.py +++ b/backend/tests/test_wartung.py @@ -68,6 +68,33 @@ def test_hermes_version_auch_wenn_der_abruf_haengt(monkeypatch, tmp_path): assert "error" in info +def test_hermes_version_nach_dem_neuen_paketmanager(monkeypatch, tmp_path): + """25.09.2026: In der pyproject.toml steht „0.0.0“, die Tags heißen nach dem Datum — gezeigt wird das Datum des + ausgecheckten Commits (die Übersicht zeigte „v0.0.0“).""" + (tmp_path / "pyproject.toml").write_text('[project]\nversion = "0.0.0"\n', encoding="utf-8") + monkeypatch.setattr(maintenance.subprocess, "run", lambda befehl, **kw: subprocess.CompletedProcess( + befehl, 0, stdout="2026-09-25\n" if "log" in befehl else "", stderr="")) + assert maintenance._hermes_version(str(tmp_path)) == "2026.9.25" + monkeypatch.setattr(maintenance.subprocess, "run", lambda befehl, **kw: subprocess.CompletedProcess( + befehl, 128, stdout="", stderr="kein git")) + assert maintenance._hermes_version(str(tmp_path)) == "0.0.0" + + +def test_hermes_update_mit_neuem_starter(monkeypatch, tmp_path): + """Nach dem Update vom 25.09.2026: eigener Starter, Build-Umgebung für python-olm, daemon-reload vor dem + Neustart, doctor-Hinweise nicht fatal, Plugin-Pakete nachlegen, ehrliche Meldung bei halbem Stand.""" + monkeypatch.setattr(maintenance.system, "find_hermes_agent_git", lambda: {"path": str(tmp_path)}) + alt = maintenance._hermes_update_cmd() + assert f"{tmp_path.as_posix()}/venv/bin/python -m hermes_cli.main update" in alt.replace("\\", "/") + (tmp_path / ".hermes" / "bin").mkdir(parents=True) + (tmp_path / ".hermes" / "bin" / "hermes").write_text("#!/bin/sh\n", encoding="utf-8") + cmd = maintenance._hermes_update_cmd().replace("\\", "/") + assert f"CC=gcc CXX=g++ CMAKE_POLICY_VERSION_MINIMUM=3.5 {tmp_path.as_posix()}/.hermes/bin/hermes update --yes" in cmd + assert "doctor || echo" in cmd and "hermes-plugin-deps.sh || true" in cmd + assert cmd.index("daemon-reload") < cmd.index("systemctl --user restart hermes-gateway") + assert "HERMES-UPDATE UNVOLLSTÄNDIG" in cmd and "aber alter Stand" in cmd + + def test_update_per_knopf_landet_im_verlauf(monkeypatch): """Einzel-Updates und „Alle aktualisieren“ schreiben ins strukturierte Verlauf-Format; bei einer gescheiterten Kette zeigt die letzte Teil-Marke, welcher Teil scheiterte.""" diff --git a/deploy/hermes-plugin-deps.sh b/deploy/hermes-plugin-deps.sh new file mode 100644 index 0000000..dd6749b --- /dev/null +++ b/deploy/hermes-plugin-deps.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# hermes-plugin-deps.sh — die Pakete unserer Hermes-Plugins in Hermes' aktive Umgebung legen (seit 25.09.2026). +# +# Seit seinem neuen Paketmanager (`hermes pm`) läuft Hermes in einer eigenen Umgebung unter +# ~/.hermes/installs//environments//venv (Python aus ~/.hermes/tools). Jedes Update kann sie neu bauen — +# ohne unsere Zusätze. mc2-web-lesen braucht trafilatura (ohne liest es Seiten nur grob). +# Welche Umgebung aktiv ist, sagt Hermes' eigener Starter (hermes_bootstrap). Ohne Paketmanager: nichts zu tun. +# Läuft nach jedem Hermes-Update (maintenance._hermes_update_cmd); von Hand: bash deploy/hermes-plugin-deps.sh +set -uo pipefail +HA="${HERMES_AGENT:-$HOME/.hermes/hermes-agent}" +PY="$(ls -d "$HOME"/.hermes/tools/python-3*/bin/python3 2>/dev/null | sort -V | tail -n 1)" +UV="$(ls -d "$HOME"/.hermes/tools/uv-*/uv 2>/dev/null | sort -V | tail -n 1)" +PAKETE=(trafilatura) + +if [ ! -x "$PY" ] || [ ! -x "$UV" ] || [ ! -f "$HA/hermes_bootstrap.py" ]; then + echo "hermes-plugin-deps: Hermes ohne eigenen Paketmanager — nichts zu tun." + exit 0 +fi +UMGEBUNG="$(cd /tmp && HERMES_HOME="$HOME/.hermes" "$PY" -I -c " +import sys +sys.path.insert(0, '$HA') +import hermes_bootstrap +print(next(p for p in sys.path if p.endswith('site-packages') and '/environments/' in p).split('/lib/')[0]) +" 2>/dev/null)" +if [ -z "$UMGEBUNG" ] || [ ! -x "$UMGEBUNG/bin/python" ]; then + echo "hermes-plugin-deps: Hermes' aktive Umgebung nicht gefunden." >&2 + exit 1 +fi +if "$UMGEBUNG/bin/python" -c "import trafilatura" 2>/dev/null; then + echo "hermes-plugin-deps: alles da ($UMGEBUNG)." + exit 0 +fi +echo "hermes-plugin-deps: lege ${PAKETE[*]} in $UMGEBUNG …" +UV_CACHE_DIR="$HOME/.hermes/cache/uv" "$UV" pip install --python "$UMGEBUNG/bin/python" "${PAKETE[@]}" diff --git a/docs/UPDATES.md b/docs/UPDATES.md index 401d669..3330d6a 100644 --- a/docs/UPDATES.md +++ b/docs/UPDATES.md @@ -38,6 +38,16 @@ flowchart TD 20 Minuten. Der Job: Sicherung → Dashboard stoppen → `hermes update --yes --no-gateway-restart` → `hermes doctor` → Dashboard-Oberfläche mit Basis `/hermes-ui/` bauen → `hermes-gateway` und `hermes-builtin-ui` neu starten → `hermes-postcheck.sh`. + Seit dem Update vom 25.09.2026 hat Hermes einen eigenen Paketmanager (`hermes pm`): Das Gateway startet über + `~/.hermes/hermes-agent/.hermes/bin/hermes` in einer eigenen Umgebung (Python 3.14 unter `~/.hermes/installs/…`); + das alte `venv` (Python 3.11) bleibt liegen, das Dashboard startet noch daraus. Der Job ruft deshalb den neuen + Starter, baut mit `CC=gcc CXX=g++ CMAKE_POLICY_VERSION_MINIMUM=3.5` (sonst scheitert `python-olm` am fehlenden + clang und an CMake 4), wertet Hinweise von `doctor` nicht als Fehler, legt mit `hermes-plugin-deps.sh` die Pakete + unserer Plugins nach (trafilatura) und lädt die Units neu, bevor er neu startet. Die Zusatzpakete, die Lucy + braucht, merkt sich der Paketmanager (`hermes pm install --extra telegram --extra ddgs --extra stt-whisper`, am + 25.09. eingerichtet); fehlen sie, meldet `hermes doctor` „Configured features whose dependencies are still + missing“, und nach dem nächsten Neustart wäre Telegram weg. Scheitert der Job, nachdem der neue Code schon da ist, + heißt es „HERMES-UPDATE UNVOLLSTÄNDIG“ statt „alter Stand“. - Grün: Meldung. - Rot oder Zeitlimit: erst `self-repair.sh` (kommentiert eindeutige, nicht sicherheitsrelevante Config-Schlüssel aus, an denen die neue Version scheitert; der Gehirn-Check muss danach grün sein).