bc60c6fefc
Scope-Entscheid 17.07.: MC2 bleibt Box-Zentrale (KEINE Homelab-Zentrale) - Infra-Radar (Proxmox/PBS/QNAP/Gitea) geht als eigenes Projekt an Lucys Queue. - restore-probe.sh (Cron 1. d. M. 06:40, no-agent): stellt WIRKLICH wieder her - PBS-Canary aus hermes.pxar (selektiv via --pattern, sudo-frei ueber die User-Unit-Zugaenge) + Tarball-Probe + Frische-Check <36h; Erstlauf-Fallbacks (llamaswap.pxar / Kern-Datei) bis der Canary in den Sicherungen ankommt. - venv-audit.sh (Cron 2. d. M. 06:40, no-agent): pip outdated + pip-audit-CVEs fuer mem0-/voice-venv ueber eigenes Audit-venv; Funde -> Kanban-Karte je Monat, Update bleibt Commander-Entscheid. - self-smoke: Disk-Waechter (Check 7, Schwelle 85%, nennt die groessten Brocken). - Trend-Radar-Watchlist: neue Typen github_release_major + pypi; Eintraege electron-major (PC-Shell auf 33!) und pocket-tts (PyPI-Versionswatch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
196 lines
11 KiB
Bash
196 lines
11 KiB
Bash
#!/usr/bin/env bash
|
||
# Tägliche Selbst-Smoke-Tests der Box (Autonomie 0d): spielt die drei Kern-Pfade AKTIV durch —
|
||
# auch OHNE Update — und meldet NUR bei Rot (kein tägliches Grün-Rauschen). Ergänzt den passiven
|
||
# Health-Wächter (sentry.py prüft nur Erreichbarkeit) um echte Funktion:
|
||
# 1) Gateway/Hirn: der Agent liefert überhaupt eine Antwort.
|
||
# 2) Tools: ein harmloser terminal-Befehl durch den echten Agenten (fängt kaputte Approval-/
|
||
# Tool-Ketten — Antwort muss kommen und darf nicht in pending_approval hängen).
|
||
# 3) Voice: Lucys Sprech-Pfad (/api/voice/chat) streamt.
|
||
# Bei Fehlschlag → notify.sh (Telegram + Briefkasten) in Lucys Stimme. Läuft als User, kein sudo.
|
||
set -uo pipefail
|
||
|
||
SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
||
HERMES_URL="${HERMES_API_URL:-http://127.0.0.1:8642}"
|
||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||
|
||
say(){ echo "[self-smoke] $*"; }
|
||
FAILED=()
|
||
HEALED=()
|
||
|
||
API_KEY="$(grep -E '^API_SERVER_KEY=' "$HERMES/.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'")"
|
||
|
||
# ── 1) Gateway/Hirn: Agent antwortet ────────────────────────────────────────
|
||
if [ -n "$API_KEY" ]; then
|
||
RESP=$(curl -sf -m 90 -X POST "$HERMES_URL/v1/chat/completions" \
|
||
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
|
||
-H "X-Hermes-Session-Id: selfsmoke-gateway" \
|
||
-d '{"model":"hermes","stream":false,"chat_template_kwargs":{"enable_thinking":false},"messages":[{"role":"user","content":"Antworte NUR mit dem einen Wort: bereit"}]}' 2>/dev/null)
|
||
if echo "$RESP" | grep -qi "bereit"; then
|
||
say "PASS · Gateway/Hirn antwortet"
|
||
else
|
||
say "FAIL · Gateway/Hirn ohne verwertbare Antwort: $(echo "$RESP" | head -c 160)"
|
||
FAILED+=("Gehirn/Gateway (keine Antwort)")
|
||
fi
|
||
else
|
||
say "SKIP · Gateway (kein API_SERVER_KEY in $HERMES/.env)"
|
||
fi
|
||
|
||
# ── 2) Tools: terminal via Agent ─────────────────────────────────────────────
|
||
if [ -n "$API_KEY" ]; then
|
||
TOOL=$(curl -sf -m 90 -X POST "$HERMES_URL/v1/chat/completions" \
|
||
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
|
||
-H "X-Hermes-Session-Id: selfsmoke-tool" \
|
||
-d '{"model":"hermes","stream":false,"messages":[{"role":"user","content":"Führe im Terminal exakt den Befehl echo selfsmoke-ok aus und gib mir nur dessen Ausgabe zurück."}]}' 2>/dev/null)
|
||
if echo "$TOOL" | grep -qi "selfsmoke-ok"; then
|
||
say "PASS · Tools (terminal via Agent)"
|
||
elif echo "$TOOL" | grep -qi "pending_approval"; then
|
||
say "FAIL · Tools hängen in pending_approval (approvals.mode prüfen!)"
|
||
FAILED+=("Tools (hängen in Freigabe)")
|
||
else
|
||
say "FAIL · Tool-Smoke ohne Ergebnis: $(echo "$TOOL" | head -c 160)"
|
||
FAILED+=("Tools (kein Ergebnis)")
|
||
fi
|
||
fi
|
||
|
||
# ── 2b) Desktop-Gateway: hermes-builtin-ui antwortet ────────────────────────
|
||
# Seit dem Hermes-Desktop-Umzug (09.07.2026) hängt die Desktop-App an :9119
|
||
# (via MC2-Proxy /hermes-ui/). Stirbt der Dienst, ist die Arbeits-Oberfläche
|
||
# am PC tot — das muss der Selbst-Test melden, nicht erst der User.
|
||
if curl -sf -m 10 "http://127.0.0.1:9119/api/status" | grep -q '"gateway_running"'; then
|
||
say "PASS · Desktop-Gateway (:9119 /api/status)"
|
||
else
|
||
say "FAIL · Desktop-Gateway: :9119/api/status antwortet nicht (hermes-builtin-ui prüfen)"
|
||
FAILED+=("Desktop-Gateway (hermes-builtin-ui :9119)")
|
||
fi
|
||
|
||
# ── 3) Voice: Lucys Sprech-Stream ────────────────────────────────────────────
|
||
# Retry: der erste Turn nach Idle zahlt evtl. den Session-Kaltstart (Prefill).
|
||
# Subshell OHNE pipefail — head -c schließt die Pipe früh (curl endet mit 23/SIGPIPE),
|
||
# das würde einen bestandenen Check sonst als FAIL werten (Lehre aus hermes-postcheck.sh).
|
||
voice_ok=0
|
||
for try in 1 2 3; do
|
||
if ( set +o pipefail; curl -sf -m 45 -N -X POST "$MC_URL/api/voice/chat" -H "Content-Type: application/json" \
|
||
-d '{"text":"Sag nur ok.","session_id":"selfsmoke-voice"}' 2>/dev/null | head -c 50 | grep -q "data:" ); then
|
||
voice_ok=1; break
|
||
fi
|
||
sleep 8
|
||
done
|
||
if [ "$voice_ok" -eq 1 ]; then
|
||
say "PASS · Voice (/api/voice/chat streamt, Versuch $try)"
|
||
else
|
||
say "FAIL · Voice: /api/voice/chat streamt nicht (3 Versuche)"
|
||
FAILED+=("Voice (Lucys Sprech-Pfad)")
|
||
fi
|
||
|
||
# ── 4) Repo-Wächter: verlorene Merges (Vorfall 12.–15.07.2026) ──────────────
|
||
# Der Annahme-Runner merged+pusht nach origin/main; klemmt der Push (Gitea-Auth
|
||
# flattert) oder überschreibt eine fremde Session origin/main, trägt Box-main
|
||
# Commits, die origin fehlen — der nächste deploy-Reset VERNICHTET sie still
|
||
# (so verlor die Box die angenommene Karte 'wartung/lucy-annahme-fixes' vom
|
||
# 12.07., bemerkt erst beim Review am 15.07.). Hier täglich gegenprüfen.
|
||
LIVE_CO="$HOME/mission-control-v2"
|
||
if git -C "$LIVE_CO" fetch -q origin 2>/dev/null; then
|
||
LOST=$(git -C "$LIVE_CO" log --oneline origin/main..main 2>/dev/null | head -3 | tr '\n' ' ')
|
||
if [ -n "$LOST" ]; then
|
||
say "FAIL · Box-main trägt ungepushte Commits (Deploy würde sie vernichten): $LOST"
|
||
FAILED+=("Repo (ungepushte Commits auf Box-main: $LOST— vor dem nächsten Deploy nach origin retten)")
|
||
else
|
||
say "PASS · Repo-Wächter (Box-main == origin/main)"
|
||
fi
|
||
else
|
||
say "SKIP · Repo-Wächter (git fetch fehlgeschlagen)"
|
||
fi
|
||
|
||
# ── 5) Bild-Weiche v2: Bild an coder → Vision beschreibt, Coder antwortet ──────
|
||
# Dieser Pfad brach am 15.07. ZWEIMAL still (toter Hermes-Patch; gepoolter Socket
|
||
# nach Modell-Swap) — deshalb täglich aktiv durchspielen. Nebeneffekt: wärmt
|
||
# coder+vision am Morgen vor. 256x256-Testbild (Winzbilder = VL-Halluzinationen).
|
||
BILD_JSON=$(python3 - <<'PYIN'
|
||
import struct, zlib, base64, json
|
||
w=h=256
|
||
raw=b"".join(b"\x00"+b"\xff\x00\x00"*w for _ in range(h))
|
||
def chunk(t,d):
|
||
c=t+d; return struct.pack(">I",len(d))+c+struct.pack(">I",zlib.crc32(c))
|
||
png=b"\x89PNG\r\n\x1a\n"+chunk(b"IHDR",struct.pack(">IIBBBBB",w,h,8,2,0,0,0))+chunk(b"IDAT",zlib.compress(raw))+chunk(b"IEND",b"")
|
||
url="data:image/png;base64,"+base64.b64encode(png).decode()
|
||
print(json.dumps({"model":"coder","stream":False,"max_tokens":40,"messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":url}},{"type":"text","text":"Welche Farbe hat das angehaengte Bild? Antworte nur mit der Farbe."}]}]}))
|
||
PYIN
|
||
)
|
||
BW_HDR=$(mktemp); BW_OUT=$(mktemp)
|
||
if curl -sf -m 420 -D "$BW_HDR" -X POST "$MC_URL/v1/chat/completions" \
|
||
-H "Content-Type: application/json" -d "$BILD_JSON" -o "$BW_OUT" 2>/dev/null \
|
||
&& grep -qi "bleibt beim Coder" "$BW_HDR" \
|
||
&& grep -qi "rot" "$BW_OUT"; then
|
||
say "PASS · Bild-Weiche v2 (Bild an coder → beschrieben → 'rot')"
|
||
else
|
||
say "FAIL · Bild-Weiche v2: $(grep -i 'x-mc-route-reason' "$BW_HDR" 2>/dev/null | head -1) Antwort: $(head -c 80 "$BW_OUT" 2>/dev/null)"
|
||
FAILED+=("Bild-Weiche v2 (Bild an coder — Umleitung statt Beschreibung oder falsche Antwort)")
|
||
fi
|
||
rm -f "$BW_HDR" "$BW_OUT"
|
||
|
||
# ── 6) Browser-Tool: agent-browser läuft (Vorfall 16.07.2026) ────────────────
|
||
# Hermes' browser_navigate ruft das agent-browser-CLI über die Kette
|
||
# ~/.hermes/node/bin → ~/.local/bin/agent-browser. Das Hermes-Update 15.07.
|
||
# 23:53 bog den Symlink per npm-postinstall auf node_modules/ um und löschte
|
||
# das Paket im selben Lauf wieder (npm ci --workspace) → toter Link, jedes
|
||
# „Website öffnen" scheiterte still als „Failed to open". Hier täglich prüfen
|
||
# und den bekannten Bruch SELBST heilen: Link aufs npm-global-Binary
|
||
# zurückbiegen + Gateway/Desktop-UI neu starten (browser_tool cached ein
|
||
# „nicht installiert" pro Prozess — ohne Restart bliebe es kaputt).
|
||
AB_LINK="$HOME/.local/bin/agent-browser"
|
||
AB_GOOD="$HOME/.local/lib/node_modules/agent-browser/bin/agent-browser-linux-x64"
|
||
if "$AB_LINK" --version >/dev/null 2>&1; then
|
||
say "PASS · Browser-Tool ($("$AB_LINK" --version 2>/dev/null))"
|
||
elif [ -x "$AB_GOOD" ] && "$AB_GOOD" --version >/dev/null 2>&1; then
|
||
ln -sfn "$AB_GOOD" "$AB_LINK"
|
||
if "$AB_LINK" --version >/dev/null 2>&1; then
|
||
systemctl --user try-restart hermes-gateway hermes-builtin-ui >/dev/null 2>&1 || true
|
||
say "GEHEILT · Browser-Tool: toter agent-browser-Link neu gesetzt, Gateway/Desktop-UI neu gestartet"
|
||
HEALED+=("Browser-Tool (agent-browser-Link war tot — selbst repariert, Gateway/Desktop-UI neu gestartet)")
|
||
else
|
||
say "FAIL · Browser-Tool: Link neu gesetzt, aber agent-browser startet nicht"
|
||
FAILED+=("Browser-Tool (agent-browser startet trotz Link-Reparatur nicht)")
|
||
fi
|
||
else
|
||
say "FAIL · Browser-Tool: agent-browser fehlt (auch kein heiles Binary unter ~/.local/lib)"
|
||
FAILED+=("Browser-Tool (agent-browser fehlt — npm install -g agent-browser && agent-browser install --with-deps)")
|
||
fi
|
||
|
||
# ── 7) Disk-Wächter (17.07.2026): /srv/models sammelt Modelle, Backups + Vault wachsen ──
|
||
# Schwelle 85 % — meldet mit den größten Brocken, damit die Aufräum-Entscheidung leicht fällt.
|
||
DISK_SCHWELLE="${DISK_SCHWELLE:-85}"
|
||
while read -r FS NUTZUNG ZIEL; do
|
||
PCT="${NUTZUNG%\%}"
|
||
if [ "${PCT:-0}" -ge "$DISK_SCHWELLE" ]; then
|
||
BROCKEN="$(du -s --block-size=1G /srv/models/*/ 2>/dev/null | sort -rn | head -3 \
|
||
| awk '{printf "%s GB %s · ", $1, $2}' || true)"
|
||
say "FAIL · Disk: $ZIEL ist zu $NUTZUNG voll (Schwelle ${DISK_SCHWELLE} %)"
|
||
FAILED+=("Disk ($ZIEL zu $NUTZUNG voll — größte Brocken: ${BROCKEN:-unbekannt}Kandidat: rollenlose Fallback-Modelle prüfen)")
|
||
else
|
||
say "OK · Disk: $ZIEL bei $NUTZUNG"
|
||
fi
|
||
done < <(df --output=source,pcent,target / /srv/models 2>/dev/null | tail -n +2 | sort -u -k1,1)
|
||
|
||
# ── Selbstheilungen nur melden, nicht alarmieren ─────────────────────────────
|
||
if [ "${#HEALED[@]}" -gt 0 ]; then
|
||
HEAL_MSG="Commander, mein Selbst-Test hat etwas gefunden und direkt selbst repariert:
|
||
$(printf '• %s\n' "${HEALED[@]}")
|
||
Nur zur Info — es ist schon wieder grün."
|
||
bash "$SRC_DIR/notify.sh" -s "[Selbst-Test]" "$HEAL_MSG" || true
|
||
fi
|
||
|
||
# ── Meldung nur bei Rot ──────────────────────────────────────────────────────
|
||
if [ "${#FAILED[@]}" -eq 0 ]; then
|
||
say "Alle Selbst-Tests grün ($(date '+%F %H:%M'))."
|
||
exit 0
|
||
fi
|
||
|
||
MSG="Commander, mein täglicher Selbst-Test hat etwas gefunden — das läuft gerade NICHT rund:
|
||
$(printf '• %s\n' "${FAILED[@]}")
|
||
Der Rest der Box kann trotzdem stehen; magst du kurz draufschauen?"
|
||
bash "$SRC_DIR/notify.sh" -s "[Selbst-Test]" "$MSG" || true
|
||
say "FEHLGESCHLAGEN: ${FAILED[*]}"
|
||
exit 1
|