2 Commits

Author SHA1 Message Date
Hitonabi cd3c5b5eed Verbinden: Zed als Haupt-Tool (Windows stabil seit 10/2025, Rust-nativ, Agent Panel) — OpenCode raus
User-Anforderung: leichtgewichtig statt VS-Code-Bloat + klickbar. Zed: ~16x weniger RAM,
<10ms Latenz, parallele Agenten, OpenAI-kompatible lokale Endpoints nativ. Meine fruehere
'Windows zweitklassig'-Einschaetzung war veraltet (Radar-Lehre #2 heute).
Finale 3: Zed (Haupt) · Kilo Code (VS-Code-Fallback/Orchestrator) · Claude Code (Sessions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:03:49 +02:00
Hitonabi 2db0ce080f Autonomie E2: Auto-Update-Timer (So 04:30) + Pin-Register + Rollback
- deploy/autoupdate.sh: Router->Engine->Hermes ueber bestehende Pfade; rot = Rollback + Selbst-Pinning (/srv/models/mc2-pins.json) + Telegram; gruen = Meldung "eingespielt X->Y"; Abschluss-Summary je Lauf
- Hermes-Rollback selbst gebaut (Job kann nur rot werden): git reset auf alten Stand + Config aus frischem Backup + Gateway-Neustart + Gehirn-Check
- sudoers-mc2-autonomie: NOPASSWD-Snippet fuer Engine/Router (einmalige sudo-Session); bis dahin wird die Ebene uebersprungen + gemeldet
- Verifiziert auf der Box: Happy-Path (11+2 Commits echt eingespielt), Fake-Fail -> Rollback+Pin+Meldung, Pin-Skip, Unpin -> regulaeres Update

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:01:13 +02:00
10 changed files with 233 additions and 20 deletions
+25 -15
View File
@@ -40,14 +40,22 @@ def build_snippets(host: str = DEFAULT_HOST,
"openAiModelId": "coding",
}, indent=2)
opencode = json.dumps({
"provider": {
"bosgame": {
"npm": "@ai-sdk/openai-compatible",
"name": "Bosgame Gateway",
"options": {"baseURL": gw, "apiKey": "local"},
"models": {IDE_MODEL: {"name": IDE_MODEL}},
# Zed = Rust-nativ, ~16x leichter als VS Code, Agent Panel mit parallelen Agenten,
# Windows stabil seit Okt 2025 — das Haupt-Tool für den User (Anti-Bloat + klickbar).
zed = json.dumps({
"language_models": {
"openai_compatible": {
"bosgame": {
"api_url": gw,
"available_models": [
{"name": IDE_MODEL, "display_name": "Box / coding", "max_tokens": 131072,
"capabilities": {"tools": True}}
],
}
}
},
"agent": {
"default_model": {"provider": "openai_compatible", "model": IDE_MODEL}
}
}, indent=2)
@@ -81,16 +89,18 @@ def build_snippets(host: str = DEFAULT_HOST,
"host": host,
"gateway_url": gw,
"mc_url": mc_url,
# Leitung 1 — das MODELL. Bewusst NUR 3 Tools (Verdikt 03.07.2026): Kilo Code (IDE,
# Multi-Agent via Orchestrator-Modus), OpenCode (Terminal), Claude Code (starke Sessions).
# Cursor/Zed/Continue/Roo entfernt — Roo eingestellt, Rest von Kilo abgedeckt bzw.
# cloud-first. Das Evolution-Radar (AUTONOMIE_PLAN E4) überwacht die Tool-Kategorie.
# Leitung 1 — das MODELL. Bewusst NUR 3 Tools (Verdikt 03.07.2026): Zed (Haupt-Tool:
# Rust-nativ/leichtgewichtig, Agent Panel, parallele Agenten, Windows stabil seit 10/2025),
# Kilo Code (VS-Code-Fallback mit Orchestrator-Modus), Claude Code (starke Sessions).
# Cursor/Continue/Roo/OpenCode entfernt — Roo eingestellt, Rest cloud-first, Terminal
# oder von Kilo abgedeckt. Das Evolution-Radar (AUTONOMIE_PLAN E4) überwacht die Kategorie.
"tools": {
"zed": {"label": "Zed (empfohlen)", "lang": "json", "snippet": zed,
"note": "Leichtgewicht-Editor (Rust, ~16x weniger RAM als VS Code). settings.json → "
"language_models.openai_compatible; Agent Panel nutzt dann die Box. Lane 'coding' routet intern."},
"kilo": {"label": "Kilo Code", "lang": "json", "snippet": kilo,
"note": "VS Code/JetBrains. OpenAI-Provider → Gateway; Lane 'coding' routet intern. "
"Empfohlen: API-Profile je Modus Code→coding · Architect/Orchestrator→heavy · Ask/Debug→chat."},
"opencode": {"label": "OpenCode", "lang": "jsonc", "snippet": opencode,
"note": "Terminal-Agent. Datei opencode.jsonc, Key 'provider'."},
"note": "VS Code/JetBrains (schwergewichtiger). OpenAI-Provider → Gateway. "
"API-Profile je Modus: Code→coding · Architect/Orchestrator→heavy · Ask/Debug→chat."},
"claude_code": {"label": "Claude Code", "lang": "bash", "snippet": claude_code,
"note": "Für die starken Sessions. Lokal-Betrieb bräuchte einen Anthropic⇄OpenAI-Übersetzer vor dem Gateway; Gedächtnis-Anbindung via Memory-MCP unten."},
},
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env bash
# Wöchentlicher Auto-Update-Lauf der Box (Autonomie E2). Läuft als User via
# mc2-autoupdate.timer (So 04:30, nach dem 03:30-Backup) oder manuell.
#
# Prinzip „Haushaltsgerät": Fremd-Software (Router → Engine → Hermes) updatet sich
# selbst über die BESTEHENDEN Pfade (update-swap.sh / update-engine.sh / hermes-update-Job).
# Jede Ebene: Update-Check → Update → Postcheck. Rot ⇒ Rollback + SELBST-PINNING im
# Pin-Register + Telegram-Meldung. Grün ⇒ Telegram „eingespielt X→Y".
# Gepinnte Ebenen werden übersprungen, bis der Pin manuell gelöst wird.
#
# OS-Ebene: bewusst NICHT hier — unattended-upgrades (nur Security) wird in der
# einmaligen sudo-Session eingerichtet (siehe deploy/sudoers-mc2-autonomie).
# Modelle: NIE automatisch (User entscheidet nach Bench, Etappe E5).
#
# Router/Engine laufen als root: braucht die NOPASSWD-Regel aus
# deploy/sudoers-mc2-autonomie. Fehlt sie, wird die Ebene übersprungen + gemeldet.
set -uo pipefail # bewusst KEIN -e: jede Ebene wird kontrolliert abgearbeitet
API="${MC_API:-http://127.0.0.1:9001}"
PINS="${MC_PINS_FILE:-/srv/models/mc2-pins.json}"
SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
HERMES_DIR="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
BACKUP_DIR="${MC_BACKUP_DIR:-/srv/models/mc2-backups}"
JOB_TIMEOUT="${MC_AUTOUPDATE_JOB_TIMEOUT:-1200}" # Sekunden für den Hermes-Job
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
say(){ echo "[autoupdate] $*"; }
notify(){ bash "$SRC_DIR/notify.sh" -s "[Box-Update]" "$1" || true; }
SUMMARY=() # eine Zeile je Ebene für die Abschluss-Meldung
# ── Pin-Register ─────────────────────────────────────────────────────────────
pins_init(){ [ -f "$PINS" ] || echo '{}' > "$PINS"; }
is_pinned(){ jq -e --arg k "$1" '.[$k].pinned == true' "$PINS" >/dev/null 2>&1; }
pin_info(){ jq -r --arg k "$1" '.[$k] | "\(.version // "?") seit \(.datum // "?") (\(.grund // "?"))"' "$PINS" 2>/dev/null; }
set_pin(){ # $1 komponente $2 version(known-good) $3 grund
local tmp; tmp="$(mktemp)"
jq --arg k "$1" --arg v "$2" --arg g "$3" --arg d "$(date +%F)" \
'.[$k] = {pinned: true, version: $v, grund: $g, datum: $d}' "$PINS" > "$tmp" && mv "$tmp" "$PINS"
}
# ── Helfer ───────────────────────────────────────────────────────────────────
details(){ curl -sf --max-time 60 "$API/api/maintenance/update-details?kind=$1"; }
# „darf ich das PASSWORTLOS?" — sudo -n -l <cmd> reicht nicht (Exit 0 auch wenn nur
# MIT Passwort erlaubt); deshalb explizit die NOPASSWD-Zeilen der Whitelist greppen.
sudo_ok(){ sudo -n -l 2>/dev/null | grep -F "NOPASSWD" | grep -qF "$1"; }
# Router und Engine teilen denselben Ablauf: root-Skript mit eingebautem
# Postcheck+Rollback (Exit 0 grün / 1 zurückgerollt / 2 kaputt).
run_root_update(){ # $1 komponente(swap|engine) $2 skript $3 anzeigename $4 installed $5 latest
local comp="$1" script="$2" name="$3" installed="$4" latest="$5" rc
say "$name: Update $installed$latest wird eingespielt…"
sudo -n /usr/bin/bash "$SRC_DIR/$script"; rc=$?
case "$rc" in
0) SUMMARY+=("$name: $installed$latest eingespielt, Stack-Check grün.")
notify "$name aktualisiert: $installed$latest. Stack-Check grün, alles läuft." ;;
1) set_pin "$comp" "$installed" "Update auf $latest zerschoss den Stack-Check; Rollback grün"
SUMMARY+=("$name: $latest FEHLGESCHLAGEN → zurückgerollt auf $installed + GEPINNT.")
notify "$name-Update auf $latest fehlgeschlagen (Stack-Check rot). Automatisch zurückgerollt auf $installed — läuft wieder. Ebene ist jetzt GEPINNT, künftige Läufe überspringen sie." ;;
*) SUMMARY+=("$name: KRITISCH — Update UND Rollback fehlgeschlagen!")
notify "KRITISCH: $name-Update auf $latest UND Rollback fehlgeschlagen — Stack möglicherweise kaputt. Bitte melden, ich brauche Hilfe (Backup liegt bereit, restore.sh existiert)." ;;
esac
}
check_layer_root(){ # $1 komponente $2 skript $3 anzeigename $4 details-kind
local comp="$1" script="$2" name="$3" kind="$4" det installed latest
if is_pinned "$comp"; then
say "$name: GEPINNT ($(pin_info "$comp")) — übersprungen."
SUMMARY+=("$name: gepinnt, übersprungen.")
return
fi
det="$(details "$kind")" || { say "$name: Update-Check nicht erreichbar."; SUMMARY+=("$name: Check fehlgeschlagen."); return; }
installed="$(jq -r '.installed_build // empty' <<<"$det")"
latest="$(jq -r '.latest_build // empty' <<<"$det")"
if [ -z "$installed" ] || [ -z "$latest" ]; then
say "$name: Versionen nicht ermittelbar (installed='$installed' latest='$latest')."
SUMMARY+=("$name: Versions-Check unklar, nichts getan.")
return
fi
if [ "$latest" -le "$installed" ] 2>/dev/null; then
say "$name: aktuell ($installed)."
SUMMARY+=("$name: aktuell ($installed).")
return
fi
if ! sudo_ok "$script"; then
say "$name: Update $installed$latest verfügbar, aber sudo-Freischaltung fehlt."
SUMMARY+=("$name: Update $installed$latest wartet — sudo-Freischaltung fehlt (einmalig deploy/sudoers-mc2-autonomie installieren).")
return
fi
run_root_update "$comp" "$script" "$name" "$installed" "$latest"
}
# ── Hermes-Agent (User-Space, via MC2-Job + Polling; Rollback machen WIR) ────
check_hermes(){
local name="Hermes-Agent" det behind old_head resp job_id state t
if is_pinned hermes; then
say "$name: GEPINNT ($(pin_info hermes)) — übersprungen."
SUMMARY+=("$name: gepinnt, übersprungen.")
return
fi
det="$(details hermes)" || { SUMMARY+=("$name: Check fehlgeschlagen."); return; }
behind="$(jq -r '.behind // 0' <<<"$det")"
if [ "${behind:-0}" -eq 0 ] 2>/dev/null; then
say "$name: aktuell."
SUMMARY+=("$name: aktuell.")
return
fi
old_head="$(git -C "$HERMES_DIR" rev-parse HEAD 2>/dev/null)"
say "$name: $behind Commits hinterher — Update-Job startet (Backup→update→doctor→Restart→Gehirn-Check)…"
resp="$(curl -sf --max-time 30 -X POST "$API/api/maintenance/hermes-update")" || { SUMMARY+=("$name: Job-Start fehlgeschlagen."); return; }
job_id="$(jq -r '.job_id // empty' <<<"$resp")"
if [ -z "$job_id" ]; then
say "$name: kein Job gestartet: $resp"
SUMMARY+=("$name: Job-Start abgelehnt ($(jq -r '.error // .detail // "unbekannt"' <<<"$resp" 2>/dev/null)).")
return
fi
t=0
while [ "$t" -lt "$JOB_TIMEOUT" ]; do
state="$(curl -sf --max-time 15 "$API/api/jobs" | jq -r --arg id "$job_id" '.jobs[] | select(.id==$id) | .state' 2>/dev/null)"
case "$state" in done|failed|canceled) break ;; esac
sleep 10; t=$((t + 10))
done
if [ "$state" = "done" ]; then
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
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"
if [ -n "$old_head" ]; then
git -C "$HERMES_DIR" reset --hard "$old_head" >/dev/null 2>&1
local bak stage
bak="$(ls -1t "$BACKUP_DIR"/mc2-state-*.tar.gz 2>/dev/null | head -1)"
if [ -n "$bak" ]; then
stage="$(mktemp -d)"
tar -xzf "$bak" -C "$stage" ./hermes 2>/dev/null || tar -xzf "$bak" -C "$stage" hermes 2>/dev/null
[ -f "$stage/hermes/config.yaml" ] && cp -a "$stage/hermes/config.yaml" "$HOME/.hermes/config.yaml"
[ -f "$stage/hermes/.env" ] && cp -a "$stage/hermes/.env" "$HOME/.hermes/.env"
rm -rf "$stage"
fi
systemctl --user restart hermes-gateway; sleep 6
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."
return
fi
fi
SUMMARY+=("$name: KRITISCH — Update UND Rollback fehlgeschlagen!")
notify "KRITISCH: Hermes-Update UND Rollback fehlgeschlagen — Gehirn-Check bleibt rot. Bitte melden. (Voll-Restore: deploy/restore.sh mit dem letzten Backup.)"
}
# ── Lauf ─────────────────────────────────────────────────────────────────────
say "Auto-Update-Lauf startet ($(date '+%F %H:%M'))."
pins_init
if ! curl -sf --max-time 20 "$API/api/health" >/dev/null; then
notify "Auto-Update abgebrochen: MC2-API ($API) nicht erreichbar — bitte Box prüfen."
exit 1
fi
check_layer_root swap update-swap.sh "Router (llama-swap)" swap
check_layer_root engine update-engine.sh "Engine (llama.cpp)" engine
check_hermes
notify "Wöchentlicher Update-Lauf fertig:
$(printf '• %s\n' "${SUMMARY[@]}")"
say "Fertig."
+4
View File
@@ -59,12 +59,16 @@ cp "$SRC/deploy/hermes-terminal.service" "$HOME/.config/systemd/user/hermes-term
# Tägliches Zustands-Backup (mem0 + Configs/Secrets) — Timer + oneshot-Service. Siehe docs/BACKUP.md.
cp "$SRC/deploy/mc2-backup.service" "$HOME/.config/systemd/user/mc2-backup.service"
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"
systemctl --user daemon-reload
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
systemctl --user enable hermes-terminal >/dev/null 2>&1 || true
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
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
+8
View File
@@ -0,0 +1,8 @@
[Unit]
Description=MC2 Auto-Update (Router/Engine/Hermes mit Rollback+Pin, Telegram-Meldung)
Documentation=file:%h/mission-control-v2/docs/AUTONOMIE_PLAN.md
After=mc2-backup.service
[Service]
Type=oneshot
ExecStart=/bin/bash %h/mission-control-v2/deploy/autoupdate.sh
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Wöchentliches MC2 Auto-Update (So 04:30, nach dem 03:30-Backup)
[Timer]
OnCalendar=Sun *-*-* 04:30:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
+13
View File
@@ -0,0 +1,13 @@
# MC2-Autonomie: erlaubt dem Auto-Update-Timer (User hitonabi) die beiden
# root-Update-Skripte OHNE Passwort. Einmalig als root installieren:
#
# sudo install -m 0440 -o root -g root ~/mission-control-v2/deploy/sudoers-mc2-autonomie /etc/sudoers.d/mc2-autonomie
# sudo visudo -c
#
# In derselben einmaligen sudo-Session sinnvoll (siehe AUTONOMIE_PLAN E2 + Faden C14):
# sudo apt-get install -y unattended-upgrades && sudo dpkg-reconfigure -plow unattended-upgrades
# sudo systemctl disable --now mission-control.service # stale v1-Unit
#
# Hinweis: die Skripte liegen im User-Checkout (User-schreibbar) — bewusste
# Entscheidung für die Single-User-Appliance; die Regel gilt NUR für exakt diese Pfade.
hitonabi ALL=(root) NOPASSWD: /usr/bin/bash /home/hitonabi/mission-control-v2/deploy/update-engine.sh, /usr/bin/bash /home/hitonabi/mission-control-v2/deploy/update-swap.sh
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-DyBEWJ2j.js"></script>
<script type="module" crossorigin src="/assets/index-D08iKoV-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-_Rap01H_.css">
</head>
<body>
+2 -2
View File
@@ -9,8 +9,8 @@ import { cn } from "@/lib/utils"
// Dateiziel je Tool — nur Hinweis im Editor-Kopf, keine Logik.
const FILE_NAMES: Record<string, string> = {
zed: "Zed: settings.json",
kilo: "Kilo: Einstellungen → API Provider",
opencode: "opencode.jsonc",
claude_code: "env / Übersetzer",
}
@@ -75,7 +75,7 @@ function CodeWindow({
export function ConnectView() {
const [host, setHost] = useState(localStorage.getItem("mc_host") || "192.168.178.151")
const [mcpPath, setMcpPath] = useState(localStorage.getItem("mc_mcp_path") || "")
const [active, setActive] = useState("kilo")
const [active, setActive] = useState("zed")
const [copied, setCopied] = useState<"model" | "memory" | null>(null)
const params = new URLSearchParams({ host })