feat(2.0): implement cockpit improvements, dynamic mcp path, v2 venv, and ko-residency

This commit is contained in:
Hitonabi
2026-06-25 21:50:16 +02:00
parent 807c2c6194
commit e1da5c797d
14 changed files with 87 additions and 50 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300"))
HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"} HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
# --- Routing-Gateway (LiteLLM, model: auto) ---------------------------------- # --- Routing-Gateway (LiteLLM, model: auto) ----------------------------------
GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", "http://127.0.0.1:4000").rstrip("/") GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/")
GATEWAY_CONFIG_PATH = Path(os.environ.get( GATEWAY_CONFIG_PATH = Path(os.environ.get(
"MC_GATEWAY_CONFIG", str(Path(__file__).resolve().parent.parent / "gateway" / "config.yaml"))) "MC_GATEWAY_CONFIG", str(Path(__file__).resolve().parent.parent / "gateway" / "config.yaml")))
+1
View File
@@ -4,3 +4,4 @@ httpx>=0.27
ruamel.yaml>=0.18 ruamel.yaml>=0.18
psutil>=5.9 psutil>=5.9
huggingface_hub>=0.27 huggingface_hub>=0.27
mcp>=1.2.0
+5 -2
View File
@@ -8,5 +8,8 @@ router = APIRouter(prefix="/api")
@router.get("/connect") @router.get("/connect")
def connect(host: str = DEFAULT_HOST) -> dict: def connect(host: str = DEFAULT_HOST, mcp_path: str | None = None) -> dict:
return build_snippets(host=host) kwargs = {}
if mcp_path:
kwargs["mcp_script_path"] = mcp_path
return build_snippets(host=host, **kwargs)
+4 -3
View File
@@ -21,7 +21,7 @@ from services.system import system_status
router = APIRouter(prefix="/api") router = APIRouter(prefix="/api")
# Nur diese User-Dienste dürfen neugestartet werden. # Nur diese User-Dienste dürfen neugestartet werden.
ALLOWED_SERVICES = {"mission-control-2", "litellm-gateway", "hermes-webui"} ALLOWED_SERVICES = {"mission-control-2", "hermes-gateway", "hermes-webui"}
# Quelle für Self-Update (auf der Box ~/mission-control-v2). # Quelle für Self-Update (auf der Box ~/mission-control-v2).
SOURCE_DIR = os.path.expanduser(os.environ.get("MC2_SOURCE_DIR", "~/mission-control-v2")) SOURCE_DIR = os.path.expanduser(os.environ.get("MC2_SOURCE_DIR", "~/mission-control-v2"))
@@ -35,16 +35,17 @@ def status() -> dict:
def services() -> dict: def services() -> dict:
"""Aggregierte Erreichbarkeit aller Stack-Dienste (für die Health-Anzeige).""" """Aggregierte Erreichbarkeit aller Stack-Dienste (für die Health-Anzeige)."""
a = agent_status() a = agent_status()
gw_url = f"{GATEWAY_URL}/v1"
return { return {
"services": [ "services": [
{"name": "Engine (llama-swap)", "url": LLAMA_SWAP_URL, "ok": engine_reachable()}, {"name": "Engine (llama-swap)", "url": LLAMA_SWAP_URL, "ok": engine_reachable()},
{"name": "Gateway (LiteLLM)", "url": GATEWAY_URL, "ok": gateway_reachable()}, {"name": "Gateway (integriert)", "url": gw_url, "ok": gateway_reachable()},
{"name": "Hermes-Gateway", "url": HERMES_API_URL, "ok": a["gateway_reachable"]}, {"name": "Hermes-Gateway", "url": HERMES_API_URL, "ok": a["gateway_reachable"]},
{"name": "Hermes-WebUI", "url": HERMES_WEBUI_URL, "ok": a["webui_reachable"]}, {"name": "Hermes-WebUI", "url": HERMES_WEBUI_URL, "ok": a["webui_reachable"]},
], ],
"links": { "links": {
"engine_ui": f"{LLAMA_SWAP_URL}/ui", "engine_ui": f"{LLAMA_SWAP_URL}/ui",
"gateway": GATEWAY_URL, "gateway": gw_url,
"hermes_webui": HERMES_WEBUI_URL, "hermes_webui": HERMES_WEBUI_URL,
}, },
} }
+13 -1
View File
@@ -6,7 +6,7 @@ Hermes' eigener Config verdrahtet (siehe docs/HERMES_SETUP.md).
import httpx import httpx
from config import HERMES_API_URL, HERMES_HOME, HERMES_WEBUI_URL from config import HERMES_API_URL, HERMES_HOME, HERMES_WEBUI_URL, yaml
def _reach(url: str, path: str = "") -> bool: def _reach(url: str, path: str = "") -> bool:
@@ -20,12 +20,24 @@ def _reach(url: str, path: str = "") -> bool:
def agent_status() -> dict: def agent_status() -> dict:
"""Erreichbarkeit von Gateway (:8642) + WebUI (:8787) + lokale Hinweise.""" """Erreichbarkeit von Gateway (:8642) + WebUI (:8787) + lokale Hinweise."""
home = HERMES_HOME home = HERMES_HOME
brain_model = "auto"
config_path = home / "config.yaml"
if config_path.exists():
try:
with config_path.open("r", encoding="utf-8") as f:
cfg = yaml.load(f) or {}
if isinstance(cfg, dict):
brain_model = cfg.get("model", {}).get("model", "auto")
except Exception:
pass
return { return {
"gateway_url": HERMES_API_URL, "gateway_url": HERMES_API_URL,
"webui_url": HERMES_WEBUI_URL, "webui_url": HERMES_WEBUI_URL,
"gateway_reachable": _reach(HERMES_API_URL, "/v1/models"), "gateway_reachable": _reach(HERMES_API_URL, "/v1/models"),
"webui_reachable": _reach(HERMES_WEBUI_URL), "webui_reachable": _reach(HERMES_WEBUI_URL),
"home_exists": home.exists(), "home_exists": home.exists(),
"brain_model": brain_model,
# Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig). # Best-effort: welche Verdrahtung lokal sichtbar ist (auf der Box aussagekräftig).
"has_config": (home / "config.yaml").exists() or (home / "config.json").exists(), "has_config": (home / "config.yaml").exists() or (home / "config.json").exists(),
"has_skills": (home / "skills").exists(), "has_skills": (home / "skills").exists(),
+1 -1
View File
@@ -18,7 +18,7 @@ from services import discover, jobengine, llamaswap
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user). # System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
SYSTEM_SERVICES = {"llama-swap"} SYSTEM_SERVICES = {"llama-swap"}
USER_SERVICES = {"mission-control-2", "hermes-gateway", "hermes-dashboard", "hermes-webui", "litellm-gateway"} USER_SERVICES = {"mission-control-2", "hermes-gateway", "hermes-dashboard", "hermes-webui"}
ENGINE_UPDATE_CMD = os.environ.get("MC_ENGINE_UPDATE_CMD", "") ENGINE_UPDATE_CMD = os.environ.get("MC_ENGINE_UPDATE_CMD", "")
ENGINE_PATH = os.environ.get("MC_ENGINE_PATH", "/opt/llamacpp") ENGINE_PATH = os.environ.get("MC_ENGINE_PATH", "/opt/llamacpp")
+3 -3
View File
@@ -69,9 +69,9 @@ delegation:
`agent.disabled_toolsets` (gilt für ALLE Plattformen). `agent.disabled_toolsets` (gilt für ALLE Plattformen).
- **Behalten:** terminal/shell, file, code_execution, skills, todo, session_search, clarify, - **Behalten:** terminal/shell, file, code_execution, skills, todo, session_search, clarify,
delegation, cronjob, web. `approvals: auto` (rein lokal). delegation, cronjob, web. `approvals: auto` (rein lokal).
- **MCP-Server** (`mcp_servers` in config) — laufen über die v1-venv (hat das `mcp`-Modul): - **MCP-Server** (`mcp_servers` in config) — laufen über die v2-venv (hat das `mcp`-Modul nach pip install):
- geteiltes Gedächtnis: `/opt/mission-control/.venv/bin/python ~/mission-control-v2/mcp/mcp_memory.py` (Env `MC_URL=http://127.0.0.1:9001`) - geteiltes Gedächtnis: `~/mission-control-v2/backend/.venv/bin/python ~/mission-control-v2/mcp/mcp_memory.py` (Env `MC_URL=http://127.0.0.1:9001`)
- Stack-Management: ` ~/mission-control-v2/mcp/mcp_mc.py` (Env `MC_URL=http://127.0.0.1:9001`) - Stack-Management: `~/mission-control-v2/backend/.venv/bin/python ~/mission-control-v2/mcp/mcp_mc.py` (Env `MC_URL=http://127.0.0.1:9001`)
- **SSH→Windows-PC** (voller Zugriff): OpenSSH-Server auf Windows aktiv + Key - **SSH→Windows-PC** (voller Zugriff): OpenSSH-Server auf Windows aktiv + Key
`~/.ssh/id_ed25519_hermes_agent` autorisiert; Hermes nutzt sein terminal-Tool für `ssh TobisPC@<win-ip>`. `~/.ssh/id_ed25519_hermes_agent` autorisiert; Hermes nutzt sein terminal-Tool für `ssh TobisPC@<win-ip>`.
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" /> <meta name="theme-color" content="#0d1117" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-S7P9XrdI.js"></script> <script type="module" crossorigin src="/assets/index-BcRd449w.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-IpcnHTkp.css"> <link rel="stylesheet" crossorigin href="/assets/index-D7pMz4id.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1
View File
@@ -143,6 +143,7 @@ export interface AgentStatus {
gateway_reachable: boolean gateway_reachable: boolean
webui_reachable: boolean webui_reachable: boolean
home_exists: boolean home_exists: boolean
brain_model?: string
has_config: boolean has_config: boolean
has_skills: boolean has_skills: boolean
has_memories: boolean has_memories: boolean
+1 -1
View File
@@ -64,7 +64,7 @@ export function AgentView() {
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> <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="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="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="Brain" ok={s.gateway_reachable} detail={`model: ${s.brain_model || "auto"}`} />
<Tile label="Verdrahtung" ok={s.has_config} detail={`config ${s.has_config ? "✓" : "—"} · skills ${s.has_skills ? "✓" : "—"} · memories ${s.has_memories ? "✓" : "—"}`} /> <Tile label="Verdrahtung" ok={s.has_config} detail={`config ${s.has_config ? "✓" : "—"} · skills ${s.has_skills ? "✓" : "—"} · memories ${s.has_memories ? "✓" : "—"}`} />
</div> </div>
+29 -10
View File
@@ -5,22 +5,31 @@ import { cn } from "@/lib/utils"
export function ConnectView() { export function ConnectView() {
const [host, setHost] = useState(localStorage.getItem("mc_host") || "192.168.178.151") const [host, setHost] = useState(localStorage.getItem("mc_host") || "192.168.178.151")
const [mcpPath, setMcpPath] = useState(localStorage.getItem("mc_mcp_path") || "")
const [data, setData] = useState<ConnectResp | null>(null) const [data, setData] = useState<ConnectResp | null>(null)
const [active, setActive] = useState("cline") const [active, setActive] = useState("cline")
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [error, setError] = useState("") const [error, setError] = useState("")
useEffect(() => { useEffect(() => {
api<ConnectResp>(`/api/connect?host=${encodeURIComponent(host)}`) const params = new URLSearchParams()
params.set("host", host)
if (mcpPath) params.set("mcp_path", mcpPath)
api<ConnectResp>(`/api/connect?${params}`)
.then(setData) .then(setData)
.catch((e) => setError(String(e))) .catch((e) => setError(String(e)))
}, [host]) }, [host, mcpPath])
function saveHost(v: string) { function saveHost(v: string) {
setHost(v) setHost(v)
if (v) localStorage.setItem("mc_host", v) if (v) localStorage.setItem("mc_host", v)
} }
function saveMcpPath(v: string) {
setMcpPath(v)
localStorage.setItem("mc_mcp_path", v)
}
const tool = data?.tools[active] const tool = data?.tools[active]
async function copy() { async function copy() {
@@ -40,14 +49,24 @@ export function ConnectView() {
</p> </p>
</div> </div>
<div className="flex items-center gap-2 text-sm"> <div className="flex flex-wrap items-center gap-4 text-sm bg-card/40 p-3 rounded-lg border border-border">
<label className="text-muted-foreground">Box-LAN-IP:</label> <div className="flex items-center gap-2">
<input <label className="text-muted-foreground">Box-LAN-IP:</label>
value={host} <input
onChange={(e) => saveHost(e.target.value)} value={host}
className="rounded-md border border-border bg-card px-2 py-1 font-mono text-xs outline-none focus:ring-2 focus:ring-ring" onChange={(e) => saveHost(e.target.value)}
/> className="w-36 rounded-md border border-border bg-background px-2 py-1 font-mono text-xs outline-none focus:ring-2 focus:ring-ring"
{data && <span className="text-xs text-muted-foreground"> {data.gateway_url}</span>} />
</div>
<div className="flex items-center gap-2 flex-1 min-w-[280px]">
<label className="text-muted-foreground whitespace-nowrap">Lokaler MCP-Pfad:</label>
<input
value={mcpPath}
onChange={(e) => saveMcpPath(e.target.value)}
placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py"
className="flex-1 rounded-md border border-border bg-background px-2 py-1 font-mono text-xs outline-none focus:ring-2 focus:ring-ring"
/>
</div>
</div> </div>
{error && ( {error && (