Welle 3 · Lucys Freiheiten je Aktionsart, Rückfragen mit „ja 7“, Lucy untersucht rote Hinweise, Wochenbericht

- services/lucy_autonomie.py: Stufe je Aktionsart (nur lesen, erst fragen, selbst handeln), Start „mehr Freiheit“;
  Meldung danach (POST /api/lucy/gehandelt)
- services/rueckfragen.py: nummerierte Rückfragen, nur Knöpfe der Oberfläche (ERLAUBT), „ja“ führt genau die
  gespeicherte Aktion aus, 24 h gültig, hinfällig wenn der Hinweis weg ist, Nachprüfung danach
- notify.sh -k: Antworttasten (Antworttastatur über den Direktweg, ohne Eingriff in Hermes)
- services/ermittlung.py: Wächter meldet rote Hinweise an Lucy; Knopf wird Rückfrage, Lucy sieht über den
  api_server nach und schickt Ursache und Weg als eine Nachricht
- services/wochenbericht.py + mc2-wochenbericht.timer (So 07:30): feste Sätze, Absatz vom Modell, offene
  App-Updates als Rückfragen
- MCP: Werkzeuge fragen die Freiheiten (auf_bitte), neu: freiheiten, rueckfragen, rueckfrage_beantworten,
  sicherheitsupdates_einspielen, snapshot_zurueckspielen, snapshots, geraet_logs, dienst_logs
- Ausführer-Aktion journal (nur lesend), POST /api/homelab/ziele/{id}/sicherheitsupdates
- Läufe des Homelabs mit flock (Oberfläche und Steward schreiben beide)
- Skill homelab-orchestrator, SOUL-Abschnitt als Kopie in deploy/nur-box

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 19:12:50 +02:00
co-authored by Claude Opus 5.5
parent dd23f3fb4c
commit 12cefb427a
37 changed files with 2649 additions and 169 deletions
+28
View File
@@ -0,0 +1,28 @@
"""Das Protokoll eines Containers für Lucy (seit 25.09.2026, Ausbauplan Welle 3, Punkt 14: „Lucy liest Logs und
Messwerte, erklärt den Befund“).
Über den Ausführer (Aktion journal, nur lesend): Warnungen und Fehler der letzten 24 Stunden aus dem Journal, ohne
systemd (Alpine) die letzten Zeilen von /var/log/messages. Nur für freigegebene Container — in eine VM kommt der
Ausführer nicht hinein. Lucys Werkzeug geraet_logs() liest es über GET /api/homelab/ziele/{id}/journal.
"""
from services.homelab import inventar, kanal
ZEITLIMIT_S = 90
def lesen(ziel_id: str, zeilen: int = 80) -> dict:
gast = inventar.gast(ziel_id)
if not gast:
return {"ok": False, "detail": "Dieses Gerät steht nicht im Bericht des Ausführers."}
if gast.get("art") != "lxc":
return {"ok": False, "detail": "Protokolle gibt es nur von Containern; in eine VM kommt der Ausführer nicht."}
if not gast.get("erlaubt"):
return {"ok": False, "detail": "Dieses Gerät ist nicht freigegeben (Etikett watcher fehlt)."}
auftrag = kanal.anlegen("journal", {"vmid": gast["vmid"], "zeilen": max(20, min(int(zeilen), 300))})
ergebnis = kanal.warten(auftrag, ZEITLIMIT_S)
if not ergebnis:
return {"ok": False, "detail": "Der Ausführer hat nicht rechtzeitig geantwortet."}
if ergebnis.get("status") != "fertig":
return {"ok": False, "detail": str(ergebnis.get("text") or "Das Protokoll ließ sich nicht lesen.")[:500]}
return {"ok": True, "text": str(ergebnis.get("text") or "").strip() or "(keine Warnungen oder Fehler)"}
+1 -1
View File
@@ -34,7 +34,7 @@ except ImportError: # Windows (Entwicklung): nur die Thread-Sperre
AKTIONEN = {"bericht", "snapshot", "update", "os_update", "suchen", "zurueck", "snapshot_loeschen",
"host_update", "host_neustart", "sichern", "sicherung_zurueck", "sicherung_loeschen",
"kernel_aufraeumen", "sicherung_probe", "etikett_setzen", "changelog"}
"kernel_aufraeumen", "sicherung_probe", "etikett_setzen", "changelog", "journal"}
VERLOREN_S = 3 * 3600 # abgeholt, aber nie beantwortet (Ausführer abgestürzt, Host neu gestartet)
BEHALTEN = 500 # so viele erledigte Aufträge bleiben sichtbar (Protokoll; bis 24.09.2026: 60)
VOLLTEXT = 60 # die jüngsten erledigten behalten ihre ganze Ausgabe …
+2
View File
@@ -72,6 +72,8 @@ _AKTION = {
"etikett_setzen": ("Etikett gesetzt", "Etikett setzen gescheitert", "Etikett wird gesetzt", "Etikett setzen"),
"changelog": ("Änderungslisten gelesen", "Änderungslisten nicht gelesen", "Änderungslisten werden gelesen",
"Änderungslisten"),
# Seit 25.09.2026: Lucy liest das Protokoll eines Containers, wenn sie einen Hinweis untersucht.
"journal": ("Protokoll gelesen (Lucy)", "Protokoll lesen gescheitert", "Protokoll wird gelesen", "Protokoll lesen"),
}
_HOST_AKTIONEN = {"bericht", "host_update", "host_neustart", "kernel_aufraeumen", "sicherung_probe", "changelog"}
_LAUF_WAS = {"app": "Update", "os": "Paket-Update", "pakete": "Paket-Update", "docker": "Docker-Update",
+25 -1
View File
@@ -35,6 +35,8 @@ import re
import threading
import time
import uuid
from collections.abc import Iterator
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
@@ -44,6 +46,11 @@ from kern.zeit import LOCAL_TZ
from services import announce, update_verlauf
from services.homelab import apps, arcane, inventar, kanal, karenz, sicherheit
try:
import fcntl # Linux: Sperre über Prozessgrenzen
except ImportError: # Windows (Entwicklung): nur die Thread-Sperre
fcntl = None
log = logging.getLogger(__name__)
WARTEN_NACH_UPDATE_S = 20
@@ -74,8 +81,25 @@ def _laeufe() -> list[dict]:
return daten if isinstance(daten, list) else []
@contextmanager
def _datei_sperre() -> Iterator[None]:
"""Seit 25.09.2026 schreiben zwei Prozesse Läufe: die Oberfläche (Knöpfe) und der Steward (Wartungsfenster). Ohne
flock könnte einer den Lauf des anderen mit einer älteren Liste überschreiben."""
if fcntl is None:
yield
return
pfad = _pfad().with_suffix(".lock")
pfad.parent.mkdir(parents=True, exist_ok=True)
with open(pfad, "a", encoding="utf-8") as f:
fcntl.flock(f, fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(f, fcntl.LOCK_UN)
def _merken(lauf: dict) -> None:
with _lock:
with _lock, _datei_sperre():
laeufe = [x for x in _laeufe() if x["id"] != lauf["id"]] + [lauf]
_pfad().parent.mkdir(parents=True, exist_ok=True)
tmp = _pfad().with_suffix(".tmp")