diff --git a/.empire/ONBOARDING.md b/.empire/ONBOARDING.md index 58b3474c7d..fe205a462e 100644 --- a/.empire/ONBOARDING.md +++ b/.empire/ONBOARDING.md @@ -281,10 +281,56 @@ Relay** — wer beide bedienen will, startet zwei Harness-Prozesse mit zwei Iden | `buzz.exe` in Git Bash nicht gefunden | Sidecar liegt nicht im PATH | mit vollem Pfad aufrufen | | Argumente kommen verstümmelt an | PowerShell 5.1 zerlegt CLI-Argumente | Buzz-CLI-Kommandos in Git Bash ausführen | | Antwort erscheint doppelt | Harness liefert bei mehreren Tool-Runden mehrere Posts | kosmetisch; `BUZZ_AGENT_MAX_ROUNDS` senken | +| `systemctl is-active` grün, Agent trotzdem stumm | Prozessstatus sagt nichts über Relay-Draht, LLM-Kontingent oder Provider-Fehler | Liveness-Probe: `bash /opt/buzz-agents/liveness-probe.sh --no-push` | +| Log wächst nicht, obwohl der Agent arbeitet | `RUST_LOG=buzz_acp=info` protokolliert nur Lebenszyklus — Nachrichtenverarbeitung erzeugt **keine** Zeile (gemessen: Antwort in 3 s, 0 Log-Zeilen) | Das Log ist **kein** Liveness-Signal. Immer die Probe fragen. | +| Log nach der Rotation ist „binary file matches" | `copytruncate` kürzt die Datei auf 0, der offene Schreib-FD behält seinen Offset → NUL-Loch am Anfang | normal, kein Datenverlust; `grep -a` bzw. `less` benutzen | +| Agent stumm, im Kanal steht `429`/`404`/`422` | freies Modell am Rate-Limit, verschwunden oder schema-inkompatibel | `bash /opt/buzz-agents/sentry-set-model.sh ` — prüft das neue Modell mit einer echten Antwort und rollt bei Stille selbst zurück | +| Nach einem Reboot fehlt ein Heartbeat, der Agent lebt aber | Timer-Lauf fiel in die Startphase | `OnBootSec=5min` + `Persistent=true` im Timer holen den Lauf nach | --- -## 8. Gerät wieder abnehmen +## 8. Dauerbetrieb eines Server-Agenten (buzz#77) + +Ein Gerät, das Laptops überleben soll, braucht mehr als eine systemd-Unit. Referenz ist +`Sentry` auf adas-hetzner; die Werkzeuge liegen in `.empire/tools/` und werden nach +`/opt/buzz-agents/` kopiert. + +**Der Kern in einem Satz: Prozessstatus ist kein Lebenszeichen.** Gemessen am 2026-08-01 +beantwortete Sentry eine echte Mention in 3 Sekunden, ohne dabei eine einzige Log-Zeile zu +schreiben — und umgekehrt kann derselbe Prozess munter laufen, während der Relay-Draht ab, +das Kontingent leer oder der Provider auf 429 steht. Weder `systemctl is-active` noch das +Log können den Unterschied zeigen. Nur eine echte Antwort kann es. + +| Baustein | Was | Wo | +|---|---|---| +| Log-Rotation | `daily`, `rotate 7`, `compress`, **`copytruncate`** (Pflicht: `append:`-Ziele vertragen kein Umbenennen) | `/etc/logrotate.d/buzz-agents` | +| Liveness-Probe | postet alle 30 min eine Mention mit Einmal-Token in `diag-liveness` und akzeptiert nur eine Antwort **von genau diesem Pubkey** mit diesem Token | `.empire/tools/sentry-liveness-probe.sh` | +| Zeitgeber | systemd-Timer, `OnBootSec=5min`, `Persistent=true` | `.empire/tools/buzz-sentry-liveness.timer.example` | +| Alarm | Kuma-Push-Monitor `sentry-liveness`, Toleranz 2 h, `maxretries=0` | `.empire/tools/kuma-add-sentry-liveness.mjs` | +| Modellwechsel | setzt Modell, startet neu, **beweist** mit einer echten Antwort, rollt bei Stille selbst zurück | `.empire/tools/sentry-set-model.sh` | + +Drei Entscheidungen, die nicht willkürlich sind: + +- **Eigene Identität für die Probe.** Sie signiert mit einem eigenen Schlüssel auf Sentrys + Allowlist (`/opt/buzz-agents/liveness.conf`, `chmod 600`). Munirs Owner-Key bleibt auf + seinem Gerät — Schlüssel verlassen ihr Gerät nicht (§4.2). +- **Toleranz 2 h, nicht 30 min.** Jede Probe kostet einen echten LLM-Aufruf, und freie + Modelle antworten gelegentlich mit 429. Bei knapper Toleranz weckt der Wächter Munir bei + jedem Provider-Schluckauf — und ein Wächter, dem man nicht glaubt, ist schlechter als + keiner. 2 h verzeiht drei Fehlschläge in Folge und meldet echte Stille am selben Tag. +- **Kein eigener Alarm für `429`/`404`.** Ein Modellausfall macht den Agenten stumm, und + Stille misst die Probe bereits. Ein zweiter Detektor für dieselbe Wirkung wäre doppelte + Pflege und ein zweiter Fehlalarm-Weg. + +**Die Toleranz gehört ins `interval`, nicht in `maxretries`.** Gemessen (adas-empire#79): +ein `status=down` auf einen Monitor mit `maxretries=1` erzeugt `status=2` (PENDING) mit +`important=0` — es geht **keine** Benachrichtigung raus. Erst der nächste fällige Beat nach +`retryInterval` macht daraus DOWN. Bei `retryInterval == interval` verdoppelt das die Zeit +bis zum Alarm, lautlos. + +--- + +## 9. Gerät wieder abnehmen 1. Harness stoppen (`systemctl disable --now buzz-` bzw. Prozess beenden). 2. `buzz-admin remove-member --pubkey ` — die Identität verliert den Relay-Zugang diff --git a/.empire/tools/buzz-sentry-liveness.timer.example b/.empire/tools/buzz-sentry-liveness.timer.example new file mode 100644 index 0000000000..79454e231a --- /dev/null +++ b/.empire/tools/buzz-sentry-liveness.timer.example @@ -0,0 +1,42 @@ +# Liveness-Probe fuer Sentry (buzz#77) — Vorlage fuer /etc/systemd/system/. +# +# Zwei Dateien, beide nach /etc/systemd/system/ kopieren, dann: +# systemctl daemon-reload +# systemctl enable --now buzz-sentry-liveness.timer +# systemctl list-timers buzz-sentry-liveness.timer +# +# Warum systemd-Timer und nicht Cron: derselbe Ort wie die Unit, die er ueberwacht, +# gleiche Log-Senke, und `Persistent=true` holt einen wegen Neustart verpassten Lauf +# nach — ohne das waere jeder Reboot ein Loch im Heartbeat und damit ein Fehlalarm. +# +# ---------- /etc/systemd/system/buzz-sentry-liveness.service ---------- +# [Unit] +# Description=Liveness-Probe: antwortet der Buzz-Agent "Sentry" noch? +# Documentation=https://github.com/munirad7s/buzz/issues/77 +# After=network-online.target buzz-sentry.service +# Wants=network-online.target +# +# [Service] +# Type=oneshot +# ExecStart=/bin/bash /opt/buzz-agents/liveness-probe.sh +# StandardOutput=append:/opt/buzz-agents/logs/liveness.log +# StandardError=append:/opt/buzz-agents/logs/liveness.log +# TimeoutStartSec=300 +# +# ---------- /etc/systemd/system/buzz-sentry-liveness.timer ---------- +# [Unit] +# Description=Sentry-Liveness alle 30 Minuten +# +# [Timer] +# OnBootSec=5min +# OnUnitActiveSec=30min +# AccuracySec=1min +# Persistent=true +# +# [Install] +# WantedBy=timers.target +# +# OnBootSec=5min statt sofort: nach einem Reboot braucht der Relay-Stack einen Moment, +# und eine Probe, die in die Startphase hineinlaeuft, meldet einen Ausfall, den es +# nicht gibt. Das Log liegt unter /opt/buzz-agents/logs/*.log und faellt damit unter +# die bestehende logrotate-Regel /etc/logrotate.d/buzz-agents. diff --git a/.empire/tools/kuma-add-sentry-liveness.mjs b/.empire/tools/kuma-add-sentry-liveness.mjs new file mode 100644 index 0000000000..851a4312f4 --- /dev/null +++ b/.empire/tools/kuma-add-sentry-liveness.mjs @@ -0,0 +1,150 @@ +// Legt EINEN Uptime-Kuma-Push-Monitor "sentry-liveness" an (buzz#77) — additiv, +// idempotent über den Monitornamen. Bestehende Monitore und die Statuspage +// werden nicht angefasst. +// +// Dieser Monitor ist der Wachhund über dem Dauer-Agenten "Sentry" auf adas-hetzner. +// Solange `.empire/tools/sentry-liveness-probe.sh` beweist, dass Sentry auf eine echte +// Mention antwortet, kommt ein Heartbeat. Bleibt er aus, ist der Agent stumm — und das +// ist der Zustand, den weder `systemctl is-active` noch das Log zeigen können. +// +// Warum das Script und nicht ein Klick in der UI: der Monitor ist Teil des +// Wächters, nicht Dekoration. Wer ihn versehentlich löscht, muss ihn exakt so +// wiederherstellen können — inklusive Intervall und Benachrichtigungen. +// +// Kuma hat keine REST-API zum Anlegen von Monitoren, nur Socket.IO. Muster +// übernommen von /opt/agency/monitoring/provision/kuma-add-funnel-e2e.mjs. +// +// Env: KUMA_URL (default http://kuma:3001), KUMA_ADMIN_USER, KUMA_ADMIN_PASSWORD, +// KUMA_PUSH_SENTRY_LIVENESS_TOKEN (Push-Token, `openssl rand -hex 8`). +// +// Aufruf auf adas-hetzner (Wegwerf-Container, nutzt die node_modules des +// monitoring-Provision-Ordners — socket.io-client liegt dort schon): +// cd /opt/agency/monitoring && set -a && . ./.env && set +a +// docker run --rm --memory 128m --network monitoring_monitoring-internal \ +// -v /opt/agency/monitoring/provision:/app:ro \ +// -v /tmp/kuma-add-sentry-liveness.mjs:/app/kuma-add-sentry-liveness.mjs:ro \ +// -w /app -e KUMA_ADMIN_USER -e KUMA_ADMIN_PASSWORD -e KUMA_PUSH_SENTRY_LIVENESS_TOKEN \ +// node:22-alpine node kuma-add-sentry-liveness.mjs +import { io } from "socket.io-client"; + +const URL = process.env.KUMA_URL ?? "http://kuma:3001"; +const USER = process.env.KUMA_ADMIN_USER; +const PASS = process.env.KUMA_ADMIN_PASSWORD; +const TOKEN = process.env.KUMA_PUSH_SENTRY_LIVENESS_TOKEN; +if (!USER || !PASS) { + console.error("[kuma] KUMA_ADMIN_USER/KUMA_ADMIN_PASSWORD fehlen"); + process.exit(1); +} +if (!TOKEN) { + console.error("[kuma] KUMA_PUSH_SENTRY_LIVENESS_TOKEN fehlt"); + process.exit(1); +} + +// interval = maximale Sekunden zwischen zwei Pushes, bis Kuma DOWN meldet. +// Die Probe läuft alle 30 Minuten -> 7200 s (2 h) Toleranz. Bewusst NICHT knapp: +// jede Probe kostet einen echten LLM-Aufruf, und das freie Modell antwortet +// gelegentlich mit 429. Bei knapper Toleranz würde der Wächter Munir bei jedem +// Provider-Schluckauf wecken — und ein Wächter, dem man nicht glaubt, ist +// schlechter als keiner. 2 h heißt: drei aufeinanderfolgende Fehlschläge werden +// verziehen, echte Stille meldet sich noch am selben Tag. +// +// maxretries = 0 — Abweichung von den neun älteren Push-Monitoren, die alle +// maxretries=1 tragen. Gemessen am 2026-08-01 (adas-empire#79): ein +// `status=down`-Push auf einen Monitor mit maxretries=1 erzeugt einen Heartbeat +// mit status=2 (PENDING) und important=0 — es geht KEINE Benachrichtigung raus. +// Erst der nächste fällige Beat nach retryInterval macht daraus DOWN. Bei +// retryInterval == interval verdoppelt das die Zeit bis zum Alarm still. +// Die Toleranz gehört ins interval, nicht in einen unsichtbaren Retry. +// +// notificationIDList: 2 = telegram-adas-agency, 3 = mail-resend-munir — dieselbe +// Kombination wie alle bestehenden Push-Monitore. +const MONITOR = { + name: "sentry-liveness", + type: "push", + interval: 7200, + retryInterval: 7200, + resendInterval: 0, + maxretries: 0, + timeout: 20, + pushToken: TOKEN, + notificationIDList: { 2: true, 3: true }, + ignoreTls: false, + upsideDown: false, + expiryNotification: false, + maxredirects: 10, + accepted_statuscodes: ["200-299"], + dns_resolve_type: "A", + dns_resolve_server: "1.1.1.1", + proxyId: null, + mqttUsername: "", + mqttPassword: "", + mqttTopic: "", + mqttSuccessMessage: "", + method: "GET", + headers: null, + body: null, + httpBodyEncoding: "json", + conditions: [], + parent: null, +}; + +const fail = (msg, detail) => { + console.error(`[kuma] FEHLER ${msg}:`, JSON.stringify(detail)); + process.exit(1); +}; + +const socket = io(URL, {}); +const call = (ev, ...args) => new Promise((res) => socket.emit(ev, ...args, (r) => res(r))); +setTimeout(() => fail("timeout", "60s ohne Abschluss"), 60000).unref?.(); + +let connectErrors = 0; +socket.on("connect_error", (e) => { + if (++connectErrors >= 5) fail("connect_error", e.message); +}); +socket.on("connect", async () => { + const login = await call("login", { username: USER, password: PASS, token: "" }); + if (!login.ok) fail("login", login); + + const listPromise = new Promise((res) => socket.once("monitorList", (l) => res(l))); + await call("getMonitorList"); + const existing = Object.values((await listPromise) ?? {}); + const found = existing.find((m) => m.name === MONITOR.name); + if (found) { + const cur = await call("getMonitor", found.id); + if (!cur.ok) fail("getMonitor", cur); + // Nicht nur der Push-Token wird nachgezogen, sondern auch das Alarmverhalten. + // Ein Monitor, dessen interval/maxretries jemand in der UI verstellt hat, ist + // still stumm — genau der Fehlermodus, gegen den dieses Ticket gebaut wurde. + const want = { + pushToken: TOKEN, + interval: MONITOR.interval, + retryInterval: MONITOR.retryInterval, + maxretries: MONITOR.maxretries, + notificationIDList: MONITOR.notificationIDList, + }; + const drift = Object.entries(want).filter(([k, v]) => + k === "notificationIDList" + ? JSON.stringify(Object.keys(v).sort()) !== + JSON.stringify(Object.keys(cur.monitor[k] ?? {}).filter((n) => cur.monitor[k][n]).sort()) + : cur.monitor[k] !== v, + ); + if (drift.length) { + const upd = await call("editMonitor", { ...cur.monitor, ...want }); + if (!upd.ok) fail("editMonitor", upd); + console.log( + `[kuma] push "sentry-liveness" existiert (id=${found.id}) — nachgezogen: ${drift + .map(([k]) => k) + .join(", ")}`, + ); + } else { + console.log(`[kuma] push "sentry-liveness" existiert (id=${found.id}) — Konfiguration ok`); + } + console.log(`MONITOR_ID=${found.id}`); + process.exit(0); + } + const r = await call("add", MONITOR); + if (!r.ok) fail("add", r); + console.log(`[kuma] push "sentry-liveness" angelegt (id=${r.monitorID})`); + console.log(`MONITOR_ID=${r.monitorID}`); + process.exit(0); +}); diff --git a/.empire/tools/sentry-liveness-probe.sh b/.empire/tools/sentry-liveness-probe.sh new file mode 100644 index 0000000000..23e6c2ed92 --- /dev/null +++ b/.empire/tools/sentry-liveness-probe.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Liveness-Probe fuer den Dauer-Agenten "Sentry" auf adas-hetzner (buzz#77). +# +# WARUM PROZESS-STATUS NICHT REICHT +# --------------------------------- +# `systemctl is-active buzz-sentry` sagt nur, dass ein Prozess laeuft. Gemessen am +# 2026-08-01: der Agent beantwortete eine echte Mention in 3 Sekunden und schrieb +# dabei KEINE EINZIGE ZEILE ins Log (RUST_LOG=buzz_acp=info protokolliert nur +# Lebenszyklus-Ereignisse). Umgekehrt gilt dasselbe: der Prozess kann munter +# weiterlaufen, waehrend der Relay-Draht abgerissen, das LLM-Kontingent erschoepft +# oder der Provider auf 429/422 steht. Weder Prozess noch Log koennen den +# Unterschied zeigen — nur eine echte Antwort kann es. +# +# WAS SIE MISST +# ------------- +# Den ganzen Pfad: Relay-Verbindung -> Kanal-Abo -> Allowlist -> Harness -> LLM -> +# Antwort zurueck in den Kanal. Sie postet eine Mention mit einem Einmal-Token und +# akzeptiert nur eine Antwort, die (a) von GENAU Sentrys Pubkey kommt und (b) das +# Token enthaelt. Ein "irgendwer hat irgendwas geschrieben" zaehlt nicht. +# +# EIGENE IDENTITAET, NICHT MUNIRS +# ------------------------------- +# Die Probe signiert mit einem eigenen Schluessel (`/opt/buzz-agents/liveness.conf`, +# chmod 600), der auf Sentrys Allowlist steht. Munirs Owner-Key bleibt auf seinem +# Geraet — Schluessel verlassen ihr Geraet nicht (.empire/ONBOARDING.md §4.2). +# +# EIGENER KANAL +# ------------- +# Sie laeuft in `diag-liveness`, nicht in Arbeitskanaelen. Sonst waere der Kanal, in +# dem Menschen mitlesen, zu 90 % Probenverkehr. +# +# bash sentry-liveness-probe.sh # Probe + Kuma-Heartbeat +# bash sentry-liveness-probe.sh --no-push # nur Probe +# +# Exit 0 = Sentry hat geantwortet. Exit 1 = jeder Fehlschlag, mit REASON= in der +# Ausgabe. Fehlschlag pusht NICHT: das Ausbleiben ist der Alarm (Kuma-Push-Monitor +# "sentry-liveness"). Ein einzelner Fehlschlag alarmiert dabei bewusst NICHT sofort +# — siehe Toleranz-Begruendung im Kopf von kuma-add-sentry-liveness.mjs. +set -uo pipefail + +CONF="${SENTRY_LIVENESS_CONF:-/opt/buzz-agents/liveness.conf}" +BUZZ_BIN="${BUZZ_BIN:-/opt/buzz-agents/bin/buzz}" +STATUS_FILE="${SENTRY_LIVENESS_STATUS:-/opt/buzz-agents/logs/liveness-status.json}" +SENTRY_PUBKEY="${SENTRY_PUBKEY:-67160ec6fba47d29d6629a3a8aca00a5a0d90fe2c240ac9ff75460664bf3629f}" +DIAG_CHANNEL="${SENTRY_DIAG_CHANNEL:-7e06a1bc-8b3a-4dc5-8658-05a53073aabb}" +TIMEOUT_SEC="${SENTRY_LIVENESS_TIMEOUT:-180}" +KUMA_PUSH_BASE="${KUMA_PUSH_BASE:-https://status.adas.jetzt/api/push}" +PUSH=1 +[ "${1:-}" = "--no-push" ] && PUSH=0 + +started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + +write_status() { + # Statusdatei ist Diagnose fuer Menschen, nicht der Alarmweg. Der Alarm ist der + # ausbleibende Kuma-Push — eine Datei, die niemand liest, alarmiert niemanden. + printf '{"ts":"%s","result":"%s","reason":"%s","detail":"%s","latency_s":%s}\n' \ + "$started_at" "$1" "${2:-}" "$(printf '%s' "${3:-}" | tr -d '"' | tr '\n' ' ')" "${4:-null}" \ + > "$STATUS_FILE" 2>/dev/null || true +} + +fail() { + write_status "FAIL" "$1" "${2:-}" + echo "RESULT=FAIL" + echo "REASON=$1" + [ -n "${2:-}" ] && echo "DETAIL=$2" + [ -n "${3:-}" ] && echo "FIX=$3" + echo "KUMA=kein Push — das Ausbleiben des Heartbeats ist der Alarm" + exit 1 +} + +[ -r "$CONF" ] || fail "conf-missing" "$CONF nicht lesbar" "Identitaet der Probe anlegen (siehe buzz#77)" +# shellcheck disable=SC1090 +set -a; . "$CONF"; set +a +[ -n "${BUZZ_PRIVATE_KEY:-}" ] || fail "no-private-key" "BUZZ_PRIVATE_KEY fehlt in $CONF" +[ -x "$BUZZ_BIN" ] || fail "buzz-cli-missing" "$BUZZ_BIN nicht ausfuehrbar" +export BUZZ_PRIVATE_KEY +export BUZZ_RELAY_URL="${BUZZ_LIVENESS_RELAY_URL:-https://buzz.adas.casa}" + +TOKEN="lp$(date -u +%Y%m%d%H%M%S)$(head -c 3 /dev/urandom | od -An -tx1 | tr -d ' \n')" +echo "TOKEN=$TOKEN" + +t0=$(date +%s) +send_out="$("$BUZZ_BIN" --format json messages send --channel "$DIAG_CHANNEL" \ + --mention "$SENTRY_PUBKEY" \ + --content "Liveness-Probe $TOKEN — antworte nur mit: OK $TOKEN" 2>&1)" +send_id="$(printf '%s' "$send_out" | jq -r 'select(.accepted == true) | .event_id // empty' 2>/dev/null)" +[ -z "$send_id" ] && fail "send-failed" "Relay hat die Probe nicht angenommen: $(printf '%s' "$send_out" | head -c 300)" \ + "Relay erreichbar? Mitgliedschaft der Probe-Identitaet noch gueltig?" +echo "SENT=$send_id" + +# Antwort einsammeln. Akzeptiert wird ausschliesslich: Absender == Sentry UND Token +# im Text. Ohne die Absenderpruefung wuerde die Probe das eigene Echo als Lebenszeichen +# verkaufen — der Detektor koennte dann nie rot werden. +deadline=$(( t0 + TIMEOUT_SEC )) +reply="" +while [ "$(date +%s)" -lt "$deadline" ]; do + sleep 10 + msgs="$("$BUZZ_BIN" --format json messages get --channel "$DIAG_CHANNEL" --limit 20 2>/dev/null)" + reply="$(printf '%s' "$msgs" | jq -r --arg pk "$SENTRY_PUBKEY" --arg tok "$TOKEN" \ + '.[]? | select(.pubkey == $pk) | select(.content | contains($tok)) | .id' 2>/dev/null | head -1)" + [ -n "$reply" ] && break +done +latency=$(( $(date +%s) - t0 )) + +[ -z "$reply" ] && fail "no-reply" \ + "Sentry hat in ${TIMEOUT_SEC}s nicht geantwortet — Prozess kann laufen und trotzdem stumm sein" \ + "journalctl -u buzz-sentry -n 50 und /opt/buzz-agents/logs/sentry.log pruefen; haeufigste Ursache: LLM-Kontingent (429) oder Relay-Abriss" +echo "REPLY=$reply" +echo "LATENCY=${latency}s" + +if [ "$PUSH" -eq 0 ]; then + write_status "OK" "" "no-push" "$latency" + echo "KUMA=uebersprungen (--no-push)" + echo "RESULT=OK" + exit 0 +fi + +[ -z "${KUMA_PUSH_SENTRY_LIVENESS_TOKEN:-}" ] && fail "kuma-token-missing" \ + "KUMA_PUSH_SENTRY_LIVENESS_TOKEN fehlt in $CONF" \ + "Monitor anlegen: .empire/tools/kuma-add-sentry-liveness.mjs" + +code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 15 \ + "${KUMA_PUSH_BASE}/${KUMA_PUSH_SENTRY_LIVENESS_TOKEN}?status=up&msg=$(printf 'sentry antwortet in %ss' "$latency" | jq -sRr @uri)" 2>&1)" +[ "$code" != "200" ] && fail "kuma-push-failed" "HTTP $code" "Kuma/Monitor sentry-liveness pruefen" +write_status "OK" "" "" "$latency" +echo "KUMA=heartbeat gepusht" +echo "RESULT=OK" +exit 0 diff --git a/.empire/tools/sentry-set-model.sh b/.empire/tools/sentry-set-model.sh new file mode 100644 index 0000000000..5495dc3a70 --- /dev/null +++ b/.empire/tools/sentry-set-model.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Modellwechsel fuer den Dauer-Agenten "Sentry" — mit Beweis und Selbst-Rollback (buzz#77). +# +# WOFUER +# ------ +# Sentry haengt an einem FREIEN OpenRouter-Modell. Freie Modelle sind kein Vertrag: +# sie laufen in Rate-Limits (`429`), verschwinden ohne Ankuendigung (`404`) oder +# vertragen die Tool-Schemata des Harness nicht (`422 ... schema references`). Dann +# ist der Agent still — der Prozess laeuft, die Antwort bleibt aus. +# +# Der Wechsel selbst ist eine Zeile in der Env-Datei. Gefaehrlich ist nicht der +# Wechsel, sondern der ungepruefte Wechsel: ein Tippfehler im Modellnamen macht den +# Agenten genauso stumm wie das kaputte Modell, nur merkt es niemand, weil "ich habe +# ja umgestellt" wie eine Loesung aussieht. Deshalb beweist dieses Script den neuen +# Zustand, bevor es ihn behaelt — und stellt sonst selbst zurueck. +# +# bash sentry-set-model.sh # z. B. deepseek/deepseek-chat-v3.1:free +# bash sentry-set-model.sh --show # aktuelles Modell +# +# Exit 0 = neues Modell aktiv UND bewiesen. Exit 1 = zurueckgerollt (altes Modell +# wieder aktiv und ebenfalls geprueft). Exit 2 = Rollback selbst fehlgeschlagen — +# nur dann ist Handarbeit noetig, und dann sagt es das deutlich. +set -uo pipefail + +CONF="${SENTRY_CONF:-/opt/buzz-agents/sentry.conf}" +PROBE="${SENTRY_LIVENESS_PROBE:-/opt/buzz-agents/liveness-probe.sh}" +UNIT="${SENTRY_UNIT:-buzz-sentry}" +PROBE_TIMEOUT="${SENTRY_SET_MODEL_TIMEOUT:-120}" + +current() { sed -n 's/^OPENROUTER_MODEL=//p' "$CONF" | tr -d '\r'; } + +[ -w "$CONF" ] || { echo "FEHLER: $CONF nicht schreibbar (als root ausfuehren)"; exit 2; } + +if [ "${1:-}" = "--show" ] || [ $# -eq 0 ]; then + echo "MODELL=$(current)" + [ $# -eq 0 ] && { echo "Aufruf: sentry-set-model.sh "; exit 1; } + exit 0 +fi + +NEW="$1" +OLD="$(current)" +[ -n "$OLD" ] || { echo "FEHLER: OPENROUTER_MODEL steht nicht in $CONF"; exit 2; } +[ "$NEW" = "$OLD" ] && { echo "MODELL=$OLD (unveraendert, nichts zu tun)"; exit 0; } +echo "ALT=$OLD" +echo "NEU=$NEW" + +apply() { + # Punkte und Schraegstriche in Modellnamen: sed-Trennzeichen ist deshalb '|'. + sed -i "s|^OPENROUTER_MODEL=.*|OPENROUTER_MODEL=$1|" "$CONF" + systemctl restart "$UNIT" || return 1 + sleep 12 + return 0 +} + +verify() { + # --no-push: ein Modelltest darf den Heartbeat nicht faelschen. Der Heartbeat + # gehoert dem planmaessigen Timer, nicht diesem Handlauf. + SENTRY_LIVENESS_TIMEOUT="$PROBE_TIMEOUT" bash "$PROBE" --no-push +} + +cp "$CONF" "$CONF.bak-setmodel" +if ! apply "$NEW"; then + echo "RESULT=FAIL"; echo "REASON=restart-failed"; apply "$OLD" >/dev/null 2>&1; exit 2 +fi + +if verify; then + echo "RESULT=OK" + echo "MODELL=$NEW aktiv und durch eine echte Antwort belegt" + exit 0 +fi + +echo "--- Neues Modell antwortet nicht -> automatischer Rollback auf $OLD ---" +if ! apply "$OLD"; then + echo "RESULT=FAIL"; echo "REASON=rollback-restart-failed" + echo "FIX=$CONF.bak-setmodel zurueckkopieren und '$UNIT' von Hand starten" + exit 2 +fi +if verify; then + echo "RESULT=FAIL" + echo "REASON=new-model-silent" + echo "MODELL=$OLD wieder aktiv und geprueft — der Agent ist nicht schlechter dran als vorher" + exit 1 +fi +echo "RESULT=FAIL" +echo "REASON=rollback-verify-failed" +echo "FIX=Weder '$NEW' noch '$OLD' antworten. Ursache ist dann NICHT das Modell —" +echo " Relay-Abriss oder OpenRouter-Key pruefen: journalctl -u $UNIT -n 50" +exit 2