Systemaudit vom 27.08.2026. Alle Befunde gemessen, nicht vermutet.
VIER STILLE DEFEKTE
1. mc2-steward startete seit Wochen nicht (live: 207.609 Neustarts).
steward.py importierte services.memory, das beim Mem0-Ausbau geloescht
wurde -> ImportError bei jedem Start. Re-Warm- und Health-Waechter
waren damit tot.
2. Jedes Hermes-Update wurde automatisch zurueckgerollt.
hermes-postcheck.sh prueft vier Dinge, die es seit dem 07.08. nicht mehr
gibt (Sidecar :8765, /api/memory, memory.provider, mc2-memory-Plugin).
Die Checks konnten nicht gruen werden -> autoupdate.sh wertete jedes
Update als rot und rollte es zurueck. Checks ersatzlos entfernt; der
Tool-Smoke laeuft ohnehin durch den echten Agenten.
3. 7 von 12 Skills waren per Knopfdruck nicht startbar.
deploy.sh kopiert Skills mit tr '-' '_' nach ~/.hermes/skills/,
routers/skills.py gab Hermes aber den Ordnernamen MIT Bindestrich.
Der Knopf meldete Erfolg, ausgefuehrt wurde nichts. Neu: _hermes_name().
4. deploy.sh warf bei jedem Deploy die Live-Modellkonfiguration weg.
MC2 schreibt /etc/llama-swap/config.yaml selbst; die Repo-Datei ist nur
ein Abzug (ihm fehlt u.a. kritiker/Devstral). Jetzt: erst sichern, Diff
zeigen, dann kopieren. MC_DEPLOY_SKIP_SWAP_CONFIG=1 ueberspringt.
MEM0-AUSBAU VOLLENDET (Kriterium 3: 17 -> 0 Dateien)
- mem0_service/, mcp/mcp_memory.py und hermes/plugins/mc2-memory entfernt;
das Plugin schickte bei JEDEM Turn zwei 404-Requests an tote Routen.
- MEMORY_DB/MEM0_SERVICE_URL, _mem0_reachable(), MC_MEMORY_DB und
MC_MEM_DEDUPE_ENABLED aus Config/Router/Unit entfernt.
- mem0_ms war strukturell tot (park("retrieve") wird nirgends mehr
aufgerufen) -> aus Backend, API-Typ und Latenzkarte entfernt.
- Verbinden-Tab: tote Gedaechtnis-MCP-Leitung raus, Status-Kachel bleibt.
- AGENTS.md beschrieb Mem0 noch als aktiv - korrigiert.
GATEWAY-ROBUSTHEIT
- _proxy gab bei ungueltigen Payloads HTTP 500 (gemessen 5/5: Rohtext,
leerer Body, JSON-Liste, JSON-String, null) -> jetzt 5/5 HTTP 400.
- Bild-Weiche ohne Deckel: 10 Bilder x 2 Versuche x 240 s hielten den
Client bis zu 80 min. Neu: MC_CODER_IMAGE_MAX (4), Rueckfall auf die
Vision-Umleitung.
- /v1/models: nicht-JSON von der Engine gab 500 -> jetzt 502.
UNITS UND DEPLOY
- mc2-steward.service, dessen warmset-Drop-in und voice-service.service
fehlten im Repo, obwohl maintenance.py und stack-postcheck.sh sie
voraussetzen. 1:1 von der laufenden Box uebernommen.
- deploy.sh startete mc2-steward nie neu; restore.sh liess mc2-gateway und
mc2-steward mit alter Config weiterlaufen. Beide ergaenzt.
FRONTEND
- useEigenleben rief /api/eigenleben - existiert im Backend nicht und wurde
nirgends genutzt. Samt Typen entfernt.
- Anleitung beschrieb einen Gedaechtnis-Tab, den es nicht gibt.
- Abgeglichen: alle uebrigen 63 Frontend-Aufrufe treffen echte Routen, alle
5 SSE-Invalidation-Keys sind gemappt, keine ungefangenen Promises.
Gates: compileall gruen - ruff "All checks passed" - tsc gruen - vite build
gruen (dist aktualisiert) - Importe app/steward/gateway_app gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
4.7 KiB
Python
103 lines
4.7 KiB
Python
import os
|
|
import subprocess
|
|
|
|
import psutil
|
|
from fastapi import APIRouter
|
|
from pydantic import BaseModel
|
|
|
|
router = APIRouter(prefix="/api")
|
|
|
|
|
|
def _hermes_name(ordner: str) -> str:
|
|
"""Repo-Ordnername -> Hermes-Skillname.
|
|
|
|
deploy/deploy.sh kopiert `deploy/skills/<name>` nach `~/.hermes/skills/<name mit _>`
|
|
(`tr '-' '_'`), weil Hermes keine Bindestriche in Skillnamen mag. Ohne dieselbe
|
|
Umschreibung hier bekam Hermes den Ordnernamen MIT Bindestrich und fand den Skill
|
|
nicht - betroffen waren 7 der 12 Skills (betrieb-playbook, konzept-fliessband,
|
|
llm-wiki, morning-report, pc-pfad-cache, projekt-start, trend-radar): der Startknopf
|
|
meldete Erfolg, ausgefuehrt wurde nichts. deploy.sh bleibt die Quelle der Wahrheit.
|
|
"""
|
|
return ordner.replace("-", "_")
|
|
|
|
|
|
class RunSkillRequest(BaseModel):
|
|
skill_name: str
|
|
|
|
@router.get("/skills")
|
|
def list_skills():
|
|
"""Listet alle verfügbaren Skills aus dem deploy/skills Verzeichnis auf."""
|
|
# Pfad relativ zu dieser Datei (backend/routers/skills.py)
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
repo_root = os.path.dirname(os.path.dirname(current_dir))
|
|
skills_dir = os.path.join(repo_root, "deploy", "skills")
|
|
skills = []
|
|
|
|
# Check currently running hermes skill processes
|
|
running_skills = set()
|
|
for p in psutil.process_iter(['name', 'cmdline']):
|
|
try:
|
|
cmdline = p.info.get('cmdline')
|
|
if cmdline and any("hermes" in arg for arg in cmdline):
|
|
for arg in cmdline:
|
|
if "Führe den " in arg and " Skill aus" in arg:
|
|
# Extract skill name from "Führe den 'skill_name' Skill aus"
|
|
import re
|
|
match = re.search(r"Führe den '(.+?)' Skill aus", arg)
|
|
if match:
|
|
running_skills.add(match.group(1))
|
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
pass
|
|
|
|
if os.path.exists(skills_dir) and os.path.isdir(skills_dir):
|
|
for entry in os.scandir(skills_dir):
|
|
if entry.is_dir():
|
|
# Suche nach SKILL.md für Metadaten
|
|
skill_md_path = os.path.join(entry.path, "SKILL.md")
|
|
description = ""
|
|
if os.path.exists(skill_md_path):
|
|
try:
|
|
with open(skill_md_path, "r", encoding="utf-8") as f:
|
|
# Lese erste Zeilen für Description (YAML Frontmatter)
|
|
lines = f.readlines()[:10]
|
|
for line in lines:
|
|
if "description:" in line.lower():
|
|
description = line.split(":", 1)[1].strip().strip('"\'')
|
|
break
|
|
elif "author:" in line.lower() and not description:
|
|
description = line.strip()
|
|
except Exception:
|
|
pass
|
|
|
|
skills.append({
|
|
"name": entry.name,
|
|
# Der Name, unter dem Hermes den Skill kennt - auch fuer den Laufend-
|
|
# Vergleich, weil im Prozess-cmdline genau dieser Name steht.
|
|
"hermes_name": _hermes_name(entry.name),
|
|
"description": description or "Keine Beschreibung verfügbar.",
|
|
"running": _hermes_name(entry.name) in running_skills
|
|
})
|
|
return {"skills": skills}
|
|
|
|
@router.post("/skills/run")
|
|
def run_skill(req: RunSkillRequest):
|
|
"""Startet einen autonomen Skill via Hermes im Hintergrund."""
|
|
hermes_bin = os.path.expanduser("~/.local/bin/hermes")
|
|
if not os.path.exists(hermes_bin):
|
|
# Fallback falls lokal (auf Windows) entwickelt wird
|
|
return {"ok": False, "err": "Hermes CLI nicht gefunden (~/.local/bin/hermes fehlt). Bist du lokal unterwegs?"}
|
|
|
|
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat - mit dem Namen, den Hermes
|
|
# nach dem Deploy-Sync wirklich kennt (Bindestriche -> Unterstriche, s. _hermes_name).
|
|
skill = _hermes_name(req.skill_name)
|
|
cmd = [hermes_bin, "-z", f"Führe den '{skill}' Skill aus", "chat"]
|
|
try:
|
|
log_path = os.path.expanduser(f"~/.hermes/logs/skill_{skill.replace('/', '_')}.log")
|
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
with open(log_path, "a", encoding="utf-8") as f:
|
|
f.write(f"\n--- Starting Skill: {skill} ---\n")
|
|
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
|
|
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
|
|
except Exception as e:
|
|
return {"ok": False, "err": str(e)}
|