Autonomie 0d+0g: Lucy-Alarm-Weg, taegliche Selbst-Smokes, Selbstreparatur nach rotem Update

- /api/alarm: Lucy-unabhaengiger Telegram-Weg (fuer den PC-Lucy-Watchdog, wenn die App haengt).
- self-smoke.sh + mc2-selfsmoke.timer (taeglich 07:15): Gateway/Tools/Voice aktiv durchspielen,
  Meldung nur bei Rot. Ergaenzt den passiven Health-Waechter um echte Funktion.
- self-repair.sh + autoupdate.sh: bei rotem Hermes-Update zieht die Box Config-Brueche selbst
  (nur eindeutige, nicht-sicherheitsrelevante Keys; Backup -> YAML-Check -> Gehirn-Check, sonst
  zurueck). Greift es nicht, haengt eine LLM-Diagnose an die Rollback-Meldung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-03 19:23:54 +02:00
parent 357285e798
commit 612ce127fa
7 changed files with 248 additions and 3 deletions
+25
View File
@@ -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,
+17 -3
View File
@@ -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
+4
View File
@@ -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
+7
View File
@@ -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
+10
View File
@@ -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
+99
View File
@@ -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=<LLM-Diagnose>" 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
+86
View File
@@ -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