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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user