feat(2.0): Phase 4 — Hermes-Schicht (MC-Seite + Runbook)

MC-Seite: services/agent.py + routers/agent.py (GET /api/agent/status:
Gateway/WebUI-Reachability + Verdrahtungs-Hinweise), AgentView (Status-
Tiles, Hermes-oeffnen-Button, Offline-Hinweis). deploy/hermes-webui.service
(systemd-USER, :8787). Box-Runbook docs/HERMES_SETUP.md (hermes-webui
installieren, Brain=model:auto via Gateway, Tools/MCP verdrahten inkl.
mcp_mc+mcp_memory, SSH-Windows, LiteLLM-Caveat #26489).

Lokal verifiziert: agent/status + Frontend-Build + Browser (Hermes-View).
Box-Ausfuehrung der Verdrahtung steht aus (Runbook). Docs aktualisiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-25 08:22:28 +02:00
parent 81468df9c0
commit c773dd7eae
16 changed files with 428 additions and 174 deletions
+6 -2
View File
@@ -7,7 +7,7 @@ siehe Architektur-Plan (`docs/` bzw. der genehmigte Plan).
Mission Control 2.0 (FastAPI + React/shadcn) · Hermes Agent + hermes-webui ·
Shared Memory (SQLite via MCP). Jede Schicht hinter stabilem Vertrag austauschbar.
## Status: Phase 3 (Memory + MCP) ✅
## Status: Phase 4 (Hermes-Schicht — MC-Seite) ✅ · Box-Ausführung offen
Fortschritt & Resume-Guide: siehe [`docs/STATUS.md`](docs/STATUS.md).
@@ -21,8 +21,12 @@ Fortschritt & Resume-Guide: siehe [`docs/STATUS.md`](docs/STATUS.md).
- **Phase 3** — Geteiltes **Gedächtnis** (SQLite/WAL, 5 Kategorien, Dedupe-Kurator) + **MCP-Server**
(`mcp/mcp_memory.py` shared, `mcp/mcp_mc.py` Stack-Management für Hermes), MemoryView.
- **Phase 4** — Hermes-**Agent-Status** (`/api/agent/status`, AgentView mit Tiles + „Hermes öffnen"),
`deploy/hermes-webui.service`, **Box-Runbook** [`docs/HERMES_SETUP.md`](docs/HERMES_SETUP.md)
(hermes-webui, Brain=`auto`, Tools/MCP-Verdrahtung). Box-Ausführung steht noch aus.
API: `health · models · discover · fit · models/register · groups · routing · system/* · connect ·
memory/*` (Details in `docs/STATUS.md`). MCP: `mcp/` (siehe `mcp/requirements.txt`).
memory/* · agent/status` (Details in `docs/STATUS.md`). MCP: `mcp/` (siehe `mcp/requirements.txt`).
## Entwickeln
+2 -1
View File
@@ -13,7 +13,7 @@ from fastapi.staticfiles import StaticFiles
from starlette.requests import Request
from config import FRONTEND_DIST, VERSION
from routers import connect, health, memory, models, routing, system
from routers import agent, connect, health, memory, models, routing, system
app = FastAPI(title="Mission Control 2.0", version=VERSION)
@@ -40,6 +40,7 @@ app.include_router(routing.router)
app.include_router(system.router)
app.include_router(connect.router)
app.include_router(memory.router)
app.include_router(agent.router)
# Prod: gebautes Frontend ausliefern (falls vorhanden). SPA-Fallback auf index.html.
+7 -1
View File
@@ -38,6 +38,12 @@ GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", "http://127.0.0.1:4000").rstrip("
GATEWAY_CONFIG_PATH = Path(os.environ.get(
"MC_GATEWAY_CONFIG", str(Path(__file__).resolve().parent.parent / "gateway" / "config.yaml")))
# --- Hermes Agent (eigener Dienst auf der Box) -------------------------------
# Gateway (OpenAI-API des Agenten) + standalone Web-UI (nesquena/hermes-webui).
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
HERMES_WEBUI_URL = os.environ.get("HERMES_WEBUI_URL", "http://127.0.0.1:8787").rstrip("/")
HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes")))
# --- Server ------------------------------------------------------------------
HOST = os.environ.get("MC_HOST", "0.0.0.0")
PORT = int(os.environ.get("MC_PORT", "9000"))
@@ -46,7 +52,7 @@ PORT = int(os.environ.get("MC_PORT", "9000"))
FRONTEND_DIST = Path(os.environ.get("MC_FRONTEND_DIST", str(Path(__file__).resolve().parent.parent / "frontend" / "dist")))
# Version (Phase 0 — Greenfield-Skeleton).
VERSION = "2.0.0-phase3"
VERSION = "2.0.0-phase4"
# Gemeinsame YAML-Instanz (preserve_quotes hält Kommentare/Quotes in config.yaml).
yaml = YAML()
+12
View File
@@ -0,0 +1,12 @@
"""Agent-Endpoint: Hermes-Status + WebUI-Link (MC verlinkt nur, betreibt nicht)."""
from fastapi import APIRouter
from services.agent import agent_status
router = APIRouter(prefix="/api")
@router.get("/agent/status")
def status() -> dict:
return agent_status()
+33
View File
@@ -0,0 +1,33 @@
"""
Hermes-Agent-Status (Control-Plane-Read). MC betreibt Hermes NICHT — es zeigt nur
Status + verlinkt das standalone hermes-webui. Voller Zugriff + Tools/MCP werden in
Hermes' eigener Config verdrahtet (siehe docs/HERMES_SETUP.md).
"""
import httpx
from config import HERMES_API_URL, HERMES_HOME, HERMES_WEBUI_URL
def _reach(url: str, path: str = "") -> bool:
try:
with httpx.Client(timeout=3.0) as c:
return c.get(f"{url}{path}").status_code < 500
except Exception:
return False
def agent_status() -> dict:
"""Erreichbarkeit von Gateway (:8642) + WebUI (:8787) + lokale Hinweise."""
home = HERMES_HOME
return {
"gateway_url": HERMES_API_URL,
"webui_url": HERMES_WEBUI_URL,
"gateway_reachable": _reach(HERMES_API_URL, "/v1/models"),
"webui_reachable": _reach(HERMES_WEBUI_URL),
"home_exists": home.exists(),
# Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig).
"has_config": (home / "config.yaml").exists() or (home / "config.json").exists(),
"has_skills": (home / "skills").exists(),
"has_memories": (home / "memories").exists(),
}
+27
View File
@@ -0,0 +1,27 @@
# systemd-USER-Unit für das standalone Hermes-WebUI (nesquena/hermes-webui).
# Eigene Oberfläche des Agenten (Chat/Sessions/Workspace/Cron/Skills/Memory).
# An 0.0.0.0:8787 gebunden, damit der Windows-PC im LAN direkt zugreifen kann
# → Passwort-Schutz aktivieren (HERMES_WEBUI_PASSWORD)!
# Ablage: ~/.config/systemd/user/hermes-webui.service ; dann:
# systemctl --user enable --now hermes-webui
# loginctl enable-linger hitonabi # läuft auch ohne aktive Session
[Unit]
Description=Hermes WebUI (standalone)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/hermes-webui
# Start über das mitgelieferte Control-Skript (siehe docs/HERMES_SETUP.md).
ExecStart=%h/hermes-webui/ctl.sh start --foreground
Environment=HERMES_WEBUI_HOST=0.0.0.0
Environment=HERMES_WEBUI_PORT=8787
Environment=HERMES_HOME=%h/.hermes
# Environment=HERMES_WEBUI_PASSWORD=... # in ~/.config/environment.d/ setzen, NICHT hier committen
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
+65
View File
@@ -0,0 +1,65 @@
# Hermes-Schicht — Box-Runbook (Phase 4)
> Diese Schritte laufen **auf der Bosgame** (`192.168.178.151`, User `hitonabi`).
> MC betreibt Hermes nicht — es zeigt nur Status + verlinkt das WebUI. Hier wird die
> eigentliche **volle Verdrahtung** gemacht (das war in v1 der „Hermes ist dumm"-Grund).
## Reihenfolge der Dienste
`llama-swap (:8080)``LiteLLM-Gateway (:4000)``hermes-gateway (:8642)``hermes-webui (:8787)`
## 1. LiteLLM-Gateway starten + Auto-Routing prüfen
```bash
uv pip install "litellm[proxy]" # oder pipx
litellm --config /opt/mission-control-2/gateway/config.yaml --port 4000
curl -s http://127.0.0.1:4000/v1/models # fast/heavy/vision/coder/auto?
# 'auto' testen: einfacher Prompt → fast; harter/zu langer → Fallback heavy.
```
⚠️ **Complexity-Auto-Router** (model:auto wählt nach Schwierigkeit) ist versionsabhängig — gegen die
installierte LiteLLM-Version verifizieren (docs.litellm.ai/docs/proxy/auto_routing). Baseline =
fast + Fallback heavy ist bereits in `gateway/config.yaml`.
## 2. Engine: Hirne + Ko-Residenz (llama-swap groups)
In MC (Modelle & Routing) bzw. direkt: Qwen3.6-35B-A3B (Alias `fast`, `--jinja`!) und
Qwen3.5-122B-A10B (Alias `heavy`) eintragen, Gruppe `brains` mit `swap:false` (ko-resident).
```bash
curl -s http://127.0.0.1:9001/api/groups # MC2 (Phase 0/1 API)
```
## 3. hermes-webui installieren (standalone)
```bash
cd ~ && git clone https://github.com/nesquena/hermes-webui && cd hermes-webui
python3 bootstrap.py # erkennt hermes-agent, installiert Abhängigkeiten
# Dienst: deploy/hermes-webui.service → ~/.config/systemd/user/, Passwort setzen:
mkdir -p ~/.config/environment.d
echo 'HERMES_WEBUI_PASSWORD=<dein-passwort>' > ~/.config/environment.d/hermes-webui.conf
systemctl --user enable --now hermes-webui
loginctl enable-linger hitonabi
```
Zugriff vom Windows-PC: `http://192.168.178.151:8787` (mit Passwort).
## 4. Hermes-Brain = `model: auto` über den Gateway
In `~/.hermes/config.yaml` (bzw. via `hermes`-CLI/WebUI-Settings) das Modell auf den **Gateway** zeigen:
```yaml
model:
provider: openai # NICHT 'custom' → Hänger-Bug #26489
base_url: http://127.0.0.1:4000/v1
api_key: local
name: auto # schnell im Alltag, eskaliert auf heavy
```
## 5. Tools/MCP verdrahten (der eigentliche Fix)
Hermes' Tools sind by-default AUS. Aktivieren:
- **Built-in:** terminal/shell, read_file/search_files, file-ops. `approvals: auto` (rein lokal).
- **MCP-Server** (`hermes mcp` oder config `mcp_servers`):
- geteiltes Gedächtnis: `python /opt/mission-control-2/mcp/mcp_memory.py` (Env `MC_URL=http://127.0.0.1:9001`)
- Stack-Management: `python /opt/mission-control-2/mcp/mcp_mc.py` (Env `MC_URL=http://127.0.0.1:9001`)
- **SSH→Windows-PC** (voller Zugriff auf deinen PC): OpenSSH-Server auf Windows aktiv + Key
`~/.ssh/id_ed25519_hermes_agent` autorisiert; Hermes nutzt sein Shell-Tool für `ssh TobisPC@<win-ip> …`.
- **100 % lokal:** Hermes' eingebauter web_search/browser laufen über Nous Portal (Cloud). Für rein
lokal stattdessen einen **lokalen Browser-/Such-MCP** binden (optional, später).
## 6. Verifikation
- WebUI öffnet vom Windows-PC, Chat antwortet (Brain via Gateway).
- Hermes kann via `mcp_mc` ein Modell listen/Routing ändern (`list_models`, `set_route`).
- Hermes erreicht den Windows-PC (SSH-Kommando) und das Netz.
- 14B-Loop weg (Guard-Beschreibungen in mcp_memory + tool-fähiges Brain).
+8 -6
View File
@@ -21,7 +21,9 @@
(CRUD/export/dedupe), `mcp/mcp_memory.py` (Guard-Beschreibungen gegen Loop) + `mcp/mcp_mc.py` NEU
(Stack-Management-Tools für Hermes: list/discover/register/route/restart/status). Frontend
MemoryView (Add/Filter/Suche/Delete/Aufräumen). Lokal verifiziert (CRUD+Dedupe; MCP syntax-OK).
- [ ] **Phase 4 — Hermes-Schicht** (hermes-webui Dienst, Brain=auto, Tools/MCP verdrahten). **Braucht Box.**
- [~] **Phase 4 — Hermes-Schicht:** MC-Seite ✅ (services/agent.py + /api/agent/status, AgentView mit
Status-Tiles + „Hermes öffnen"), `deploy/hermes-webui.service`, **Runbook `docs/HERMES_SETUP.md`**.
**Box-Ausführung offen** (hermes-webui installieren, Brain=auto, Tools/MCP verdrahten — Runbook).
- [ ] **Phase 5 — Betrieb/Observability/Politur** (Backup, LiteLLM-Traces, Health).
- [ ] **Phase 6 — Cutover** (/opt → v2, v1 aus). **Braucht Box.**
- Offen aus Phase 0/1: **Box-Deploy** (Gitea-Repo da; Box-Setup als systemd-USER-Dienst noch offen),
@@ -50,10 +52,10 @@ cd frontend && npm run build
- `GET /api/connect?host=` — IDE-/Agent-Snippets (Gateway + Memory-MCP)
- `GET/POST/PUT/DELETE /api/memory[...]` + `/api/memory/export` + `/api/memory/dedupe` — geteiltes Gedächtnis
- `mcp/mcp_memory.py` (geteiltes Memory) + `mcp/mcp_mc.py` (Stack-Management für Hermes) — stdio-MCP
- `GET /api/agent/status` — Hermes Gateway/WebUI-Erreichbarkeit + Verdrahtungs-Hinweise
## Nächster sinnvoller Schritt
**Phase 4 (Hermes-Schicht) braucht die Box** — hermes-webui als Dienst, Brain=model:auto, Tools/MCP
verdrahten (FS/Shell/SSH→Win/Netz + mcp_mc + mcp_memory), LiteLLM-Caveat verifizieren. Vorher sinnvoll:
**Box-Deploy** einrichten (systemd-USER-Dienst, sudo-frei; `deploy/deploy.sh` auf User-Dienst umstellen,
kein /opt/sudo) + **LiteLLM-Gateway** auf der Box starten und `model:auto`/Complexity-Router verifizieren.
Lokal noch machbar ohne Box: Phase-4-Frontend (Agent-Status/Verdrahtungs-Anzeige + „Hermes öffnen").
Alle **lokal baubaren** Teile sind fertig (Phase 04 MC-Seite + Phase 5-Vorbereitung folgt). Was jetzt
die **Box** braucht: **Box-Deploy** (systemd-USER-Dienst, sudo-frei) → **LiteLLM-Gateway** starten +
`model:auto` verifizieren → **Hermes-Schicht** nach `docs/HERMES_SETUP.md` einrichten → **Cutover** (Phase 6).
Lokal noch offen/sinnvoll: Phase 5 (Backup-Skript, Observability/Health-Anzeige im System-Tab).
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
<meta name="theme-color" content="#0d1117" />
<link rel="manifest" href="/manifest.webmanifest" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-ByXSUESS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CE_qxW4X.css">
<script type="module" crossorigin src="/assets/index-umMcKqw6.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-nVsgoG2p.css">
</head>
<body>
<div id="root"></div>
+3 -1
View File
@@ -7,6 +7,7 @@ import { RoutingView } from "@/views/RoutingView"
import { SystemView } from "@/views/SystemView"
import { ConnectView } from "@/views/ConnectView"
import { MemoryView } from "@/views/MemoryView"
import { AgentView } from "@/views/AgentView"
import { Placeholder } from "@/views/Placeholder"
import { api, type Health } from "@/lib/api"
import { cn } from "@/lib/utils"
@@ -96,7 +97,8 @@ export default function App() {
{view === "system" && <SystemView />}
{view === "connect" && <ConnectView />}
{view === "memory" && <MemoryView />}
{!["models", "routing", "system", "connect", "memory"].includes(view) && (
{view === "agent" && <AgentView />}
{!["models", "routing", "system", "connect", "memory", "agent"].includes(view) && (
<Placeholder title={active.label} hint={active.hint} />
)}
</main>
+11
View File
@@ -116,6 +116,17 @@ export interface DedupeResult {
applied: boolean
}
export interface AgentStatus {
gateway_url: string
webui_url: string
gateway_reachable: boolean
webui_reachable: boolean
home_exists: boolean
has_config: boolean
has_skills: boolean
has_memories: boolean
}
export interface Health {
status: string
version: string
+86
View File
@@ -0,0 +1,86 @@
import { useEffect, useState } from "react"
import { ExternalLink } from "lucide-react"
import { api, type AgentStatus } from "@/lib/api"
import { cn } from "@/lib/utils"
function Dot({ ok }: { ok: boolean }) {
return <span className={cn("h-2 w-2 rounded-full", ok ? "bg-emerald-500" : "bg-amber-500")} />
}
function Tile({ label, ok, detail }: { label: string; ok: boolean; detail?: string }) {
return (
<div className="rounded-lg border border-border bg-card p-4">
<div className="flex items-center gap-2">
<Dot ok={ok} />
<span className="text-sm font-medium">{label}</span>
</div>
{detail && <div className="mt-1 text-xs text-muted-foreground">{detail}</div>}
</div>
)
}
export function AgentView() {
const [s, setS] = useState<AgentStatus | null>(null)
const [error, setError] = useState("")
useEffect(() => {
const load = () => api<AgentStatus>("/api/agent/status").then(setS).catch((e) => setError(String(e)))
load()
const t = setInterval(load, 8000)
return () => clearInterval(t)
}, [])
return (
<div className="space-y-4">
<div className="flex items-start justify-between">
<div>
<h1 className="text-xl font-semibold">Hermes</h1>
<p className="text-sm text-muted-foreground">
Der autonome Agent läuft eigenständig (Gateway :8642) und hat seine eigene Oberfläche
(hermes-webui :8787). Mission Control verlinkt nur Chat &amp; Steuerung leben dort.
</p>
</div>
{s?.webui_url && (
<a
href={s.webui_url}
target="_blank"
rel="noopener"
className={cn(
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium",
s.webui_reachable
? "bg-primary text-primary-foreground hover:opacity-90"
: "border border-border text-muted-foreground",
)}
>
<ExternalLink className="h-4 w-4" /> Hermes öffnen
</a>
)}
</div>
{error && <div className="text-sm text-muted-foreground">Status nicht lesbar ({error}).</div>}
{s && (
<>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Tile label="Gateway (:8642)" ok={s.gateway_reachable} detail={s.gateway_reachable ? "erreichbar" : "offline"} />
<Tile label="WebUI (:8787)" ok={s.webui_reachable} detail={s.webui_reachable ? "erreichbar" : "offline"} />
<Tile label="Brain" ok={s.gateway_reachable} detail="model: auto (über Gateway)" />
<Tile label="Verdrahtung" ok={s.has_config} detail={`config ${s.has_config ? "✓" : "—"} · skills ${s.has_skills ? "✓" : "—"} · memories ${s.has_memories ? "✓" : "—"}`} />
</div>
{!s.gateway_reachable && (
<div className="rounded-xl border border-dashed border-border bg-card/50 p-5 text-sm">
<div className="mb-2 font-medium">Hermes ist (hier) offline</div>
<p className="text-muted-foreground">
Der Agent + hermes-webui laufen auf der Box. Einrichtung & volle Verdrahtung
(Brain = <code>model: auto</code>, Tools/MCP inkl. <code>mcp_mc</code> + <code>mcp_memory</code>,
SSHWindows, lokale Browser-/Such-MCP) sind im Runbook beschrieben:
<code className="ml-1">docs/HERMES_SETUP.md</code>.
</p>
</div>
)}
</>
)}
</div>
)
}