#!/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=() 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" # ── 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