diff --git a/backend/routers/voice.py b/backend/routers/voice.py index a982585..a50ef4e 100644 --- a/backend/routers/voice.py +++ b/backend/routers/voice.py @@ -98,6 +98,31 @@ class AnnounceIn(BaseModel): priority: str = "normal" # 'silent' = nur im Verlauf zeigen, nicht sprechen +class AlarmIn(BaseModel): + text: str # die Alarm-Meldung + subject: str = "[Alarm]" # Betreff (Telegram-Präfix) + source: str = "alarm" # Absender fürs Log/Panel (z.B. "lucy-watchdog") + + +@router.post("/alarm") +def alarm(body: AlarmIn) -> dict: + """Lucy-UNABHÄNGIGER Alarm-Weg: schickt direkt auf Telegram (und legt die Meldung in den + Briefkasten). Für Absender, die NICHT auf die sprechende Lucy zählen können — allen voran + der PC-seitige Lucy-Watchdog, wenn die Desktop-App selbst hängt (dann nützt der Briefkasten + nichts, weil niemand ihn vorliest → Telegram ist der einzige verlässliche Kanal). LAN-only + wie alle MC2-Endpoints.""" + text = (body.text or "").strip() + if not text: + raise HTTPException(400, "Leere Meldung.") + subject = (body.subject or "[Alarm]").strip() + try: + item = announce.add(text, subject, body.source or "alarm", "normal") + except ValueError as exc: + raise HTTPException(400, str(exc)) + announce.notify_telegram(subject, text) # best-effort Telegram (posix/bash; Windows = No-op) + return {"ok": True, "item": item} + + @router.post("/voice/announce") def voice_announce(body: AnnounceIn) -> dict: """Meldung in den Briefkasten legen (Lucy-Proaktivität). Absender: Health-Wächter, diff --git a/deploy/autoupdate.sh b/deploy/autoupdate.sh index 1d8dbc3..94fb0d4 100644 --- a/deploy/autoupdate.sh +++ b/deploy/autoupdate.sh @@ -126,8 +126,20 @@ check_hermes(){ notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Mem0, Tools, Voice) grün — alles läuft." return fi - # Rot/Timeout ⇒ Rollback: Code auf alten Stand, Config aus dem frischen Backup, Neustart, Gehirn-Check. - say "$name: Job endete '$state' — ROLLBACK auf $old_head…" + # Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) — + # Autonomie 0g. Nur Config, reversibel, hart gegatet; scheitert es, fällt es sauber in den Rollback. + say "$name: Job endete '$state' — versuche Selbstreparatur (Config) vor dem Rollback…" + local sr_out sr_rc sugg="" + sr_out="$(bash "$SRC_DIR/self-repair.sh" 2>&1)"; sr_rc=$? + echo "$sr_out" + if [ "$sr_rc" -eq 0 ]; then + SUMMARY+=("$name: Config-Bruch nach Update SELBST repariert — neue Version läuft (kein Rollback).") + return + fi + sugg="$(echo "$sr_out" | sed -n 's/^SELFREPAIR_SUGGESTION=//p' | tail -1)" + + # Rollback: Code auf alten Stand, Config aus dem frischen Backup, Neustart, Gehirn-Check. + say "$name: Selbstreparatur griff nicht — ROLLBACK auf $old_head…" if [ -n "$old_head" ]; then git -C "$HERMES_DIR" reset --hard "$old_head" >/dev/null 2>&1 local bak stage @@ -143,7 +155,9 @@ check_hermes(){ if bash "$SRC_DIR/hermes-postcheck.sh" >/dev/null 2>&1; then set_pin hermes "$old_head" "Update ($behind Commits) riss den Gehirn-Check; Rollback grün" SUMMARY+=("$name: Update FEHLGESCHLAGEN → zurückgerollt + GEPINNT auf ${old_head:0:9}.") - notify "Hermes-Update fehlgeschlagen (Gehirn-Check rot). Automatisch zurückgerollt auf ${old_head:0:9} — läuft wieder. Hermes ist jetzt GEPINNT." + notify "Hermes-Update fehlgeschlagen (Gehirn-Check rot). Selbstreparatur der Config griff nicht, deshalb automatisch zurückgerollt auf ${old_head:0:9} — läuft wieder. Hermes ist jetzt GEPINNT.${sugg:+ + +Wahrscheinliche Ursache/Fix (Box-Diagnose): $sugg}" return fi fi diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 5dab865..f265c46 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -62,6 +62,9 @@ cp "$SRC/deploy/mc2-backup.timer" "$HOME/.config/systemd/user/mc2-backup.timer" # Wöchentliches Auto-Update (Router/Engine/Hermes, Rollback+Pin+Telegram) — Autonomie E2. cp "$SRC/deploy/mc2-autoupdate.service" "$HOME/.config/systemd/user/mc2-autoupdate.service" cp "$SRC/deploy/mc2-autoupdate.timer" "$HOME/.config/systemd/user/mc2-autoupdate.timer" +# Tägliche Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv, Alarm bei Rot) — Autonomie 0d. +cp "$SRC/deploy/mc2-selfsmoke.service" "$HOME/.config/systemd/user/mc2-selfsmoke.service" +cp "$SRC/deploy/mc2-selfsmoke.timer" "$HOME/.config/systemd/user/mc2-selfsmoke.timer" # Evolution-Radar-Feed (Autonomie E4): Hermes-cron speist ihn als Prompt ein. # Cron-Job selbst wird EINMALIG registriert (hermes cron create, siehe docs/RUNBOOK.md). mkdir -p "$HOME/.hermes/scripts" @@ -78,6 +81,7 @@ systemctl --user enable mem0-service >/dev/null 2>&1 || true systemctl --user enable voice-service >/dev/null 2>&1 || true systemctl --user enable --now mc2-backup.timer >/dev/null 2>&1 || true systemctl --user enable --now mc2-autoupdate.timer >/dev/null 2>&1 || true +systemctl --user enable --now mc2-selfsmoke.timer >/dev/null 2>&1 || true loginctl enable-linger "$USER" >/dev/null 2>&1 || true # Mem0-Sidecar VOR dem Backend (re)starten, damit /api/memory sofort bedient wird. [ -x "$HOME/.mem0/venv/bin/python" ] && systemctl --user restart mem0-service 2>/dev/null || true diff --git a/deploy/mc2-selfsmoke.service b/deploy/mc2-selfsmoke.service new file mode 100644 index 0000000..0796f6a --- /dev/null +++ b/deploy/mc2-selfsmoke.service @@ -0,0 +1,7 @@ +[Unit] +Description=MC2 Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv durchspielen, Alarm bei Rot) +Documentation=file:%h/mission-control-v2/docs/AUTONOMIE_PLAN.md + +[Service] +Type=oneshot +ExecStart=/bin/bash %h/mission-control-v2/deploy/self-smoke.sh diff --git a/deploy/mc2-selfsmoke.timer b/deploy/mc2-selfsmoke.timer new file mode 100644 index 0000000..3118250 --- /dev/null +++ b/deploy/mc2-selfsmoke.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Täglicher MC2-Selbst-Smoke-Test (07:15, unabhängig von Updates) + +[Timer] +OnCalendar=*-*-* 07:15:00 +Persistent=true +RandomizedDelaySec=300 + +[Install] +WantedBy=timers.target diff --git a/deploy/self-repair.sh b/deploy/self-repair.sh new file mode 100644 index 0000000..097e310 --- /dev/null +++ b/deploy/self-repair.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Selbstreparatur-Modus (Autonomie 0g, Kür): Macht ein Hermes-Update den Gehirn-Check rot, +# WEIL sich Config-Schlüssel geändert haben (v0.18-Klasse: „Unknown key … → stiller Default → +# alle Tools hingen"), versucht die Box den Config-Bruch SELBST zu ziehen — bevor zurückgerollt +# wird, um die neue Version zu behalten. NUR Config, NIE Code, NIE Security-Schlüssel. +# +# Sicher durch Kapselung — jeder Schritt ist reversibel und hart gegatet: +# Backup → nur EINDEUTIGE (genau 1× vorkommende), nicht-sicherheitsrelevante Keys auskommentieren +# → YAML muss gültig bleiben → Gehirn-Check (hermes-postcheck.sh) muss GRÜN werden → sonst zurück. +# Schlimmster Fall bei Fehlgriff = exakt der heutige: Rollback. Der Aufrufer (autoupdate.sh) ruft +# dies VOR seinem Rollback; bei Rückgabe 2 rollt er wie gehabt zurück + pinnt. +# +# Rückgabe: 0 = repariert (Gehirn-Check danach grün) · 2 = nichts sicher reparierbar. +# Gibt bei Misserfolg eine Zeile „SELFREPAIR_SUGGESTION=" auf stdout aus. +set -uo pipefail + +SRC_DIR="$(cd "$(dirname "$0")" && pwd)" +HERMES="${HERMES_HOME:-$HOME/.hermes}" +CFG="$HERMES/config.yaml" +WARN_FILE="${HERMES_CONFIG_WARNINGS:-/tmp/hermes-config-warnings.txt}" +LLAMA="${LLAMA_SWAP_URL:-http://127.0.0.1:8080}" +VENV_PY="$HERMES/hermes-agent/venv/bin/python" +export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + +say(){ echo "[self-repair] $*"; } + +# ── 1) Evidenz: Postcheck-Warnungen + frisches Gateway-Journal ──────────────── +EVID="$(cat "$WARN_FILE" 2>/dev/null)" +JOURN="$(journalctl --user -u hermes-gateway --since '-6 minutes' --no-pager -o cat 2>/dev/null \ + | grep -iE "Unknown [a-z._]+ '|deprecated .*(setting|option|config)|defaulting to|invalid config" \ + | grep -v check_fn | head -10)" +[ -z "$EVID" ] && EVID="$JOURN" +if [ -z "$EVID" ]; then + say "Keine Config-Bruch-Hinweise gefunden — hier ist (sicher) nichts zu reparieren." + exit 2 +fi +say "Config-Hinweise:"; echo "$EVID" + +# LLM-Diagnose/Empfehlung (best-effort) — für die Meldung, egal ob wir reparieren. +SUGG="$(curl -sf -m 60 "$LLAMA/v1/chat/completions" -H 'Content-Type: application/json' \ + -d "$(python3 - "$EVID" <<'PY' +import json, sys +p = ("Ein Hermes-Agent-Update hat den Selbsttest rot gemacht. Log-Hinweise:\n" + sys.argv[1] + + "\n\nNenne auf Deutsch in 1-2 kurzen Sätzen die wahrscheinliche Ursache und die konkrete " + "Config-Korrektur in ~/.hermes/config.yaml (Schlüssel → neuer Wert). Keine Einleitung.") +print(json.dumps({"model": "coder-lite", "max_tokens": 200, "temperature": 0.1, + "messages": [{"role": "user", "content": p}]})) +PY +)" 2>/dev/null | python3 -c 'import sys,json; print((json.load(sys.stdin).get("choices") or [{}])[0].get("message",{}).get("content","").strip())' 2>/dev/null)" + +# ── 2) Offending Leaf-Keys ziehen ──────────────────────────────────────────── +KEYS="$(echo "$EVID" | grep -oE "Unknown [a-zA-Z0-9_.]+|'[a-zA-Z0-9_.]+'" | sed -E "s/^Unknown //; s/'//g" | sort -u)" +LEAVES="$(for k in $KEYS; do echo "${k##*.}"; done | sort -u | grep -vE '^(true|false|null|)$')" + +SEC_RX='token|secret|password|approval|api_server_key|ufw|sudo|allowlist|credential' +BAK="$CFG.selfrepair-bak-$(date +%Y%m%d-%H%M%S)" +cp -a "$CFG" "$BAK" + +CHANGED=""; SKIPPED="" +for leaf in $LEAVES; do + if echo "$leaf" | grep -qiE "$SEC_RX"; then SKIPPED="$SKIPPED $leaf(security-tabu)"; continue; fi + # Nur EINDEUTIGE Schlüssel-Zeilen (genau 1×) anfassen — Mehrdeutige sind zu riskant. + n="$(grep -cE "^[[:space:]]*${leaf}:" "$CFG" 2>/dev/null || echo 0)" + if [ "$n" = "1" ]; then + sed -i -E "s|^([[:space:]]*)(${leaf}:.*)$|\1# [selbstrepariert $(date +%F)] \2|" "$CFG" + CHANGED="$CHANGED $leaf"; say "auskommentiert: $leaf (unbekannt/veraltet)" + else + SKIPPED="$SKIPPED $leaf(${n}x)" + fi +done +[ -n "$SKIPPED" ] && say "nicht automatisch angefasst:$SKIPPED" + +if [ -z "$CHANGED" ]; then + say "Nichts sicher automatisch reparierbar." + cp -a "$BAK" "$CFG" + echo "SELFREPAIR_SUGGESTION=$SUGG" + exit 2 +fi + +# ── 3) YAML noch gültig? ────────────────────────────────────────────────────── +if [ -x "$VENV_PY" ] && ! "$VENV_PY" -c "import yaml; yaml.safe_load(open('$CFG'))" 2>/dev/null; then + say "YAML nach Fix ungültig → zurück zum Backup." + cp -a "$BAK" "$CFG"; echo "SELFREPAIR_SUGGESTION=$SUGG"; exit 2 +fi + +# ── 4) Neustart + Gehirn-Check ──────────────────────────────────────────────── +systemctl --user restart hermes-gateway; sleep 6 +if bash "$SRC_DIR/hermes-postcheck.sh" >/dev/null 2>&1; then + say "Gehirn-Check nach Selbstreparatur GRÜN." + bash "$SRC_DIR/notify.sh" -s "[Box-Update]" \ + "Commander, nach dem Hermes-Update hakte der Selbsttest wegen geänderter Einstellungen — ich habe das selbst gerichtet (angepasst:${CHANGED// /, }). Der Test ist wieder grün, die neue Version läuft. Ein Rollback war nicht nötig." || true + exit 0 +fi + +say "Auch nach Selbstreparatur rot → zurück zum Backup (der Aufrufer rollt zurück)." +cp -a "$BAK" "$CFG" +systemctl --user restart hermes-gateway; sleep 6 +echo "SELFREPAIR_SUGGESTION=$SUGG" +exit 2 diff --git a/deploy/self-smoke.sh b/deploy/self-smoke.sh new file mode 100644 index 0000000..2891894 --- /dev/null +++ b/deploy/self-smoke.sh @@ -0,0 +1,86 @@ +#!/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 + +# ── 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 + +# ── 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