Initial commit

This commit is contained in:
admin committed 2026-09-05 22:47:35 +02:00
commit 8641464a6c
44 files changed
+2192

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
name: CI
on:
push:
pull_request:
jobs:
smoke:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- uses: actions/checkout@v4
- name: Install test dependencies
run: pacman -Syu --noconfirm bash shellcheck ripgrep python jq
- name: Run smoke tests
run: ./tests/smoke.sh
+19
View File
@@ -0,0 +1,19 @@
# Local configuration and secrets
config/config.env
config/hermes.env
config/llama.env
config/signal-cli.env
*.local
*.secret
*.key
# Runtime and generated files
dist/
*.tar.zst
*.log
*.rej
.idea/
.vscode/
/hermes-test-*/
__pycache__/
*.pyc
+25
View File
@@ -0,0 +1,25 @@
# 2026-09-05 — RTX-3080-/Qwen-Deployment
- CUDA-Quellbuild mit expliziter Revision, dokumentierter SHA, drei Tools und
getrennten Build-Verzeichnissen. Bestehende Binary-Auswahl bleibt erhalten.
- Modellwahl per HF oder lokaler GGUF, optional lokaler Vision-Projektor,
stabiler Hermes-Modellalias.
- Vier Profile mit 64K/48K/32K Kontext und 1536/1280 MiB Fit-Zielmarge;
ursprüngliche Parameter bleiben Standard und Rückweg.
- Vorprüfung bekannter Optionsnamen ohne Modellstart. Keine automatische
Hardware-/Treiberänderung oder Behauptung einer garantierten VRAM-Reserve.
- Profilwahl mit Backup ohne Neustart; separater Neustart mit ausdrücklicher
Bestätigung, dass Jobs fertig sind. Geteiltes Maintenance-Lock.
- Healthcheck startet den laufenden llama-server bei HTTP-Timeouts nicht neu.
Automatischer Start inaktiver llama-Dienste standardmäßig deaktiviert.
- Installationssicherung vorhandener Wrapper/Units; bestehende Konfigurationen
und Gateway-Unit bleiben erhalten. CUDA-Installation ausdrücklich optional.
- Anleitungen für neue Rechner, bestehende Workstation, Messung und Rücknahme.
- Isolierte Tests für Profilwerte, Legacy-Konfiguration, Pfade mit Leerzeichen,
Modellquellen, Optionsprüfung, Locks, Neustartschutz, HTTP-Fehler, CUDA-Build-
Veröffentlichung/Fehler und Signal-JSON-RPC-Payloads ohne echte Nachrichten.
Nicht hier getestet: echte CUDA-Kompilierung, Qwen-Modell-/Projektor-Download,
GPU-Offload, Hyprland-Spitzenlast, Inferenzqualität, tatsächlicher Hermes-Update-
Rollback oder externe Signal-/Telegram-Zustellung. Das Repository ist ein
Deployment-Paket, kein vollständiges Image deiner privaten Installation.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Johannes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+14
View File
@@ -0,0 +1,14 @@
.PHONY: test install doctor archive
test:
./tests/smoke.sh
install:
./install.sh
doctor:
~/.local/libexec/hermes-stack-doctor
archive:
mkdir -p dist
git archive --format=tar.gz --prefix=hermes-arch-stack/ -o dist/hermes-arch-stack.tar.gz HEAD
+208
View File
@@ -0,0 +1,208 @@
# Hermes Arch Stack
Reproduzierbares Benutzer-Deployment für Hermes Agent auf Arch Linux mit:
- offizieller Hermes-Git-Installation unter `~/.hermes/hermes-agent`
- von Hermes erzeugtem systemd-User-Service für den Messaging Gateway
- optionalem lokalen llama.cpp/OpenAI-kompatiblen Modellendpunkt
- optionalem signal-cli HTTP-Daemon
- robustem Hermes-Update mit Vorprüfung, Retry, Backup und Rollback
- regelmäßigen Healthchecks und Selbstheilungsversuchen
- Statusmeldungen an Telegram und Signal
- systemd-Timern für Updates und Zustandsprüfungen
- optionalem CUDA-Build von llama-server, llama-cli und llama-bench
- Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg
Stand: 2026-09-05. Für deine **bestehende Workstation** zuerst
[`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für
eine neue Maschine. Hardware-Leistungsgewinne sind noch nicht auf deiner GPU
gemessen. Es wird kein Gitea-Server angelegt und nichts automatisch gepusht.
Das Repository enthält **keine Zugangsdaten, Signal-Sitzung oder
Modellgewichte**. Diese Daten werden lokal angelegt und sind durch `.gitignore`
ausgeschlossen.
## Schnellstart auf einer neuen Arch-Maschine
```bash
git clone ssh://git@GITEA/OWNER/hermes-arch-stack.git
cd hermes-arch-stack
./install.sh --install-packages --install-hermes
```
Für den lokalen GPU-Endpunkt anschließend den
[`CUDA-Installer`](docs/LLAMA_INSTALL.md) ausführen. Ein vorhandener Server kann
stattdessen über `LLAMA_SERVER_BIN` verwendet werden. Der Installer benötigt
funktionierende NVIDIA-Treiber; diese werden nicht von diesem Repo eingerichtet.
`--install-packages` führt ein vollständiges `pacman -Syu` aus: bei Kernel-/
Treiberänderungen einen Neustart einplanen.
Danach die beim ersten Lauf erzeugten lokalen Konfigurationen bearbeiten:
```bash
$EDITOR ~/.config/hermes-stack-maintenance/config.env
$EDITOR ~/.config/hermes-stack-maintenance/llama.env
$EDITOR ~/.config/hermes-stack-maintenance/signal-cli.env
$EDITOR ~/.hermes/.env
```
Hermes und die Messaging-Plattformen konfigurieren:
```bash
hermes setup
hermes gateway setup
hermes gateway install --force
```
In Hermes als lokalen OpenAI-kompatiblen Endpunkt `http://127.0.0.1:8080/v1`
und als Modell-ID `qwen38-27b-local` wählen. Bestehende Hermes-Konfigurationen,
Profile, Skills und Memories werden nicht durch generische Vorgaben ersetzt.
Für Neuinstallationen diese Einstellungen interaktiv vornehmen oder die eigenen
Daten gemäß Migration sicher übernehmen. Der Alias allein setzt kein Kontextlimit.
Signal benötigt zusätzlich eine installierte/verknüpfte signal-cli-Instanz.
Nach Konfiguration kann `./scripts/install-signal-cli.sh` den vorhandenen
Signal-Updater aufrufen; siehe dessen Konfiguration und `docs/MIGRATION.md`.
Optionale Dienste erst nach ihrer Konfiguration aktivieren:
```bash
systemctl --user enable --now signal-cli.service
systemctl --user enable --now llama-qwen.service
```
Maintenance aktivieren:
```bash
systemctl --user enable --now \
hermes-stack-hermes-update.timer \
hermes-stack-health.timer \
hermes-stack-signal-health.timer \
hermes-stack-signal-update-check.timer
```
Für Start ohne interaktive Anmeldung:
```bash
sudo loginctl enable-linger "$USER"
```
Abschließend:
```bash
~/.local/libexec/hermes-stack-doctor
```
## Installationsoptionen
```text
./install.sh [Optionen]
--install-packages Arch-Pakete mit pacman installieren
--install-hermes offiziellen Hermes-Installer ausführen
--install-llama CUDA-Build installieren (benötigt funktionierenden Treiber)
--llama-ref REF expliziter llama.cpp-Tag/Commit oder bewusst master
--enable-timers Maintenance-Timer sofort aktivieren
--skip-browser Hermes ohne Browser-Automation installieren (Standard)
--with-browser Browser-Automation nicht überspringen
```
Ohne Optionen installiert das Skript nur die versionierten Dateien und legt
fehlende Beispielkonfigurationen an. Bestehende lokale Konfigurationen werden
nicht überschrieben.
Ersetzte Wrapper und Units werden vorher gesichert. Eine vorhandene
hermes-gateway.service wird nicht neu erzeugt. Ein Modell wird erst beim echten
Start des llama-Dienstes geladen, nicht bei der Konfigurationsprüfung.
## RTX 3080 und Qwen
```bash
~/.local/libexec/hermes-stack-llama-run --check
~/.local/libexec/hermes-stack-llama-profile list
~/.local/libexec/hermes-stack-llama-report
```
`baseline` behält deine bisherigen 64K Kontext / 1536 MiB Fit-Zielmarge.
`balanced` reduziert nur den Kontext auf 48K; `vram-plus` danach optional die
Marge auf 1280 MiB. `compact` bietet 32K bei 1536 MiB. Das sind Vergleichsprofile,
keine Garantie gegen Desktop-Speicherdruck. Details, Messprotokoll und Rücknahme:
[`docs/RTX3080.md`](docs/RTX3080.md).
Profilwechsel ändern zunächst nur eine separate Auswahl-Datei. Ein Neustart
benötigt `hermes-stack-llama-restart --jobs-finished`. Der Healthcheck teilt das
Update-Lock und löst bei Backend-HTTP-Timeouts keine Inferenz-Neustarts mehr aus.
## Update-Verhalten
`hermes-stack-hermes-update` führt folgende Schritte aus:
1. exklusives Lock und Prüfung auf lokale Änderungen
2. `origin/main`-Fetch mit begrenzten Retries und letztem HTTP/1.1-Fallback
3. schneller Abschluss ohne Backup, wenn kein Update vorliegt
4. komprimiertes Backup vor jeder Änderung
5. Stoppen des Gateways und Aufruf von `hermes update`
6. einmaliger Retry bei eindeutig transientem Fetch-Fehler
7. Konfigurationsmigration und frischer Gateway-Neustart
8. Gateway-Healthcheck; bei Fehler Wiederherstellung des gesicherten Checkouts und der erfassten Konfigurationsdateien
9. separater llama.cpp-Healthcheck ohne unsinnigen Hermes-Rollback
10. Aufbewahrung der drei neuesten Hermes-Backups
Ein Fehler von Telegram oder Signal kann den Updateprozess niemals fehlschlagen
lassen. Detaillierte Zwischenstände landen im Journal; nur zusammengefasste
Ergebnisse werden versendet.
## Bedienung
```bash
# Update manuell
systemctl --user start hermes-stack-hermes-update.service
# Status und Logs
systemctl --user status hermes-stack-hermes-update.service
journalctl --user -u hermes-stack-hermes-update.service -n 200
journalctl --user -t hermes-stack-maintenance -n 100
# Benachrichtigung testen
~/.local/libexec/hermes-stack-notify \
"Hermes-Stack: Benachrichtigungstest"
# signal-cli nach einer Update-Meldung aktualisieren
~/.local/libexec/hermes-stack-signal-update
# Gesamtprüfung
~/.local/libexec/hermes-stack-doctor
```
## Migration
Deployment-Code und Benutzerdaten werden bewusst getrennt behandelt. Siehe
[`docs/MIGRATION.md`](docs/MIGRATION.md) für die Übernahme von Hermes-Daten,
Signal-Sitzung und Modellgewichten.
## Gitea
Nach dem Entpacken oder Erzeugen des Repositories:
```bash
git init
git add .
git commit -m "Initial Hermes Arch deployment"
git branch -M main
git remote add origin ssh://git@GITEA/OWNER/hermes-arch-stack.git
git push -u origin main
```
Die mitgelieferte Gitea-Actions-Workflowdatei führt Syntax- und Smoke-Tests aus.
`make test` führt zusätzlich isolierte Funktionstests mit simulierten Programmen
aus. Ein echter NVIDIA-/Arch-/Signal-/Hermes-End-to-End-Test bleibt auf der
Zielmaschine erforderlich. `make archive` verwendet den letzten Commit (also
zuerst eigene Änderungen prüfen und committen).
## Grenzen der Reproduktion
Das Repo reproduziert die Deployment-Logik, nicht deine privaten Daten oder ein
eingefrorenes Arch-Abbild. Für denselben Modell-/Backendstand Quell-SHA sowie
Modell-/Projektor-Prüfsummen notieren. Der offizielle Hermes-Installer und Arch
liefern beim Neuaufsetzen sonst den dann verfügbaren Stand. Technische Quellen
stehen in `docs/LLAMA_INSTALL.md` und `docs/RTX3080.md`.
+22
View File
@@ -0,0 +1,22 @@
# Wird als Bash-Konfiguration eingelesen. Keine Leerzeichen um '=' verwenden.
HERMES_REPO="$HOME/.hermes/hermes-agent"
HERMES_ENV="$HOME/.hermes/.env"
HERMES_REMOTE="origin"
HERMES_BRANCH="main"
GATEWAY_SERVICE="hermes-gateway.service"
LLAMA_SERVICE="llama-qwen.service"
LLAMA_URL="http://127.0.0.1:8080"
# 1: inaktiven Dienst wieder starten. HTTP-Timeouts allein starten nie neu.
# Standard 0 verhindert unerwünschte Modellstarts während Wartung/Tuning.
LLAMA_HEALTH_AUTORESTART=0
SIGNAL_SERVICE="signal-cli.service"
# Zusammengefasste Maintenance-Meldungen. Credentials/Ziele stehen in HERMES_ENV.
TELEGRAM_NOTIFY=1
SIGNAL_NOTIFY=1
# Backup- und Prüfverhalten
BACKUP_KEEP=3
GATEWAY_HEALTH_ATTEMPTS=12
GATEWAY_HEALTH_DELAY=5
+16
View File
@@ -0,0 +1,16 @@
# Diese Datei dient nur als Feldübersicht. Auf dem Zielsystem verwendet Hermes
# standardmäßig ~/.hermes/.env. Niemals die ausgefüllte Datei committen.
# Telegram
TELEGRAM_BOT_TOKEN=
TELEGRAM_HOME_CHANNEL=
TELEGRAM_ALLOWED_USERS=
# Signal
SIGNAL_HTTP_URL=http://127.0.0.1:8090
SIGNAL_ACCOUNT=
SIGNAL_HOME_CHANNEL=
SIGNAL_ALLOWED_USERS=
# Modellprovider bzw. lokaler OpenAI-kompatibler Endpunkt werden bevorzugt mit
# `hermes setup` bzw. `hermes model` konfiguriert.
+17
View File
@@ -0,0 +1,17 @@
# Bash-Konfiguration für hermes-stack-llama-run
# Leer: zuerst den verwalteten CUDA-Build, dann llama-server aus PATH verwenden.
# Auf deiner bestehenden Workstation explizit /usr/bin/llama-server eintragen.
LLAMA_SERVER_BIN=""
LLAMA_MODEL=""
LLAMA_HF_REPO="ggml-org/Qwen3.8-27B-GGUF:Q4_K_M"
LLAMA_ALIAS="qwen38-27b-local"
LLAMA_MMPROJ=""
LLAMA_HOST="127.0.0.1"
LLAMA_PORT="8080"
# Erster Start unverändert mit 64K / 1536 MiB Fit-Zielmarge.
# Anschließend erst balanced, danach ggf. vram-plus vergleichen.
LLAMA_PROFILE="baseline"
LLAMA_ARGS=()
# HF lädt beim ersten echten Start herunter. Für offline/reproduzierbar:
# LLAMA_MODEL und optional LLAMA_MMPROJ setzen; LLAMA_HF_REPO leeren.
+5
View File
@@ -0,0 +1,5 @@
# Bash-Konfiguration für hermes-stack-signal-run
SIGNAL_CLI_BIN="$HOME/.local/bin/signal-cli"
SIGNAL_ACCOUNT=""
SIGNAL_HTTP_BIND="127.0.0.1:8090"
SIGNAL_CONFIG_DIR="$HOME/.local/share/signal-cli"
+77
View File
@@ -0,0 +1,77 @@
# llama.cpp samt Server, CLI und Benchmark installieren
Der Installer baut die drei Programme `llama-server`, `llama-cli`, `llama-bench`
aus dem offiziellen Repository mit CUDA. Er überschreibt weder `/usr/bin` noch
einen vorhandenen Build und startet keinen Dienst. Kein AUR-Helper erforderlich.
## Neue Arch-Maschine
Zuerst einen zur GPU passenden NVIDIA-Treiber einrichten. `nvidia-smi` muss
funktionieren. Treiberwahl und Kernelupdates werden nicht automatisiert.
```bash
sudo pacman -Syu --needed base-devel git cmake ninja cuda openssl
```
Dies ist eine vollständige Arch-Systemaktualisierung. Bei Kernel-/Treiberupdate
Jobs beenden und neu starten, bevor du die GPU-Installation testest. Keine
partielle Aktualisierung durch Ausschließen einzelner Systempakete erzwingen.
Dann aus dem Deployment-Repo:
```bash
# master bewusst ausgewählt, keine als stabil behauptete Release-Version:
./scripts/install-llama-cpp.sh --ref master --jobs 4 --cuda-arch 86
```
`86` ist für RTX 3080 gewählt. Für einen anderen Rechner dessen GPU-Architektur
angeben. Mit `--ref <vollständiger-Commit>` lässt sich derselbe Quellstand erneut
bauen. Tags/Branches werden aufgelöst und im jeweiligen `BUILD-INFO.txt` zusammen
mit der SHA gespeichert. CUDA/Compiler/Arch-Pakete bleiben rollende Abhängigkeiten;
das ist kein bitidentisch eingefrorenes Betriebssystem-Image.
`--prefix /absoluter/pfad` erlaubt einen anderen Build-Speicherort. Dann den
ausgegebenen Binary-Pfad ausdrücklich in `LLAMA_SERVER_BIN` konfigurieren.
Falls CUDA den installierten Hostcompiler ablehnt: kompatiblen Compiler nach
CUDA-/Arch-Dokumentation wählen und CMake z.B. über `CC`, `CXX`, `CUDAHOSTCXX`
konfigurieren. Kein automatisches `--allow-unsupported-compiler` und keine
Treiber-/Systemdateipatches. Ein fehlgeschlagener Build lässt aktive Versionen
unverändert und bleibt zur Diagnose erhalten.
Der Installer kopiert das Build-bin-Verzeichnis einschließlich dortiger
Begleitdateien. Die Projektbibliotheken werden statisch gebaut; CUDA- und
Systembibliotheken bleiben Laufzeitabhängigkeiten. Auf neuer Hardware neu bauen,
nicht nur die Binary übertragen.
## Auswahl
- Erster verwalteter Build: Link `~/.local/opt/llama.cpp/current` wird angelegt.
- Spätere Builds: der Link bleibt stehen, der neue Pfad wird ausgegeben.
- Expliziter `LLAMA_SERVER_BIN` in `llama.env` hat Vorrang vor dem Link.
- Ohne expliziten Pfad: verwalteter Link, sonst `llama-server` aus PATH.
Auf der bisherigen Workstation **zunächst `/usr/bin/llama-server` beibehalten**.
Einen Backend-Versionswechsel und VRAM-Tuning nicht im selben Vergleich vermischen.
Für einen späteren Build dessen ausgegebenen Pfad in `LLAMA_SERVER_BIN` eintragen,
prüfen und erst nach Jobende mit dem Restart-Helfer aktivieren. Den alten Pfad
notieren; Rücknahme bedeutet Pfad zurücksetzen und erneut kontrolliert starten.
```bash
~/.local/libexec/hermes-stack-llama-run --check
```
Die Prüfung lädt kein Modell. Das erste echte Starten mit `LLAMA_HF_REPO`
benötigt Netzwerk, genug Plattenplatz und ein vorhandenes HF-Repository. Bei
Download-/Modellproblemen die vom Nutzer angegebene Modell-ID prüfen; sie wurde
hier nicht heruntergeladen. Für offline und feste Modellversionen lokale
GGUF-Dateien und ggf. passenden Vision-Projektor verwenden, SHA256 notieren.
Modell und Projektor werden separat gesichert, nicht im Git-Repo abgelegt.
## Offizielle Grundlagen
- [llama.cpp bauen, CUDA und GPU-Architekturen](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md)
- [Arch CUDA-Paket](https://archlinux.org/packages/extra/x86_64/cuda/)
- [NVIDIA Compute Capability](https://developer.nvidia.com/cuda-gpus)
Ein echter CUDA-Build und Modellbetrieb müssen auf der Zielmaschine geprüft
werden. Die mitgelieferten Tests benutzen keine GPU und laden keine Gewichte.
+83
View File
@@ -0,0 +1,83 @@
# Migration auf eine neue Arch-Linux-Maschine
Das Gitea-Repository reproduziert Programme, Units und Wartungslogik. Persönliche
Hermes-Daten, Secrets, Signal-Geräteidentität und Modellgewichte werden separat
übertragen.
## 1. Deployment installieren
```bash
git clone ssh://git@GITEA/OWNER/hermes-arch-stack.git
cd hermes-arch-stack
./install.sh --install-packages --install-hermes
```
## 2. Hermes-Benutzerdaten übertragen
Auf der alten Maschine Hermes und Gateway stoppen und ein verschlüsseltes
Backup erstellen. Mindestens relevant sind:
- `~/.hermes/config.yaml`
- `~/.hermes/.env`
- `~/.hermes/auth.json`, sofern vorhanden
- Profile, Sessions, Memories, Skills und Cron-Daten unter `~/.hermes/`
Der Upstream-Checkout `~/.hermes/hermes-agent/` muss nicht übertragen werden;
er wird vom offiziellen Installer neu erzeugt. Ein unverschlüsseltes Archiv mit
Secrets darf weder ins Repository noch auf einen ungeschützten Fileserver.
Alternativ den jeweils aktuellen Hermes-Backup-/Import-Befehl verwenden und
vorher dessen Hilfe prüfen:
```bash
hermes backup --help
hermes import --help
```
## 3. Signal
Entweder signal-cli auf dem neuen Rechner als neues verknüpftes Gerät anmelden
oder das vollständige signal-cli-Datenverzeichnis verschlüsselt übertragen:
```text
~/.local/share/signal-cli/
```
Dieses Verzeichnis enthält Zugangsdaten und ist wie ein Passwort zu behandeln.
Es darf niemals committed werden. Anschließend Account und Port in
`~/.config/hermes-stack-maintenance/signal-cli.env` sowie in `~/.hermes/.env`
abgleichen.
Eine kopierte Signal-Geräteidentität nicht auf beiden Maschinen gleichzeitig
betreiben. Für Parallelbetrieb die neue Maschine als separates Gerät verknüpfen.
## 4. llama.cpp und Modell
`llama-server` mit [LLAMA_INSTALL.md](LLAMA_INSTALL.md) installieren (CUDA-Build
inklusive CLI und Benchmark) oder vorhandene Binary verwenden. Die GGUF-Datei
und einen ggf. benötigten Vision-Projektor übertragen oder beim ersten Start
über HF beziehen. Danach
`~/.config/hermes-stack-maintenance/llama.env` anpassen. Modellgewichte gehören
wegen Größe und Lizenz nicht in dieses Repository.
Die RTX-3080-Profile sind für 10 GB VRAM und die bisherige CPU ausgelegt; auf
anderer Hardware neu messen. `LLAMA_SERVER_BIN`, Modellpfad, Port und Alias
zwischen Hermes und llama.cpp abgleichen. Der neue Rechner braucht seine eigenen
NVIDIA-/Kernelpakete. Keine Binärkopie des alten CUDA-Builds erzwingen.
## 5. Aktivieren und prüfen
```bash
hermes gateway install --force
systemctl --user daemon-reload
systemctl --user enable --now signal-cli.service llama-qwen.service
systemctl --user enable --now \
hermes-stack-hermes-update.timer \
hermes-stack-health.timer \
hermes-stack-signal-health.timer \
hermes-stack-signal-update-check.timer
~/.local/libexec/hermes-stack-doctor
```
Erst wenn der Doctor erfolgreich ist, die alte Maschine bzw. deren Signal-Gerät
außer Betrieb nehmen.
+98
View File
@@ -0,0 +1,98 @@
# Qwen auf RTX 3080 10 GB / Hyprland
Zielhardware: RTX 3080 mit 10 GiB, i9-13900KF, 64 GB RAM. Der Desktop und
llama.cpp nutzen dieselbe GPU. Das 27B-Modell in Q4_K_M passt nicht vollständig
in 10 GiB: CPU/RAM-Offload bleibt erforderlich. Mehr VRAM für Gewichte kann
helfen; weder Voll-Offload noch eine bestimmte Beschleunigung werden versprochen.
## Profile
| Profil | Kontextlimit | Fit-Zielmarge | Einsatz |
| --- | ---: | ---: | --- |
| `baseline` | 65536 | 1536 MiB | bisherige Startparameter, Referenz und Rückweg |
| `balanced` | 49152 | 1536 MiB | erster Vergleich, nur Kontext reduzieren |
| `vram-plus` | 49152 | 1280 MiB | zweiter Vergleich, kleinere Zielmarge |
| `compact` | 32768 | 1536 MiB | Alternative für kürzere Sitzungen |
Alle Profile: ein Slot, K/V-Cache q8_0, Flash Attention auto, GPU-Layer auto,
Fit an, 6 Generierungs-/16 Batch-Threads, Jinja, Reasoning auto/512/preserve,
Image-Minimum 1024 Tokens und Idle-Sleep nach 300 Sekunden. Modellalias bleibt
`qwen38-27b-local`. Es werden keine Sampling-Werte oder Hermes-Memories geändert.
Die Definitionen stehen zentral in `libexec/hermes-stack-llama-run`.
Die Konfiguration und Optionsnamen werden anhand der installierten Binary
geprüft. Nicht unterstützte Optionen führen zum Abbruch, nicht zum stillen
Weglassen. Diese Prüfung kann GPU-Laufzeitfehler oder ungültige Optionswerte
nicht ausschließen. Auf anderer Hardware die Profile nicht ungeprüft übernehmen.
**Fit-Zielmarge ist keine geschützte VRAM-Reservierung.** Sie beeinflusst die
Speicheranpassung beim Laden; später wachsende Browser-/Desktop-/Bildlast kann
trotzdem zu Speicherdruck führen. Verfügbare Layer und Puffer hängen auch von
Modellarchitektur, Kontext, Projektor und llama.cpp-Version ab. Weniger Kontext
bedeutet nicht proportional weniger Gesamtspeicher. 1024 MiB und weniger sind
hier bewusst nicht als fertiges Profil vorgesehen.
## Messung statt Auslastungsziel
1. Zuerst `baseline` mit deiner normalen Browser-/Hyprland-Nutzung messen.
2. Jobs beenden. `balanced` wählen und kontrolliert neu starten.
3. Dieselben Aufgaben erneut ausführen: kurze Textantwort, längerer Agent-Task
mit Tools und eine repräsentative Bildaufgabe. Qualität mitbewerten.
4. Kalten Start nach Idle-Sleep getrennt von warmem Prompt-Cache messen.
5. Erst bei genügend Spielraum `vram-plus` probieren. Bei Rucklern, OOM,
Grafikfehlern oder deutlich schlechterer Latenz zurück auf `baseline`.
```bash
~/.local/libexec/hermes-stack-llama-profile list
~/.local/libexec/hermes-stack-llama-report
# Nur speichern, unterbricht nichts:
~/.local/libexec/hermes-stack-llama-profile balanced
# Ausschließlich nach Abschluss aller Backend-Jobs:
~/.local/libexec/hermes-stack-llama-restart --jobs-finished
journalctl --user -u llama-qwen.service -f
```
Laufende Inferenz überlebt diesen **llama.cpp-Neustart nicht**. Ein Hermes-Update
ist davon getrennt. Die neue Healthcheck-Version startet einen laufenden
llama-server nicht allein wegen eines HTTP-Timeouts neu. Ein inaktiver Dienst
wird nur mit `LLAMA_HEALTH_AUTORESTART=1` automatisch wieder gestartet.
GPU während aller Vergleichsläufe beobachten, nicht nur nach Modellstart:
```bash
nvidia-smi --query-gpu=timestamp,memory.used,memory.free,utilization.gpu \
--format=csv --loop=1
```
Pro Profil notieren: freie VRAM-Minima, tatsächlich ausgelagerte GPU-Layer,
Prompt-tok/s, Generierungs-tok/s, Zeit bis erster Antwort, Job-Gesamtdauer,
Desktop-Verhalten und Ergebnisqualität. Nicht die höchste VRAM-Anzeige gewinnt,
sondern die beste zuverlässig nutzbare Leistung.
Bei kleinerem Kontext auch Hermes' Kontextbudget passend konfigurieren: erst
die in deiner Hermes-Version verfügbaren Modell-/Konfigurationsoptionen ansehen.
Der Alias bleibt gleich, daher erkennt Hermes eine geänderte Grenze nicht
zwingend automatisch. Lange alte Sitzungen nicht ungeprüft fortsetzen; eine
frische Sitzung für den Vergleich verwenden. Das Deployment überschreibt deine
`config.yaml` bewusst nicht.
## Rückkehr
```bash
~/.local/libexec/hermes-stack-llama-profile baseline
~/.local/libexec/hermes-stack-llama-restart --jobs-finished
```
Eine Profiländerung stellt keine anderen Konfigurationen zurück. Für alte
vollständige Startparameter siehe `WORKSTATION.md`. Browserbeschleunigung,
NVIDIA-Treiber, Hyprland und Kernelparameter werden nicht verändert.
## Quellen
Offizielle [llama.cpp-Argumentdefinitionen](https://github.com/ggml-org/llama.cpp/blob/master/common/arg.cpp)
für Fit-Marge, Kontext, Cache und Idle-Sleep; geprüft am 2026-09-05. Maßgeblich
für deinen Rechner bleibt `llama-server --help` seiner tatsächlich installierten
Version. Die Profile übernehmen die von dir gezeigten Modellparameter; sie
sind noch kein auf deiner GPU gemessener Optimalwert.
+12
View File
@@ -0,0 +1,12 @@
# Sicherheit
- Keine ausgefüllten `.env`-Dateien, Signal-Daten oder Modellzugangsdaten committen.
- Lokale Konfigurationen werden mit Modus `0600` angelegt.
- Signal- und Telegram-Ziele werden ausschließlich aus `~/.hermes/.env` gelesen.
- Signal-Zugriff per `SIGNAL_ALLOWED_USERS` oder Hermes-DM-Pairing beschränken.
- `GATEWAY_ALLOW_ALL_USERS` nicht für einen Agenten mit Terminalzugriff verwenden.
- Das Update löscht beim Rollback nur einen geprüften Pfad innerhalb von `$HOME`,
dessen letzter Bestandteil `hermes-agent` ist.
- Backups enthalten Secrets und müssen wie Zugangsdaten geschützt werden.
- Gitea-Repository privat halten, auch wenn keine Secrets eingecheckt werden.
- Vor produktivem Einsatz `git diff --cached` und Secret-Scanning ausführen.
+22
View File
@@ -0,0 +1,22 @@
# Validierung vom 2026-09-05
Ausgeführt in einer isolierten Arbeitsumgebung, nicht auf der Workstation:
- `make test`: erfolgreich.
- `bash -n`: Installer, Uninstaller, alle libexec-/Installationsskripte und
alle vier Beispielkonfigurationen erfolgreich.
- Python-Vertragstests: **25 Tests erfolgreich**. Fake-Binaries ersetzen
CUDA-Build, Git-Netzwerk, systemd, HTTP und Signal-Zustellung.
- `git diff --check`: erfolgreich.
- Einfacher Secret-Mustercheck des Smoke-Skripts: kein Treffer. Kein Ersatz
für einen vollständigen Secret-Scanner oder die Prüfung vor einem Push.
- ShellCheck war hier nicht installiert und wurde nicht ausgeführt; die
mitgelieferte Gitea-CI installiert und verwendet ihn.
Nicht durchgeführt: Arch-Paketinstallation, echter CUDA-Build, Modell-Download,
GPU-/Hyprland-Benchmark, Hermes-Installation, End-to-End-Update/Rollback oder
echte Signal-/Telegram-Nachrichten. Die 25 Tests sind kein Nachweis dafür.
Vor Produktivbetrieb: `WORKSTATION.md` bzw. `MIGRATION.md` abarbeiten, danach
`RTX3080.md` für kontrollierte Vergleiche. Kein Dienst auf der Workstation wurde
durch die Erstellung dieses Pakets verändert.
+97
View File
@@ -0,0 +1,97 @@
# Bestehende Workstation behutsam übernehmen
Nicht blind den Schnellstart für eine neue Maschine ausführen. Vorhandene
Hermes-/Signal-Konfiguration bleibt privat und wird nicht durch das Repo ersetzt.
Das aktuelle Deployment enthält deine gezeigten Anpassungen; weitere lokale,
hier nicht übermittelte Änderungen können nicht automatisch rekonstruiert werden.
## 1. Ausgangszustand sichern
Jobs zu Ende laufen lassen, sicherstellen dass gerade kein Hermes-Update läuft.
Vorhandene Startparameter, Versionen, Units und Drop-ins lokal dokumentieren:
```bash
systemctl --user cat llama-qwen.service
systemctl --user cat hermes-gateway.service
/usr/bin/llama-server --version
systemctl --user list-timers --all --no-pager
```
Bestehende Maintenance-Timer während der Umstellung anhalten; zuvor aktive
Timer notieren und nur diese später wieder aktivieren. Laufende Service-Jobs
werden durch Stoppen ihrer Timer nicht beendet.
```bash
systemctl --user stop hermes-stack-health.timer hermes-stack-hermes-update.timer \
hermes-stack-signal-health.timer hermes-stack-signal-update-check.timer
```
Das folgende `install.sh` sichert ersetzte Dateien unter
`~/.local/state/hermes-stack-deployment.*`, behält vorhandene env-Dateien und
Gateway-Unit bei und startet ohne Optionen keine Timer und keinen Backend-Dienst.
Eigene systemd-Drop-ins bleiben bestehen; sie können den neuen ExecStart übersteuern!
Vergleiche nachher `systemctl --user cat llama-qwen.service`.
```bash
./install.sh
```
Nicht `--install-hermes`, `--install-llama` oder `--install-packages` verwenden,
wenn zunächst nur das bestehende funktionierende Setup übernommen werden soll.
## 2. llama.env bewusst migrieren
Der Installer überschreibt `llama.env` nicht. Vergleiche sie mit
`config/llama.env.example`. Vor manueller Bearbeitung eine datierte Kopie anlegen:
```bash
cp -a ~/.config/hermes-stack-maintenance/llama.env \
~/.config/hermes-stack-maintenance/llama.env.before-rtx-tuning
```
Den Backup-Namen nur einmal verwenden oder ein neues Datum anhängen. Folgende
Werte aus der Vorlage übernehmen, vorhandene individuelle Modellpfade prüfen:
```bash
LLAMA_SERVER_BIN="/usr/bin/llama-server"
LLAMA_MODEL=""
LLAMA_HF_REPO="ggml-org/Qwen3.8-27B-GGUF:Q4_K_M"
LLAMA_ALIAS="qwen38-27b-local"
LLAMA_MMPROJ=""
LLAMA_HOST="127.0.0.1"
LLAMA_PORT="8080"
LLAMA_PROFILE="baseline"
LLAMA_ARGS=()
```
Eine alte Konfiguration mit `LLAMA_MODEL` und vollständigem `LLAMA_ARGS` läuft
weiter, solange `LLAMA_PROFILE` nicht gesetzt ist. Der Profilschalter verweigert
unmigrierte Konfigurationen. Lokale GGUF statt HF: Modellpfad setzen und HF leeren.
```bash
~/.local/libexec/hermes-stack-llama-run --print
~/.local/libexec/hermes-stack-llama-run --check
```
Kein Modellstart, kein Neustart. Erst nach erfolgreicher Prüfung und Jobende:
```bash
~/.local/libexec/hermes-stack-llama-restart --jobs-finished
curl -fsS http://127.0.0.1:8080/health
~/.local/libexec/hermes-stack-llama-report
```
Ein kurzzeitiges 503 während des Ladens ist noch kein Beweis für einen Defekt.
Logs kontrollieren und auf Bereitschaft warten. Danach eine echte Text-/Bild-
und Tool-Aufgabe in Hermes prüfen. Erst dann vorher aktive Timer wieder starten.
`LLAMA_HEALTH_AUTORESTART=0` ist auch bei älterer config.env ohne diesen Schlüssel
der neue Standard: HTTP-Probleme werden gemeldet, nicht durch Neustarts behandelt.
## 3. Optimieren und zurücksetzen
Nun gemäß [RTX3080.md](RTX3080.md) jeweils nur ein Profil vergleichen. Zurück:
Profil `baseline` wählen und kontrolliert neustarten. Für vollständige Rückkehr
alte llama.env und ggf. alte Unit/Wrapper aus der Deployment-Sicherung einzeln
wiederherstellen, `daemon-reload` und Neustart nach Jobende. Nicht pauschal ein
Archiv über dein gesamtes Home extrahieren. Die Datei `llama-profile.env` hat nur
bei aktivem Profilbetrieb Vorrang; ihre vorherigen Stände liegen daneben.
Executable
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
INSTALL_PACKAGES=0
INSTALL_HERMES=0
INSTALL_LLAMA=0
LLAMA_REF=""
ENABLE_TIMERS=0
SKIP_BROWSER=1
usage() {
sed -n '/^## Installationsoptionen/,/^## /p' "$ROOT_DIR/README.md" |
sed '$d'
}
while (($# > 0)); do
case "$1" in
--install-packages) INSTALL_PACKAGES=1 ;;
--install-hermes) INSTALL_HERMES=1 ;;
--install-llama) INSTALL_LLAMA=1 ;;
--llama-ref) [[ $# -ge 2 ]] || exit 2; LLAMA_REF="$2"; shift ;;
--enable-timers) ENABLE_TIMERS=1 ;;
--skip-browser) SKIP_BROWSER=1 ;;
--with-browser) SKIP_BROWSER=0 ;;
-h|--help) usage; exit 0 ;;
*) printf 'Unbekannte Option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
esac
shift
done
if ((INSTALL_LLAMA)) && [[ -z "$LLAMA_REF" ]]; then
printf -- '--install-llama benötigt --llama-ref TAG_ODER_COMMIT (oder bewusst master).\n' >&2
exit 2
fi
if [[ ! -e /etc/arch-release ]]; then
printf 'Warnung: Dieses Deployment ist für Arch Linux ausgelegt.\n' >&2
fi
export PATH="$HOME/.local/bin:$PATH"
if ((INSTALL_PACKAGES)); then
# Vollständige Aktualisierung statt partieller Arch-Aktualisierung.
packages=(
bash git curl xz jq zstd tar ripgrep ffmpeg coreutils util-linux \
jre-openjdk-headless
)
if ((INSTALL_LLAMA)); then
packages+=(base-devel cmake ninja cuda openssl)
fi
sudo pacman -Syu --needed "${packages[@]}"
fi
if ((INSTALL_LLAMA)); then
"$ROOT_DIR/scripts/install-llama-cpp.sh" --ref "$LLAMA_REF"
fi
if ((INSTALL_HERMES)) && ! command -v hermes >/dev/null 2>&1; then
installer_args=()
((SKIP_BROWSER)) && installer_args+=(--skip-browser)
curl -fsSL https://hermes-agent.nousresearch.com/install.sh |
bash -s -- "${installer_args[@]}"
export PATH="$HOME/.local/bin:$PATH"
fi
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
unit_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
libexec_dir="$HOME/.local/libexec"
mkdir -p "$config_dir" "$unit_dir" "$libexec_dir" "${XDG_STATE_HOME:-$HOME/.local/state}"
# Bestehende Deployment-Dateien vor Ersetzen sichern. Keine Secrets ausgeben.
deploy_backup="$(mktemp -d "${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-deployment.XXXXXXXX")"
mkdir -p "$deploy_backup/libexec" "$deploy_backup/units"
for file in "$ROOT_DIR"/libexec/*; do
target="$libexec_dir/$(basename "$file")"
[[ ! -e "$target" ]] || cp -a -- "$target" "$deploy_backup/libexec/"
done
for file in "$ROOT_DIR"/systemd/user/*; do
target="$unit_dir/$(basename "$file")"
[[ ! -e "$target" ]] || cp -a -- "$target" "$deploy_backup/units/"
done
printf 'Vorherige Deployment-Dateien: %s\n' "$deploy_backup"
for file in "$ROOT_DIR"/libexec/*; do
install -m 0755 "$file" "$libexec_dir/$(basename "$file")"
done
for file in "$ROOT_DIR"/systemd/user/*; do
install -m 0644 "$file" "$unit_dir/$(basename "$file")"
done
install_config_if_missing() {
local example="$1" target="$2"
if [[ ! -e "$target" ]]; then
install -m 0600 "$example" "$target"
printf 'Konfiguration angelegt: %s\n' "$target"
else
printf 'Konfiguration beibehalten: %s\n' "$target"
fi
}
install_config_if_missing \
"$ROOT_DIR/config/config.env.example" "$config_dir/config.env"
install_config_if_missing \
"$ROOT_DIR/config/llama.env.example" "$config_dir/llama.env"
install_config_if_missing \
"$ROOT_DIR/config/signal-cli.env.example" "$config_dir/signal-cli.env"
if [[ ! -e "$HOME/.hermes/.env" ]]; then
mkdir -p "$HOME/.hermes"
install -m 0600 "$ROOT_DIR/config/hermes.env.example" "$HOME/.hermes/.env"
printf 'Hermes-Umgebung angelegt: %s\n' "$HOME/.hermes/.env"
fi
systemctl --user daemon-reload
if command -v hermes >/dev/null 2>&1 && [[ ! -e "$unit_dir/hermes-gateway.service" ]]; then
if [[ -d "$HOME/.hermes/hermes-agent/.git" ]]; then
hermes gateway install ||
printf 'Gateway-Unit konnte noch nicht erzeugt werden; nach `hermes setup` erneut ausführen.\n' >&2
fi
fi
if ((ENABLE_TIMERS)); then
systemctl --user enable --now \
hermes-stack-hermes-update.timer \
hermes-stack-health.timer \
hermes-stack-signal-health.timer \
hermes-stack-signal-update-check.timer
fi
printf '\nDeployment installiert. Nächster Schritt:\n %s\n' \
"$libexec_dir/hermes-stack-doctor"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -Euo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
failures=0
ok() { printf '✓ %s\n' "$*"; }
bad() { printf '✗ %s\n' "$*"; failures=$((failures + 1)); }
[[ -r "$CONFIG_FILE" ]] || {
bad "$CONFIG_FILE fehlt"
exit 1
}
# shellcheck disable=SC1090
source "$CONFIG_FILE"
for command in git curl jq tar zstd systemctl hermes; do
command -v "$command" >/dev/null 2>&1 && ok "$command vorhanden" || bad "$command fehlt"
done
for script in "$HOME/.local/libexec"/hermes-stack-*; do
[[ -f "$script" ]] || continue
bash -n "$script" && ok "Syntax: ${script##*/}" || bad "Syntaxfehler: ${script##*/}"
done
if [[ -d "$HERMES_REPO/.git" ]]; then
ok "Hermes-Git-Checkout vorhanden"
dirty="$(git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
grep -vxF '?? .install_method' || true)"
[[ -z "$dirty" ]] && ok "Hermes-Checkout sauber" || bad "Hermes-Checkout enthält lokale Änderungen"
else
bad "Hermes-Git-Checkout fehlt"
fi
systemctl --user is-active --quiet "$GATEWAY_SERVICE" && ok "Gateway aktiv" || bad "Gateway inaktiv"
hermes gateway status >/dev/null 2>&1 && ok "Gateway gesund" || bad "Gateway-Status fehlgeschlagen"
if curl --fail --silent --max-time 5 "$LLAMA_URL/health" |
jq -e '.status == "ok"' >/dev/null 2>&1; then
ok "llama.cpp-Endpunkt gesund"
else
bad "llama.cpp-Endpunkt nicht gesund"
fi
set +u
set -a
# shellcheck disable=SC1090
source "$HERMES_ENV" 2>/dev/null || true
set +a
set -u
if [[ -n "${SIGNAL_HTTP_URL:-}" ]]; then
status="$(curl --silent --output /dev/null --max-time 5 --write-out '%{http_code}' \
"${SIGNAL_HTTP_URL%/}/api/v1/check" || true)"
[[ "$status" =~ ^2[0-9][0-9]$ ]] && ok "signal-cli HTTP erreichbar" || bad "signal-cli HTTP nicht erreichbar"
fi
printf '\nFehler: %d\n' "$failures"
((failures == 0))
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -Euo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
mkdir -p "$STATE_DIR"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
# Niemals den absichtlich gestoppten Gateway mitten im Update neu starten.
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
flock -n 9 || exit 0
state_file="$STATE_DIR/health-state"
problems=()
if ! systemctl --user is-active --quiet "$GATEWAY_SERVICE"; then
problems+=("$GATEWAY_SERVICE war inaktiv; Neustart wurde versucht")
systemctl --user restart "$GATEWAY_SERVICE" || true
fi
if ! hermes gateway status >/dev/null 2>&1; then
problems+=("Hermes-Gateway antwortete nicht gesund; Neustart wurde versucht")
systemctl --user restart "$GATEWAY_SERVICE" || true
fi
if [[ "${LLAMA_HEALTH_AUTORESTART:-0}" == 1 ]] &&
! systemctl --user is-active --quiet "$LLAMA_SERVICE"; then
problems+=("$LLAMA_SERVICE war inaktiv; Neustart wurde versucht")
systemctl --user restart "$LLAMA_SERVICE" || true
fi
if ! curl --fail --silent --max-time 10 "$LLAMA_URL/health" |
jq -e '.status == "ok"' >/dev/null 2>&1; then
problems+=("llama-server antwortete nicht gesund; kein HTTP-bedingter Neustart laufender Inferenz")
fi
if ((${#problems[@]} == 0)); then
current="healthy"
previous="$(cat "$state_file" 2>/dev/null || true)"
if [[ "$previous" == unhealthy ]]; then
"$NOTIFIER" "Hermes-Stack: Dienste sind wieder gesund." || true
fi
else
current="unhealthy"
previous="$(cat "$state_file" 2>/dev/null || true)"
if [[ "$previous" != unhealthy ]]; then
message="$(IFS='; '; printf '%s' "${problems[*]}")"
"$NOTIFIER" "Hermes-Stack: Gesundheitsprüfung fehlgeschlagen. $message. Details: journalctl --user -u $LLAMA_SERVICE -u $GATEWAY_SERVICE" || true
fi
fi
printf '%s\n' "$current" >"$state_file"
[[ "$current" == healthy ]]
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
mkdir -p "$STATE_DIR"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
log() {
printf '%s\n' "$*" | systemd-cat -t hermes-stack-maintenance
}
notify_user() {
local text="$1"
if [[ -x "$NOTIFIER" ]]; then
"$NOTIFIER" "$text" || true
else
log "$text"
fi
}
fetch_origin_main_with_retry() {
local -a retry_delays=(0 5 20)
local -a git_options=()
local attempt delay
for ((attempt = 1; attempt <= ${#retry_delays[@]}; attempt++)); do
delay="${retry_delays[attempt - 1]}"
if ((delay > 0)); then
log "Hermes-Git-Fetch: neuer Versuch $attempt/${#retry_delays[@]} in ${delay}s."
sleep "$delay"
fi
git_options=()
if ((attempt == ${#retry_delays[@]})); then
git_options=(-c http.version=HTTP/1.1)
log "Hermes-Git-Fetch: letzter Versuch mit HTTP/1.1."
fi
if GIT_TERMINAL_PROMPT=0 timeout --foreground 90s \
git "${git_options[@]}" -C "$HERMES_REPO" fetch --no-tags \
"$HERMES_REMOTE" \
"+refs/heads/$HERMES_BRANCH:refs/remotes/$HERMES_REMOTE/$HERMES_BRANCH"; then
((attempt == 1)) ||
log "Hermes-Git-Fetch war im Versuch $attempt/${#retry_delays[@]} erfolgreich."
return 0
fi
log "Hermes-Git-Fetch fehlgeschlagen (Versuch $attempt/${#retry_delays[@]})."
done
return 1
}
run_hermes_update_attempt() {
local attempt="$1"
local -a pipe_status
printf '\n===== Hermes update: Versuch %s/2 (%s) =====\n' \
"$attempt" "$(date --iso-8601=seconds)" >>"$update_log"
set +e
hermes update 2>&1 | tee -a "$update_log"
pipe_status=("${PIPESTATUS[@]}")
set -e
((pipe_status[1] == 0)) || return "${pipe_status[1]}"
return "${pipe_status[0]}"
}
is_retryable_update_fetch_failure() {
local current_commit dirty_after
current_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)" || return 1
[[ "$current_commit" == "$old_commit" ]] || return 1
dirty_after="$(
git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
grep -vxF '?? .install_method' || true
)"
[[ -z "$dirty_after" ]] || return 1
grep -Fq 'Fetching updates' "$update_log" || return 1
grep -Fq 'Pulling updates' "$update_log" && return 1
grep -Eqi \
'HTTP (429|500|502|503|504)|returned error: (429|500|502|503|504)|rate.?limit|could not read Username|Authentication failed|Could not resolve host|Failed to connect|Connection (timed out|reset)|TLS|early EOF|Failed to fetch updates|Network error' \
"$update_log"
}
for command in git tar zstd jq curl systemctl timeout flock hermes; do
command -v "$command" >/dev/null 2>&1 || {
notify_user "Hermes-Update abgebrochen: benötigtes Kommando fehlt: $command"
exit 1
}
done
LOCK_FILE="${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
if [[ ! -d "$HERMES_REPO/.git" ]]; then
notify_user "Hermes-Update abgebrochen: Git-Installation nicht gefunden."
exit 1
fi
case "$HERMES_REPO" in
"$HOME"/*/hermes-agent) ;;
*)
notify_user "Hermes-Update abgebrochen: unsicherer Repository-Pfad: $HERMES_REPO"
exit 1
;;
esac
dirty="$(
git -C "$HERMES_REPO" status --porcelain --untracked-files=all |
grep -vxF '?? .install_method' || true
)"
if [[ -n "$dirty" ]]; then
notify_user "Hermes-Update abgebrochen: lokale Änderungen im Hermes-Repository:
$dirty"
exit 1
fi
old_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
if ! fetch_origin_main_with_retry; then
notify_user "Hermes-Updateprüfung fehlgeschlagen: $HERMES_REMOTE/$HERMES_BRANCH ist nicht erreichbar. Gateway und Installation bleiben unverändert."
exit 1
fi
remote_commit="$(git -C "$HERMES_REPO" rev-parse "refs/remotes/$HERMES_REMOTE/$HERMES_BRANCH")"
if [[ ! "$remote_commit" =~ ^[0-9a-f]{40}$ ]]; then
notify_user "Hermes-Updateprüfung fehlgeschlagen: Remote lieferte keinen gültigen Commit."
exit 1
fi
if [[ "$remote_commit" == "$old_commit" ]]; then
notify_user "Hermes ist bereits aktuell: $old_commit"
exit 0
fi
backup_dir="$STATE_DIR/backups"
mkdir -p "$backup_dir"
timestamp="$(date +%Y%m%d-%H%M%S)"
backup="$backup_dir/hermes-$timestamp.tar.zst"
update_log="$STATE_DIR/hermes-update-$timestamp.log"
paths=("${HERMES_REPO#"$HOME/"}")
for path in \
"$HOME/.hermes/config.yaml" "$HOME/.hermes/.env" "$HOME/.hermes/auth.json" \
"${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/$GATEWAY_SERVICE"; do
[[ -e "$path" && "$path" == "$HOME/"* ]] && paths+=("${path#"$HOME/"}")
done
tar --zstd -cf "$backup" -C "$HOME" "${paths[@]}"
rollback() {
trap - ERR
set +e
systemctl --user stop "$GATEWAY_SERVICE"
if [[ "$HERMES_REPO" == "$HOME/"*/hermes-agent ]]; then
rm -rf -- "$HERMES_REPO"
tar --zstd -xf "$backup" -C "$HOME"
fi
systemctl --user daemon-reload
systemctl --user restart "$GATEWAY_SERVICE"
notify_user "Hermes-Update zurückgerollt auf $old_commit. Backup: $backup"
exit 1
}
trap rollback ERR
systemctl --user stop "$GATEWAY_SERVICE"
if run_hermes_update_attempt 1; then
update_exit=0
else
update_exit=$?
log "Hermes-Updater meldete Exitcode $update_exit."
if is_retryable_update_fetch_failure; then
log "Transienter Fetch-Fehler erkannt; Hermes-Update wird einmal wiederholt."
sleep 20
if run_hermes_update_attempt 2; then
update_exit=0
else
update_exit=$?
log "Hermes-Updater-Retry meldete Exitcode $update_exit."
fi
fi
fi
if ! hermes config check >/dev/null 2>&1; then
hermes config migrate
hermes config check
fi
systemctl --user daemon-reload
systemctl --user reset-failed "$GATEWAY_SERVICE" || true
systemctl --user restart "$GATEWAY_SERVICE"
gateway_ready=0
for ((attempt = 1; attempt <= ${GATEWAY_HEALTH_ATTEMPTS:-12}; attempt++)); do
if systemctl --user is-active --quiet "$GATEWAY_SERVICE" &&
hermes gateway status >/dev/null 2>&1; then
gateway_ready=1
break
fi
sleep "${GATEWAY_HEALTH_DELAY:-5}"
done
if ((gateway_ready == 0)); then
notify_user "Hermes-Gateway wurde nach dem Update nicht gesund. Rollback wird ausgeführt."
false
fi
if ! curl --fail --silent --max-time 10 "$LLAMA_URL/health" |
jq -e '.status == "ok"' >/dev/null; then
notify_user "Hermes und Gateway sind aktualisiert, aber der lokale Modell-Endpunkt ist nicht gesund: $LLAMA_URL. Kein Hermes-Rollback, da llama-server unabhängig ist."
fi
trap - ERR
mapfile -t old_backups < <(
find "$backup_dir" -maxdepth 1 -type f -name 'hermes-*.tar.zst' \
-printf '%T@ %p\n' | sort -nr | tail -n "+$((${BACKUP_KEEP:-3} + 1))" |
cut -d' ' -f2-
)
((${#old_backups[@]} == 0)) || rm -f -- "${old_backups[@]}"
new_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
if [[ "$new_commit" == "$old_commit" ]]; then
notify_user "Hermes-Update wurde nicht installiert (Updater-Exitcode $update_exit); der bisherige Commit $new_commit und der frisch gestartete Gateway sind gesund."
exit 1
elif ((update_exit != 0)); then
notify_user "Hermes erfolgreich aktualisiert: $old_commit -> $new_commit. Eine Updater-Restartwarnung wurde durch den frischen Gateway-Neustart behoben."
else
notify_user "Hermes erfolgreich aktualisiert: $old_commit -> $new_commit"
fi
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
runner="$(dirname -- "${BASH_SOURCE[0]}")/hermes-stack-llama-run"
case "${1:-list}" in
list)
printf '%-12s %8s %10s\n' Profil Kontext Fit-MiB \
baseline 65536 1536 balanced 49152 1536 vram-plus 49152 1280 compact 32768 1536
exit 0 ;;
baseline|balanced|vram-plus|compact) selected="$1" ;;
*) printf 'Aufruf: %s [list|baseline|balanced|vram-plus|compact]\n' "$0" >&2; exit 2 ;;
esac
[[ $# -eq 1 ]] || exit 2
# Profilwechsel nur für bewusst migrierte Konfigurationen erlauben.
# shellcheck disable=SC1090
source "$config_dir/llama.env"
[[ -n "${LLAMA_PROFILE:-}" ]] || {
printf 'Zuerst llama.env nach docs/WORKSTATION.md migrieren. Keine Änderung.\n' >&2; exit 1;
}
"$runner" --profile "$selected" --check
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
flock -n 9 || { printf 'Maintenance läuft. Später erneut versuchen.\n' >&2; exit 1; }
target="$config_dir/llama-profile.env"
if [[ -e "$target" ]]; then
backup="$(mktemp "$config_dir/llama-profile.backup.XXXXXX")"
cp -p -- "$target" "$backup"
printf 'Vorherige Profilwahl: %s\n' "$backup"
fi
candidate="$(mktemp "$config_dir/llama-profile.env.XXXXXX")"
printf 'LLAMA_PROFILE=%q\n' "$selected" >"$candidate"
mv -T -- "$candidate" "$target"
printf 'Profil %s gespeichert. Laufende Jobs bleiben unverändert.\n' "$selected"
printf 'Erst nach Jobende: hermes-stack-llama-restart (siehe docs/WORKSTATION.md).\n'
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Nur lesen: kein Modellstart, keine Inferenz, kein Neustart.
set -Euo pipefail
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
if [[ -r "$config_dir/config.env" ]]; then
# shellcheck disable=SC1090
source "$config_dir/config.env"
fi
printf '\nGPU (Gesamtspeicher, belegt, frei in MiB):\n'
nvidia-smi --query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,power.draw,temperature.gpu \
--format=csv || true
printf '\nDienstzustand:\n'
systemctl --user show "${LLAMA_SERVICE:-llama-qwen.service}" \
-p ActiveState -p SubState -p MainPID -p ExecMainStartTimestamp --no-pager || true
printf '\nLetzte Speicher-/Leistungsdaten (keine vollständigen Prompts):\n'
journalctl --user -u "${LLAMA_SERVICE:-llama-qwen.service}" -n 1000 --no-pager |
rg 'offload|buffer size|n_ctx|prompt eval time|eval time|sleeping|waking' |
tail -n 35 || true
printf '\nDas ist eine Momentaufnahme, kein Spitzenlast-Benchmark.\n'
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -Eeuo pipefail
if [[ "${1:-}" != --jobs-finished || $# -ne 1 ]]; then
printf 'Stoppt laufende Inferenz! Erst nach Jobende ausführen:\n %s --jobs-finished\n' "$0" >&2
exit 2
fi
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
# shellcheck disable=SC1090
source "$config_dir/config.env"
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/hermes-stack-hermes-update.lock"
flock -n 9 || { printf 'Maintenance läuft; kein Neustart.\n' >&2; exit 1; }
"$(dirname -- "${BASH_SOURCE[0]}")/hermes-stack-llama-run" --check
systemctl --user restart "$LLAMA_SERVICE"
printf 'Neustart ausgelöst. Modell kann noch laden; Logs und Inferenz prüfen.\n'
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -Eeuo pipefail
config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance"
CONFIG_FILE="${HERMES_LLAMA_CONFIG:-$config_dir/llama.env}"
LLAMA_ARGS=()
# shellcheck disable=SC1090
source "$CONFIG_FILE"
mode=run
profile_override=""
while (($#)); do
case "$1" in
--check) mode=check ;;
--print) mode=print ;;
--profile) [[ $# -ge 2 ]] || exit 2; profile_override="$2"; shift ;;
*) printf 'Aufruf: %s [--check|--print] [--profile NAME]\n' "$0" >&2; exit 2 ;;
esac
shift
done
if [[ -n "${LLAMA_PROFILE:-}" && -r "$config_dir/llama-profile.env" ]]; then
# shellcheck disable=SC1090
source "$config_dir/llama-profile.env"
fi
[[ -z "$profile_override" ]] || LLAMA_PROFILE="$profile_override"
if [[ -z "${LLAMA_SERVER_BIN:-}" ]]; then
LLAMA_SERVER_BIN="$HOME/.local/opt/llama.cpp/current/bin/llama-server"
if [[ ! -x "$LLAMA_SERVER_BIN" ]]; then
LLAMA_SERVER_BIN="$(command -v llama-server || true)"
fi
fi
[[ -x "$LLAMA_SERVER_BIN" ]] || {
printf 'llama-server nicht ausführbar: %s\n' "$LLAMA_SERVER_BIN" >&2
exit 1
}
model_args=()
if [[ -n "${LLAMA_MODEL:-}" && -n "${LLAMA_HF_REPO:-}" ]]; then
printf 'Nur LLAMA_MODEL ODER LLAMA_HF_REPO setzen.\n' >&2
exit 1
elif [[ -n "${LLAMA_MODEL:-}" ]]; then
[[ -r "$LLAMA_MODEL" ]] || { printf 'Modell nicht lesbar.\n' >&2; exit 1; }
model_args=(--model "$LLAMA_MODEL")
elif [[ -n "${LLAMA_HF_REPO:-}" ]]; then
model_args=(--hf-repo "$LLAMA_HF_REPO")
else
printf 'LLAMA_MODEL oder LLAMA_HF_REPO fehlt.\n' >&2
exit 1
fi
if [[ -n "${LLAMA_MMPROJ:-}" ]]; then
[[ -r "$LLAMA_MMPROJ" ]] || { printf 'Projektor nicht lesbar.\n' >&2; exit 1; }
model_args+=(--mmproj "$LLAMA_MMPROJ")
fi
[[ -z "${LLAMA_ALIAS:-}" ]] || model_args+=(--alias "$LLAMA_ALIAS")
profile_args=()
if [[ -n "${LLAMA_PROFILE:-}" ]]; then
case "$LLAMA_PROFILE" in
baseline) ctx=65536; margin=1536 ;;
balanced) ctx=49152; margin=1536 ;;
vram-plus) ctx=49152; margin=1280 ;;
compact) ctx=32768; margin=1536 ;;
*) printf 'Unbekanntes Profil: %s\n' "$LLAMA_PROFILE" >&2; exit 2 ;;
esac
profile_args=(
--ctx-size "$ctx" --parallel 1 --jinja
--reasoning auto --reasoning-budget 512 --reasoning-preserve
--flash-attn auto --threads 6 --threads-batch 16
--n-gpu-layers auto --fit on --fit-target "$margin"
--cache-type-k q8_0 --cache-type-v q8_0
--sleep-idle-seconds 300 --image-min-tokens 1024
--cors-origins localhost --no-ui
)
# Alte vollständige LLAMA_ARGS nicht still mit einem Profil vermischen.
if ((${#LLAMA_ARGS[@]})); then
printf 'Bei Profilbetrieb LLAMA_ARGS leeren; eigene vollständige Args nur ohne LLAMA_PROFILE.\n' >&2
exit 2
fi
fi
args=("${model_args[@]}" --host "${LLAMA_HOST:-127.0.0.1}" --port "${LLAMA_PORT:-8080}"
"${profile_args[@]}" "${LLAMA_ARGS[@]}")
if [[ "$mode" == print ]]; then
printf '%q ' "$LLAMA_SERVER_BIN" "${args[@]}"
printf '\n'
exit 0
fi
# Kein Download/Modellstart: nur prüfen, ob die installierte Version die Namen kennt.
help_text="$(timeout 30 "$LLAMA_SERVER_BIN" --help 2>&1)" || {
printf 'llama-server --help fehlgeschlagen.\n' >&2; exit 1;
}
for arg in "${args[@]}"; do
[[ "$arg" == --* ]] || continue
option="${arg%%=*}"
if ! grep -Eq -- "(^|[[:space:],])${option}([[:space:],=]|$)" <<<"$help_text"; then
printf 'Die installierte llama.cpp-Version kennt %s nicht. Kein automatisches Weglassen.\n' "$option" >&2
exit 1
fi
done
if [[ "$mode" == check ]]; then
printf 'Konfiguration und Optionsnamen OK; GPU/Modell/Optionswerte noch nicht laufzeitgetestet.\n'
exit 0
fi
exec "$LLAMA_SERVER_BIN" "${args[@]}"
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -Euo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
log() {
printf '%s\n' "$*" | systemd-cat -t hermes-stack-maintenance 2>/dev/null || true
}
text="${*:-}"
if [[ -z "$text" ]]; then
printf 'Verwendung: %s TEXT\n' "${0##*/}" >&2
exit 2
fi
log "$text"
if [[ ! -r "$CONFIG_FILE" ]]; then
log "Benachrichtigung übersprungen: $CONFIG_FILE ist nicht lesbar."
exit 0
fi
set +u
# shellcheck disable=SC1090
source "$CONFIG_FILE" || {
set -u
log "Benachrichtigung übersprungen: Maintenance-Konfiguration ist ungültig."
exit 0
}
telegram_notify="${TELEGRAM_NOTIFY:-0}"
signal_notify="${SIGNAL_NOTIFY:-0}"
if [[ -r "${HERMES_ENV:-}" ]] &&
[[ "$telegram_notify" == 1 || "$signal_notify" == 1 ]]; then
set -a
# shellcheck disable=SC1090
source "$HERMES_ENV" || true
set +a
fi
set -u
if [[ "$telegram_notify" == 1 &&
-n "${TELEGRAM_BOT_TOKEN:-}" &&
-n "${TELEGRAM_HOME_CHANNEL:-}" ]]; then
if ! curl --fail --silent --show-error --max-time 15 \
--request POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${TELEGRAM_HOME_CHANNEL}" \
--data-urlencode "text=${text}" >/dev/null; then
log "Telegram-Benachrichtigung konnte nicht zugestellt werden."
fi
fi
if [[ "$signal_notify" == 1 &&
-n "${SIGNAL_HTTP_URL:-}" &&
-n "${SIGNAL_ACCOUNT:-}" &&
-n "${SIGNAL_HOME_CHANNEL:-}" ]]; then
if [[ "$SIGNAL_HOME_CHANNEL" == group:* ]]; then
signal_payload="$(
jq -nc --arg account "$SIGNAL_ACCOUNT" --arg message "$text" \
--arg group_id "${SIGNAL_HOME_CHANNEL#group:}" \
'{jsonrpc:"2.0",method:"send",params:{account:$account,message:$message,groupId:$group_id},id:"hermes-maintenance"}'
)" || signal_payload=""
else
signal_payload="$(
jq -nc --arg account "$SIGNAL_ACCOUNT" --arg message "$text" \
--arg recipient "$SIGNAL_HOME_CHANNEL" \
'{jsonrpc:"2.0",method:"send",params:{account:$account,message:$message,recipient:[$recipient]},id:"hermes-maintenance"}'
)" || signal_payload=""
fi
if [[ -z "$signal_payload" ]] ||
! curl --fail --silent --show-error --max-time 30 \
--header 'Content-Type: application/json' \
--data-binary "$signal_payload" \
"${SIGNAL_HTTP_URL%/}/api/v1/rpc" |
jq -e '((.error // null) == null) and (.result != null)' >/dev/null; then
log "Signal-Benachrichtigung konnte nicht zugestellt werden."
fi
fi
exit 0
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -Euo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
mkdir -p "$STATE_DIR"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
set +u
set -a
# shellcheck disable=SC1090
source "$HERMES_ENV" || true
set +a
set -u
state_file="$STATE_DIR/signal-health-state"
signal_url="${SIGNAL_HTTP_URL:-}"
[[ -n "$signal_url" ]] || exit 0
check_signal() {
local status
status="$(curl --silent --output /dev/null --max-time 5 \
--write-out '%{http_code}' "${signal_url%/}/api/v1/check" || true)"
[[ "$status" =~ ^2[0-9][0-9]$ ]]
}
if check_signal; then
current=healthy
else
systemctl --user restart "$SIGNAL_SERVICE" || true
sleep 5
check_signal && current=healthy || current=unhealthy
fi
previous="$(cat "$state_file" 2>/dev/null || true)"
if [[ "$current" == unhealthy && "$previous" != unhealthy ]]; then
"$NOTIFIER" "Hermes-Stack: signal-cli ist nicht erreichbar; Neustart wurde versucht." || true
elif [[ "$current" == healthy && "$previous" == unhealthy ]]; then
"$NOTIFIER" "Hermes-Stack: signal-cli ist wieder erreichbar." || true
fi
printf '%s\n' "$current" >"$state_file"
[[ "$current" == healthy ]]
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/signal-cli.env"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
[[ -x "$SIGNAL_CLI_BIN" ]] || {
printf 'signal-cli nicht ausführbar: %s\n' "$SIGNAL_CLI_BIN" >&2
exit 1
}
[[ -n "$SIGNAL_ACCOUNT" ]] || {
printf 'SIGNAL_ACCOUNT ist nicht konfiguriert.\n' >&2
exit 1
}
args=(--account "$SIGNAL_ACCOUNT")
if [[ -n "${SIGNAL_CONFIG_DIR:-}" ]]; then
mkdir -p "$SIGNAL_CONFIG_DIR"
args=(--config "$SIGNAL_CONFIG_DIR" "${args[@]}")
fi
exec "$SIGNAL_CLI_BIN" "${args[@]}" daemon \
--http "${SIGNAL_HTTP_BIND:-127.0.0.1:8090}"
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/config.env"
SIGNAL_CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hermes-stack-maintenance/signal-cli.env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
# shellcheck disable=SC1090
source "$SIGNAL_CONFIG_FILE"
version="${1:-}"
if [[ -z "$version" ]]; then
latest_url="$(curl -LsS --max-time 30 -o /dev/null -w '%{url_effective}' \
https://github.com/AsamK/signal-cli/releases/latest)"
version="${latest_url##*/v}"
fi
[[ "$version" =~ ^[0-9]+(\.[0-9]+)+$ ]] || {
printf 'Ungültige signal-cli-Version: %s\n' "$version" >&2
exit 1
}
old_version="unbekannt"
if [[ -x "$SIGNAL_CLI_BIN" ]]; then
old_version="$("$SIGNAL_CLI_BIN" --version 2>/dev/null |
grep -Eo '[0-9]+(\.[0-9]+)+' | head -n1 || true)"
fi
[[ -n "$old_version" ]] || old_version="unbekannt"
tmp_dir="$(mktemp -d)"
trap 'rm -rf -- "$tmp_dir"' EXIT
archive="$tmp_dir/signal-cli.tar.gz"
url="https://github.com/AsamK/signal-cli/releases/download/v${version}/signal-cli-${version}.tar.gz"
curl --fail --location --show-error "$url" -o "$archive"
tar -xzf "$archive" -C "$tmp_dir"
binary="$(find "$tmp_dir" -type f -path '*/bin/signal-cli' -print -quit)"
[[ -n "$binary" ]] || {
printf 'signal-cli-Binary wurde im Archiv nicht gefunden.\n' >&2
exit 1
}
source_root="$(dirname "$(dirname "$binary")")"
timestamp="$(date +%Y%m%d-%H%M%S)"
backup_dir="$STATE_DIR/backups/signal-cli/$timestamp"
mkdir -p "$backup_dir"
old_target=""
if [[ -e "$SIGNAL_CLI_BIN" || -L "$SIGNAL_CLI_BIN" ]]; then
old_target="$(readlink -f "$SIGNAL_CLI_BIN" 2>/dev/null || true)"
if [[ -n "$old_target" ]]; then
old_root="$(dirname "$(dirname "$old_target")")"
[[ -d "$old_root" ]] && cp -a "$old_root" "$backup_dir/program"
fi
fi
if [[ -d "${SIGNAL_CONFIG_DIR:-}" ]]; then
tar --zstd -cf "$backup_dir/signal-data.tar.zst" \
-C "$(dirname "$SIGNAL_CONFIG_DIR")" "$(basename "$SIGNAL_CONFIG_DIR")"
fi
was_active=0
systemctl --user is-active --quiet "$SIGNAL_SERVICE" && was_active=1
((was_active == 0)) || systemctl --user stop "$SIGNAL_SERVICE"
target="$HOME/.local/opt/signal-cli-$version"
mkdir -p "$target" "$(dirname "$SIGNAL_CLI_BIN")"
cp -a "$source_root/." "$target/"
ln -sfn "$target/bin/signal-cli" "$SIGNAL_CLI_BIN"
if ((was_active)); then
systemctl --user restart "$SIGNAL_SERVICE"
sleep 5
set +u
set -a
# shellcheck disable=SC1090
source "$HERMES_ENV" || true
set +a
set -u
signal_http_url="${SIGNAL_HTTP_URL:-}"
status=""
if [[ -n "$signal_http_url" ]]; then
status="$(curl --silent --output /dev/null --max-time 10 \
--write-out '%{http_code}' "${signal_http_url%/}/api/v1/check" || true)"
fi
if [[ ! "$status" =~ ^2[0-9][0-9]$ ]]; then
systemctl --user stop "$SIGNAL_SERVICE" || true
if [[ -n "$old_target" ]]; then
ln -sfn "$old_target" "$SIGNAL_CLI_BIN"
systemctl --user restart "$SIGNAL_SERVICE" || true
fi
"$NOTIFIER" "signal-cli-Update auf $version fehlgeschlagen; Binary wurde auf $old_version zurückgestellt. Backup: $backup_dir" || true
exit 1
fi
fi
mapfile -t old_backups < <(
find "$STATE_DIR/backups/signal-cli" -mindepth 1 -maxdepth 1 -type d \
-printf '%T@ %p\n' | sort -nr | tail -n +4 | cut -d' ' -f2-
)
((${#old_backups[@]} == 0)) || rm -rf -- "${old_backups[@]}"
"$NOTIFIER" "signal-cli wurde erfolgreich aktualisiert: $old_version -> $version. Daten- und Binary-Backup: $backup_dir" || true
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -Euo pipefail
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hermes-stack-maintenance"
NOTIFIER="$HOME/.local/libexec/hermes-stack-notify"
mkdir -p "$STATE_DIR"
command -v signal-cli >/dev/null 2>&1 || exit 0
installed="$(signal-cli --version 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+)+' | head -n1)"
latest_url="$(curl -LsS --max-time 30 -o /dev/null -w '%{url_effective}' \
https://github.com/AsamK/signal-cli/releases/latest || true)"
latest="${latest_url##*/v}"
[[ "$installed" =~ ^[0-9]+(\.[0-9]+)+$ ]] || exit 1
[[ "$latest" =~ ^[0-9]+(\.[0-9]+)+$ ]] || exit 1
if [[ "$(printf '%s\n%s\n' "$installed" "$latest" | sort -V | tail -n1)" == "$latest" &&
"$installed" != "$latest" ]]; then
"$NOTIFIER" "signal-cli-Update verfügbar: $installed -> $latest. Manuell ausführen: ~/.local/libexec/hermes-stack-signal-update $latest" || true
fi
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Baut in einem neuen Verzeichnis. Kein Ersetzen von /usr/bin oder laufenden Builds.
set -Eeuo pipefail
umask 077
revision=""
jobs=4
cuda_arch=86
base_dir="$HOME/.local/opt/llama.cpp"
usage() {
printf 'Aufruf: %s --ref TAG_ODER_COMMIT [--jobs N] [--cuda-arch 86] [--prefix ABSOLUTER_PFAD]\n' "$0"
printf 'Explizit --ref master für aktuellen Stand; aufgelöster Commit wird gespeichert.\n'
}
while (($#)); do
case "$1" in
--ref) [[ $# -ge 2 ]] || exit 2; revision="$2"; shift ;;
--jobs) [[ $# -ge 2 ]] || exit 2; jobs="$2"; shift ;;
--cuda-arch) [[ $# -ge 2 ]] || exit 2; cuda_arch="$2"; shift ;;
--prefix) [[ $# -ge 2 ]] || exit 2; base_dir="$2"; shift ;;
-h|--help) usage; exit 0 ;;
*) usage >&2; exit 2 ;;
esac
shift
done
[[ "$revision" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]] || { usage >&2; exit 2; }
[[ "$jobs" =~ ^[1-9][0-9]*$ && "$cuda_arch" =~ ^[1-9][0-9]*$ ]] || exit 2
[[ "$base_dir" == /* && "$base_dir" != / && "$base_dir" != "$HOME" ]] || exit 2
for dependency in git cmake ninja c++ nvidia-smi; do
command -v "$dependency" >/dev/null || {
printf '%s fehlt. Siehe docs/LLAMA_INSTALL.md.\n' "$dependency" >&2; exit 1;
}
done
nvidia-smi >/dev/null
nvcc_path="${CUDACXX:-/opt/cuda/bin/nvcc}"
[[ -x "$nvcc_path" ]] || { printf 'CUDA nvcc fehlt: %s\n' "$nvcc_path" >&2; exit 1; }
mkdir -p "$base_dir"
exec 9>"$base_dir/build.lock"
flock -n 9 || { printf 'Ein CUDA-Build läuft bereits.\n' >&2; exit 1; }
build_dir="$(mktemp -d "$base_dir/build.XXXXXXXX")"
printf 'Neuer Build unter %s (bleibt auch bei Fehler zur Diagnose erhalten).\n' "$build_dir"
git init -q "$build_dir/source"
git -C "$build_dir/source" remote add origin https://github.com/ggml-org/llama.cpp.git
GIT_TERMINAL_PROMPT=0 timeout 300 git -C "$build_dir/source" fetch --depth 1 origin "$revision"
git -C "$build_dir/source" checkout --detach FETCH_HEAD
commit="$(git -C "$build_dir/source" rev-parse HEAD)"
cmake -S "$build_dir/source" -B "$build_dir/build" -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=ON -DBUILD_SHARED_LIBS=OFF \
-DCMAKE_CUDA_COMPILER="$nvcc_path" -DCMAKE_CUDA_ARCHITECTURES="$cuda_arch"
cmake --build "$build_dir/build" --parallel "$jobs" --target llama-server llama-cli llama-bench
cp -a -- "$build_dir/build/bin" "$build_dir/bin"
"$build_dir/bin/llama-server" --version
"$build_dir/bin/llama-cli" --version
printf 'upstream=https://github.com/ggml-org/llama.cpp.git\nref=%s\ncommit=%s\ncuda_arch=%s\nbuilt=%s\n' \
"$revision" "$commit" "$cuda_arch" "$(date --iso-8601=seconds)" >"$build_dir/BUILD-INFO.txt"
if [[ ! -e "$base_dir/current" && ! -L "$base_dir/current" ]]; then
ln -s -- "$build_dir" "$base_dir/current"
printf 'Erster verwalteter Build ausgewählt; noch kein Dienst gestartet.\n'
else
printf 'Bestehender current-Build bleibt unverändert.\n'
fi
printf 'Neue Binary: %s/bin/llama-server\nCommit: %s\n' "$build_dir" "$commit"
printf 'Zur bewussten Auswahl LLAMA_SERVER_BIN in llama.env setzen. Keine automatische Aktualisierung.\n'
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
exec "$ROOT_DIR/libexec/hermes-stack-signal-update" "$@"
+6
View File
@@ -0,0 +1,6 @@
[Unit]
Description=Check and recover Hermes stack
[Service]
Type=oneshot
ExecStart=%h/.local/libexec/hermes-stack-health
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Periodic Hermes stack health check
[Timer]
OnBootSec=2min
OnUnitActiveSec=10min
AccuracySec=30s
Unit=hermes-stack-health.service
[Install]
WantedBy=timers.target
@@ -0,0 +1,9 @@
[Unit]
Description=Update Hermes Agent with backup and rollback
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=%h/.local/libexec/hermes-stack-hermes-update
TimeoutStartSec=30min
@@ -0,0 +1,11 @@
[Unit]
Description=Weekly Hermes Agent update
[Timer]
OnCalendar=weekly
Persistent=true
RandomizedDelaySec=2h
Unit=hermes-stack-hermes-update.service
[Install]
WantedBy=timers.target
@@ -0,0 +1,6 @@
[Unit]
Description=Check and recover signal-cli for Hermes
[Service]
Type=oneshot
ExecStart=%h/.local/libexec/hermes-stack-signal-health
@@ -0,0 +1,11 @@
[Unit]
Description=Periodic signal-cli health check
[Timer]
OnBootSec=3min
OnUnitActiveSec=10min
AccuracySec=30s
Unit=hermes-stack-signal-health.service
[Install]
WantedBy=timers.target
@@ -0,0 +1,8 @@
[Unit]
Description=Check for a new signal-cli release
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=%h/.local/libexec/hermes-stack-signal-update-check
@@ -0,0 +1,11 @@
[Unit]
Description=Daily signal-cli update check
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
Unit=hermes-stack-signal-update-check.service
[Install]
WantedBy=timers.target
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=Local llama.cpp Qwen model server
After=network-online.target
[Service]
Type=simple
ExecStart=%h/.local/libexec/hermes-stack-llama-run
Restart=on-failure
RestartSec=10
TimeoutStopSec=60
[Install]
WantedBy=default.target
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=signal-cli daemon for Hermes
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=%h/.local/libexec/hermes-stack-signal-run
Restart=always
RestartSec=10
TimeoutStopSec=30
[Install]
WantedBy=default.target
Executable
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
failures=0
for script in "$ROOT_DIR/install.sh" "$ROOT_DIR/uninstall.sh" \
"$ROOT_DIR"/libexec/* "$ROOT_DIR"/scripts/*.sh "$ROOT_DIR"/config/*.example; do
if bash -n "$script"; then
printf 'OK bash -n %s\n' "${script#"$ROOT_DIR/"}"
else
failures=$((failures + 1))
fi
done
if command -v shellcheck >/dev/null 2>&1; then
shellcheck -x "$ROOT_DIR/install.sh" "$ROOT_DIR/uninstall.sh" \
"$ROOT_DIR"/libexec/* "$ROOT_DIR"/scripts/*.sh || failures=$((failures + 1))
else
printf 'HINWEIS: shellcheck fehlt; Lint wird nur in entsprechend ausgestatteter CI ausgeführt.\n'
fi
python3 "$ROOT_DIR/tests/test_llama.py" || failures=$((failures + 1))
if rg -n --hidden --glob '!tests/smoke.sh' --glob '!tests/test_llama.py' --glob '!.git/**' \
'(TELEGRAM_BOT_TOKEN|SIGNAL_ACCOUNT|API_KEY)=("|'"'"')?[A-Za-z0-9+]' "$ROOT_DIR"; then
printf 'Möglicher eingecheckter Secret-Wert gefunden.\n' >&2
failures=$((failures + 1))
fi
for unit in "$ROOT_DIR"/systemd/user/*.{service,timer}; do
[[ -s "$unit" ]] || {
printf 'Leere Unit: %s\n' "$unit" >&2
failures=$((failures + 1))
}
done
((failures == 0))
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""Isolierte Vertragstests. Kein HOME-Override, GPU, Netzwerk oder echter systemd-Aufruf."""
import os
import json
from pathlib import Path
import shlex
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
RUNNER = ROOT / "libexec/hermes-stack-llama-run"
PROFILE = ROOT / "libexec/hermes-stack-llama-profile"
RESTART = ROOT / "libexec/hermes-stack-llama-restart"
OPTIONS = """--model --hf-repo --mmproj --alias --host --port --ctx-size --parallel
--jinja --reasoning --reasoning-budget --reasoning-preserve --flash-attn --threads
--threads-batch --n-gpu-layers --fit --fit-target --cache-type-k --cache-type-v
--sleep-idle-seconds --image-min-tokens --cors-origins --no-ui"""
class LlamaTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="hermes-test-", dir=ROOT.parent)
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.config = self.root / "config/hermes-stack-maintenance"
self.config.mkdir(parents=True)
self.bin = self.root / "bin"
self.bin.mkdir()
self.record = self.root / "calls"
self.env = dict(os.environ, XDG_CONFIG_HOME=str(self.config.parent),
XDG_STATE_HOME=str(self.root / "state"),
XDG_RUNTIME_DIR=str(self.root),
PATH=str(self.bin) + ":" + os.environ["PATH"],
TEST_RECORD=str(self.record))
self.server = self.bin / "llama-server"
self.write_server()
self.write_config()
self.program("systemctl", 'printf "%s\\n" "$*" >>"$TEST_RECORD"\nexit 0\n')
self.program("notify-test", "exit 0\n")
def program(self, name, body):
path = self.bin / name
path.write_text("#!/usr/bin/env bash\nset -eu\n" + body)
path.chmod(0o755)
return path
def write_server(self, options=OPTIONS):
self.program("llama-server", 'if [[ "${1:-}" == --help ]]; then\n'
f' printf "%s\\n" {shlex.quote(options)}\n exit 0\nfi\n'
'printf "%s\\n" "$@" >"$TEST_RECORD"\n')
def write_config(self, extra="", profile="baseline", model="", hf="test/model:Q4_K_M"):
content = (f'LLAMA_SERVER_BIN={shlex.quote(str(self.server))}\n'
f'LLAMA_MODEL={shlex.quote(model)}\nLLAMA_HF_REPO={shlex.quote(hf)}\n'
f'LLAMA_PROFILE={shlex.quote(profile)}\n'
'LLAMA_ALIAS=qwen38-27b-local\nLLAMA_ARGS=()\n')
(self.config / "llama.env").write_text(content + extra)
(self.config / "config.env").write_text(
"LLAMA_SERVICE=llama-qwen.service\nGATEWAY_SERVICE=hermes-gateway.service\n"
"LLAMA_URL=http://127.0.0.1:8080\n"
f'NOTIFIER={shlex.quote(str(self.bin / "notify-test"))}\n')
def run_tool(self, script, *args, success=True):
result = subprocess.run(["bash", str(script), *args], env=self.env,
text=True, capture_output=True, timeout=8)
if success:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
else:
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
return result
def test_profile_values(self):
for name, ctx, margin in [("baseline", "65536", "1536"),
("balanced", "49152", "1536"),
("vram-plus", "49152", "1280"),
("compact", "32768", "1536")]:
with self.subTest(name=name):
result = self.run_tool(RUNNER, "--profile", name, "--print")
args = shlex.split(result.stdout)
self.assertEqual(args[args.index("--ctx-size") + 1], ctx)
self.assertEqual(args[args.index("--fit-target") + 1], margin)
self.assertEqual(args.count("--ctx-size"), 1)
self.assertIn("qwen38-27b-local", args)
def test_check_does_not_run_inference(self):
self.run_tool(RUNNER, "--check")
self.assertFalse(self.record.exists())
def test_unsupported_option_rejected(self):
self.write_server(OPTIONS.replace("--fit-target", ""))
result = self.run_tool(RUNNER, "--check", success=False)
self.assertIn("--fit-target", result.stderr)
self.assertFalse(self.record.exists())
def test_execution_uses_array(self):
self.run_tool(RUNNER)
args = self.record.read_text().splitlines()
self.assertIn("--hf-repo", args)
self.assertIn("test/model:Q4_K_M", args)
def test_local_legacy_config_and_path_spaces(self):
model = self.root / "a model.gguf"
model.touch()
self.write_config(profile="", model=str(model), hf="",
extra="LLAMA_ARGS=(--ctx-size 8192 --parallel 1)\n")
self.run_tool(RUNNER)
args = self.record.read_text().splitlines()
self.assertIn(str(model), args)
self.assertNotIn("--fit", args)
def test_model_source_conflict(self):
self.write_config(model="/some/model.gguf")
self.run_tool(RUNNER, "--check", success=False)
def test_missing_model(self):
self.write_config(model="/does/not/exist.gguf", hf="")
self.run_tool(RUNNER, "--check", success=False)
def test_missing_model_source(self):
self.write_config(hf="")
self.run_tool(RUNNER, "--check", success=False)
def test_unknown_profile(self):
self.run_tool(RUNNER, "--profile", "../unknown", "--check", success=False)
def test_profile_rejects_legacy_args(self):
self.write_config(extra="LLAMA_ARGS=(--ctx-size 64)\n")
self.run_tool(RUNNER, "--check", success=False)
def test_profile_selection_preserves_config(self):
before = (self.config / "llama.env").read_bytes()
self.run_tool(PROFILE, "balanced")
self.assertEqual((self.config / "llama.env").read_bytes(), before)
self.assertFalse(self.record.exists())
result = self.run_tool(RUNNER, "--print")
self.assertIn("49152", result.stdout)
self.run_tool(PROFILE, "baseline")
self.assertEqual(len(list(self.config.glob("llama-profile.backup.*"))), 1)
self.assertEqual((self.config / "llama-profile.env").stat().st_mode & 0o777, 0o600)
def test_profile_does_not_migrate_implicitly(self):
self.write_config(profile="")
self.run_tool(PROFILE, "balanced", success=False)
self.assertFalse((self.config / "llama-profile.env").exists())
def test_restart_requires_confirmation(self):
self.run_tool(RESTART, success=False)
self.assertFalse(self.record.exists())
def test_restart_after_check(self):
self.run_tool(RESTART, "--jobs-finished")
self.assertEqual(self.record.read_text(), "--user restart llama-qwen.service\n")
def test_failed_check_does_not_restart(self):
self.write_server("")
self.run_tool(RESTART, "--jobs-finished", success=False)
self.assertFalse(self.record.exists())
def test_lock_blocks_restart_and_profile_change(self):
import fcntl
with (self.root / "hermes-stack-hermes-update.lock").open("w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
self.run_tool(RESTART, "--jobs-finished", success=False)
self.run_tool(PROFILE, "balanced", success=False)
self.assertFalse(self.record.exists())
self.assertFalse((self.config / "llama-profile.env").exists())
def test_failed_http_does_not_restart_backend(self):
self.program("hermes", "exit 0\n")
self.program("curl", "exit 28\n")
self.run_tool(ROOT / "libexec/hermes-stack-health", success=False)
self.assertNotIn("restart llama", self.record.read_text())
def test_installer_requires_explicit_ref(self):
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", success=False)
self.run_tool(ROOT / "install.sh", "--install-llama", success=False)
def test_installer_help(self):
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", "--help")
self.run_tool(ROOT / "install.sh", "--help")
def prepare_build_mocks(self):
self.env["CUDACXX"] = str(self.program("nvcc", "exit 0\n"))
self.program("nvidia-smi", "exit 0\n")
self.program("ninja", "exit 0\n")
self.program("c++", "exit 0\n")
self.program("git", 'if [[ "$*" == *rev-parse* ]]; then\n'
'printf "%s\\n" 0123456789012345678901234567890123456789\nfi\n')
self.program("cmake", 'if [[ "${1:-}" == --build ]]; then\n'
' [[ "${TEST_BUILD_FAIL:-0}" != 1 ]] || exit 1\n'
' mkdir -p "$2/bin"\n'
' for name in llama-server llama-cli llama-bench; do\n'
' cp "$CUDACXX" "$2/bin/$name"\n'
' done\nfi\n')
def test_build_publishes_all_three_tools(self):
self.prepare_build_mocks()
prefix = self.root / "managed"
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", "--ref", "master",
"--prefix", str(prefix))
for name in ("llama-server", "llama-cli", "llama-bench"):
self.assertTrue((prefix / "current/bin" / name).is_file())
manifest = (prefix / "current/BUILD-INFO.txt").read_text()
self.assertIn("0123456789012345678901234567890123456789", manifest)
self.assertIn("cuda_arch=86", manifest)
def test_build_preserves_existing_selection(self):
self.prepare_build_mocks()
prefix = self.root / "managed"
prefix.mkdir()
old = prefix / "old"
old.mkdir()
(prefix / "current").symlink_to(old)
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", "--ref", "master",
"--prefix", str(prefix))
self.assertEqual((prefix / "current").resolve(), old)
def test_failed_build_does_not_activate(self):
self.prepare_build_mocks()
self.env["TEST_BUILD_FAIL"] = "1"
prefix = self.root / "managed"
self.run_tool(ROOT / "scripts/install-llama-cpp.sh", "--ref", "master",
"--prefix", str(prefix), success=False)
self.assertFalse((prefix / "current").exists())
def signal_fixture(self, target, response):
env_file = self.root / "hermes.env"
env_file.write_text('SIGNAL_HTTP_URL=http://127.0.0.1:8090\n'
'SIGNAL_ACCOUNT=test-account\n'
f'SIGNAL_HOME_CHANNEL={shlex.quote(target)}\n')
with (self.config / "config.env").open("a") as config:
config.write(f'HERMES_ENV={shlex.quote(str(env_file))}\n'
'SIGNAL_NOTIFY=1\nTELEGRAM_NOTIFY=0\n')
self.program("systemd-cat", "exit 0\n")
self.program("curl", 'while (($#)); do\n'
' if [[ "$1" == --data-binary ]]; then\n'
' printf "%s" "$2" >"$TEST_RECORD"\n fi\n shift\ndone\n'
f'printf "%s\\n" {shlex.quote(response)}\n')
def test_signal_direct_payload(self):
self.signal_fixture("test-recipient", '{"result":{"timestamp":1}}')
message = 'Test "Anführungszeichen"\nzweite Zeile'
self.run_tool(ROOT / "libexec/hermes-stack-notify", message)
payload = json.loads(self.record.read_text())
self.assertEqual(payload["params"]["recipient"], ["test-recipient"])
self.assertEqual(payload["params"]["message"], message)
def test_signal_group_payload(self):
self.signal_fixture("group:test-group", '{"result":{"timestamp":1}}')
self.run_tool(ROOT / "libexec/hermes-stack-notify", "Test")
payload = json.loads(self.record.read_text())
self.assertEqual(payload["params"]["groupId"], "test-group")
self.assertNotIn("recipient", payload["params"])
def test_signal_rpc_failure_is_nonfatal(self):
self.signal_fixture("test-recipient", '{"error":{"code":-1}}')
self.run_tool(ROOT / "libexec/hermes-stack-notify", "Test")
if __name__ == "__main__":
unittest.main(verbosity=2)
Executable
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Entfernt ausschließlich die von diesem Repository verwalteten Dateien.
# Hermes-Daten, Modelle, Signal-Sitzung, Konfigurationen und Backups bleiben erhalten.
units=(
hermes-stack-hermes-update.service hermes-stack-hermes-update.timer
hermes-stack-health.service hermes-stack-health.timer
hermes-stack-signal-health.service hermes-stack-signal-health.timer
hermes-stack-signal-update-check.service hermes-stack-signal-update-check.timer
llama-qwen.service signal-cli.service
)
systemctl --user disable --now \
hermes-stack-hermes-update.timer \
hermes-stack-health.timer \
hermes-stack-signal-health.timer \
hermes-stack-signal-update-check.timer 2>/dev/null || true
for unit in "${units[@]}"; do
rm -f -- "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/$unit"
done
for file in \
hermes-stack-notify hermes-stack-hermes-update hermes-stack-health \
hermes-stack-signal-health hermes-stack-signal-update-check hermes-stack-signal-update \
hermes-stack-llama-run hermes-stack-llama-profile hermes-stack-llama-restart \
hermes-stack-llama-report \
hermes-stack-signal-run hermes-stack-doctor; do
rm -f -- "$HOME/.local/libexec/$file"
done
systemctl --user daemon-reload
printf 'Deployment-Dateien entfernt; Benutzerdaten und Konfigurationen wurden beibehalten.\n'