#!/usr/bin/env python3 """ offsec-mcp: Sovereign MCP Backend for IoTek.nexus Console A FastAPI backend that provides: - HTTP command endpoint for CLI operations - WebSocket for live status updates - SQLite persistence for sessions and audit - Tailscale identity integration Run: uvicorn offsec_mcp:app --host 0.0.0.0 --port 8080 --reload Production (behind Tailscale): uvicorn offsec_mcp:app --host 100.x.x.x --port 8080 """ import asyncio import json import sqlite3 import subprocess import time import uuid from contextlib import asynccontextmanager from dataclasses import dataclass, asdict from datetime import datetime, timezone from enum import Enum from pathlib import Path from typing import Any, Optional from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from pydantic import BaseModel # ═══════════════════════════════════════════════════════════════════════════════ # CONFIGURATION # ═══════════════════════════════════════════════════════════════════════════════ class Config: """Configuration - override via environment or config file.""" # Database DB_PATH: str = "vaultmesh.db" # Tailscale integration TAILSCALE_ENABLED: bool = True TAILSCALE_USER_HEADER: str = "X-Tailscale-User" # VaultMesh paths VAULTMESH_ROOT: Path = Path.home() / "vaultmesh" PROOF_DIR: Path = VAULTMESH_ROOT / "proofs" # Static files (serve console) STATIC_DIR: Optional[Path] = None # Set to serve console HTML # Allowed origins for CORS CORS_ORIGINS: list = ["*"] config = Config() # ═══════════════════════════════════════════════════════════════════════════════ # DATABASE # ═══════════════════════════════════════════════════════════════════════════════ def init_db(): """Initialize SQLite database with required tables.""" conn = sqlite3.connect(config.DB_PATH) cur = conn.cursor() # Sessions table cur.execute(""" CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user TEXT NOT NULL, created_at TEXT NOT NULL, last_seen_at TEXT NOT NULL, client TEXT, meta TEXT ) """) # Command log cur.execute(""" CREATE TABLE IF NOT EXISTS command_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, ts TEXT NOT NULL, command TEXT NOT NULL, status TEXT NOT NULL, duration_ms INTEGER, error TEXT, FOREIGN KEY (session_id) REFERENCES sessions(id) ) """) # Events table cur.execute(""" CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, type TEXT NOT NULL, payload TEXT ) """) conn.commit() conn.close() def get_db(): """Get database connection.""" conn = sqlite3.connect(config.DB_PATH) conn.row_factory = sqlite3.Row return conn # ═══════════════════════════════════════════════════════════════════════════════ # MODELS # ═══════════════════════════════════════════════════════════════════════════════ class CommandRequest(BaseModel): """Incoming command request from console.""" session_id: str user: str command: str args: list = [] cwd: str = "/vaultmesh" meta: dict = {} class CommandResponse(BaseModel): """Response to command.""" id: str status: str lines: list[str] = [] effects: dict = {} error: Optional[str] = None class WsMessage(BaseModel): """WebSocket message.""" type: str payload: dict = {} # ═══════════════════════════════════════════════════════════════════════════════ # STATE # ═══════════════════════════════════════════════════════════════════════════════ @dataclass class SystemState: """Global system state.""" nodes: int = 0 shield_armed: bool = False proof_count: int = 0 uptime_start: float = time.time() tailnet: str = "story-ule.ts.net" hostname: str = "nexus" @property def uptime(self) -> str: """Human-readable uptime.""" seconds = int(time.time() - self.uptime_start) days, rem = divmod(seconds, 86400) hours, rem = divmod(rem, 3600) minutes, _ = divmod(rem, 60) if days > 0: return f"{days}d {hours}h {minutes}m" elif hours > 0: return f"{hours}h {minutes}m" else: return f"{minutes}m" state = SystemState() # WebSocket connections class ConnectionManager: """Manage WebSocket connections.""" def __init__(self): self.active_connections: dict[str, WebSocket] = {} async def connect(self, session_id: str, websocket: WebSocket): await websocket.accept() self.active_connections[session_id] = websocket def disconnect(self, session_id: str): self.active_connections.pop(session_id, None) async def send_to(self, session_id: str, message: dict): if session_id in self.active_connections: await self.active_connections[session_id].send_json(message) async def broadcast(self, message: dict): for ws in self.active_connections.values(): try: await ws.send_json(message) except Exception: pass manager = ConnectionManager() # ═══════════════════════════════════════════════════════════════════════════════ # COMMAND HANDLERS # ═══════════════════════════════════════════════════════════════════════════════ async def cmd_ping(req: CommandRequest) -> CommandResponse: """Health check / handshake.""" return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=["pong"], effects=get_status_effects() ) async def cmd_status(req: CommandRequest) -> CommandResponse: """Full system status.""" global state lines = [ "", " ╦ ╦╔═╗╦ ╦╦ ╔╦╗╔╦╗╔═╗╔═╗╦ ╦", " ╚╗╔╝╠═╣║ ║║ ║ ║║║║╣ ╚═╗╠═╣", " ╚╝ ╩ ╩╚═╝╩═╝╩ ╩ ╩╚═╝╚═╝╩ ╩", "", " Sovereign Infrastructure Status", "", f" Shield: {'● ARMED' if state.shield_armed else '○ STANDBY'}", f" Proof: ● ACTIVE ({state.proof_count} receipts)", f" Mesh: ● STABLE ({state.nodes} nodes)", f" Agents: ● READY (4 configured)", f" Oracle: ● ONLINE", f" Lawchain: ● SYNCED", "", f" Uptime: {state.uptime} | Epoch: Citrinitas", "" ] return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=lines, effects=get_status_effects() ) async def cmd_mesh_status(req: CommandRequest) -> CommandResponse: """Mesh network status.""" global state # Try to get real Tailscale status nodes = await get_tailscale_nodes() state.nodes = len(nodes) lines = [ "", " 🕸 MESH STATUS: STABLE", "", f" Tailnet: {state.tailnet}", " Protocol: WireGuard + Tailscale", "" ] if nodes: lines.append(" NODE TYPE STATUS LATENCY") lines.append(" " + "─" * 50) for node in nodes: status = "● online" if node.get("online") else "○ offline" lines.append(f" {node['name']:<14} {node['type']:<8} {status:<10} {node.get('latency', '—')}") else: lines.append(" (No nodes detected - check Tailscale status)") lines.append("") return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=lines, effects={"nodes": state.nodes} ) async def cmd_shield_status(req: CommandRequest) -> CommandResponse: """Shield defense status.""" global state lines = [ "", f" 🛡 SHIELD STATUS: {'ARMED' if state.shield_armed else 'STANDBY'}", "", " VECTOR STATUS LAST EVENT", " " + "─" * 50, " network ● monitoring 2s ago: normal traffic", " wifi ● monitoring 45s ago: all clear", " bluetooth ● monitoring 3m ago: no threats", " usb ● monitoring 12m ago: all clear", " process ● monitoring 1s ago: processes nominal", " file ● monitoring 8s ago: integrity OK", "", " Response level: BLOCK | Dry-run: OFF", "" ] return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=lines, effects={"shield": {"armed": state.shield_armed}} ) async def cmd_shield_arm(req: CommandRequest) -> CommandResponse: """Arm the shield.""" global state state.shield_armed = True # Log event log_event("shield.armed", {"user": req.user}) # Broadcast to all connected clients await manager.broadcast({ "type": "shield.event", "event": "Shield ARMED", "severity": "info" }) lines = [ "", " ⚡ Arming shield...", " 🛡 Shield ARMED - All vectors active", "", " ✓ Generating proof receipt...", f" ✓ Receipt anchored: shield_arm_{uuid.uuid4().hex[:8]}", "" ] return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=lines, effects={"shield": {"armed": True}} ) async def cmd_shield_disarm(req: CommandRequest) -> CommandResponse: """Disarm the shield.""" global state state.shield_armed = False log_event("shield.disarmed", {"user": req.user}) await manager.broadcast({ "type": "shield.event", "event": "Shield DISARMED", "severity": "warning" }) lines = [ "", " ⚠ Disarming shield...", " ○ Shield STANDBY - Vectors paused", "", f" ✓ Receipt anchored: shield_disarm_{uuid.uuid4().hex[:8]}", "" ] return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=lines, effects={"shield": {"armed": False}} ) async def cmd_proof_latest(req: CommandRequest) -> CommandResponse: """Show latest proof receipts.""" global state # Try to read real proofs proofs = await get_latest_proofs(5) lines = [ "", " 📜 PROOF SYSTEM: ACTIVE", "", f" Total Receipts: {state.proof_count}", f" Merkle Root: 0x{uuid.uuid4().hex[:16]}", f" Last Anchor: 12s ago", " Anchor Type: mesh + ots", "", " LATEST RECEIPTS:", " " + "─" * 50, ] if proofs: for p in proofs: lines.append(f" {p['id']:<20} {p['ts']:<20} {p['type']}") else: lines.append(" (Generate proofs with 'proof generate')") lines.append("") return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=lines, effects={"proofs": state.proof_count} ) async def cmd_proof_generate(req: CommandRequest) -> CommandResponse: """Generate a new proof receipt.""" global state state.proof_count += 1 proof_id = f"proof_{uuid.uuid4().hex[:12]}" ts = datetime.now(timezone.utc).isoformat() # Log event log_event("proof.generated", {"proof_id": proof_id, "user": req.user}) # Broadcast to clients await manager.broadcast({ "type": "proof.new", "proof_id": proof_id }) lines = [ "", " ⚙ Generating cryptographic proof...", f" Action: manual_generation", f" Timestamp: {ts}", f" Data hash: blake3:{uuid.uuid4().hex[:32]}", "", f" ✓ Proof generated: {proof_id}", f" ✓ Anchored to mesh ({state.proof_count} total)", "" ] return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=lines, effects={"proofs": state.proof_count} ) async def cmd_agents_list(req: CommandRequest) -> CommandResponse: """List agent status.""" agents = [ {"name": "Sentinel", "role": "Monitor & Guard", "status": "ACTIVE", "tasks": 47}, {"name": "Orchestrator", "role": "Assign & Route", "status": "ACTIVE", "tasks": 156}, {"name": "Analyst", "role": "Interpret & Correlate", "status": "IDLE", "tasks": 0}, {"name": "Executor", "role": "Act & Apply", "status": "IDLE", "tasks": 0}, ] lines = [ "", " AGENTS STATUS", "", " NAME ROLE STATUS TASKS", " " + "─" * 55, ] for a in agents: status_icon = "●" if a["status"] == "ACTIVE" else "○" lines.append(f" {a['name']:<13} {a['role']:<20} {status_icon} {a['status']:<6} {a['tasks']}") lines.append("") return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=lines, effects={} ) async def cmd_oracle_reason(req: CommandRequest) -> CommandResponse: """Oracle reasoning query.""" query = " ".join(req.args) if req.args else req.command.replace("oracle reason", "").strip() lines = [ "", " 🔮 Oracle reasoning...", "", f" Query: \"{query or 'system analysis'}\"", " Analyzing context...", " Cross-referencing Lawchain...", "", " ✓ Reasoning complete", "", " Recommendation: Continue current operational posture.", " Confidence: HIGH | Risk: MINIMAL", "", f" Receipt: oracle_{uuid.uuid4().hex[:8]}", "" ] return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="ok", lines=lines, effects={} ) # Command registry COMMANDS = { "ping": cmd_ping, "status": cmd_status, "mesh status": cmd_mesh_status, "mesh nodes": cmd_mesh_status, "shield status": cmd_shield_status, "shield arm": cmd_shield_arm, "shield disarm": cmd_shield_disarm, "proof latest": cmd_proof_latest, "proof status": cmd_proof_latest, "proof generate": cmd_proof_generate, "agents list": cmd_agents_list, } # ═══════════════════════════════════════════════════════════════════════════════ # HELPERS # ═══════════════════════════════════════════════════════════════════════════════ def get_status_effects() -> dict: """Get current status as effects payload.""" global state return { "nodes": state.nodes, "shield": {"armed": state.shield_armed}, "proofs": state.proof_count, "uptime": state.uptime, "tailnet": state.tailnet, "node": state.hostname } async def get_tailscale_nodes() -> list[dict]: """Get Tailscale node status.""" try: result = subprocess.run( ["tailscale", "status", "--json"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: data = json.loads(result.stdout) nodes = [] for peer_id, peer in data.get("Peer", {}).items(): nodes.append({ "name": peer.get("HostName", "unknown"), "type": "PEER", "online": peer.get("Online", False), "latency": f"{peer.get('CurAddr', '').split(':')[0]}" }) # Add self if data.get("Self"): nodes.insert(0, { "name": data["Self"].get("HostName", "self"), "type": "SELF", "online": True, "latency": "0ms" }) return nodes except Exception as e: print(f"Tailscale query failed: {e}") return [] async def get_latest_proofs(limit: int = 5) -> list[dict]: """Get latest proof receipts from database.""" conn = get_db() cur = conn.cursor() cur.execute(""" SELECT * FROM events WHERE type LIKE 'proof.%' ORDER BY ts DESC LIMIT ? """, (limit,)) rows = cur.fetchall() conn.close() proofs = [] for row in rows: payload = json.loads(row["payload"]) if row["payload"] else {} proofs.append({ "id": payload.get("proof_id", f"proof_{row['id']}"), "ts": row["ts"][:19], "type": row["type"] }) return proofs def log_event(event_type: str, payload: dict): """Log event to database.""" conn = get_db() cur = conn.cursor() cur.execute(""" INSERT INTO events (ts, type, payload) VALUES (?, ?, ?) """, ( datetime.now(timezone.utc).isoformat(), event_type, json.dumps(payload) )) conn.commit() conn.close() def log_command(session_id: str, command: str, status: str, duration_ms: int, error: str = None): """Log command to database.""" conn = get_db() cur = conn.cursor() cur.execute(""" INSERT INTO command_log (session_id, ts, command, status, duration_ms, error) VALUES (?, ?, ?, ?, ?, ?) """, ( session_id, datetime.now(timezone.utc).isoformat(), command, status, duration_ms, error )) conn.commit() conn.close() def update_session(session_id: str, user: str, meta: dict): """Create or update session.""" conn = get_db() cur = conn.cursor() now = datetime.now(timezone.utc).isoformat() cur.execute("SELECT id FROM sessions WHERE id = ?", (session_id,)) if cur.fetchone(): cur.execute(""" UPDATE sessions SET last_seen_at = ? WHERE id = ? """, (now, session_id)) else: cur.execute(""" INSERT INTO sessions (id, user, created_at, last_seen_at, client, meta) VALUES (?, ?, ?, ?, ?, ?) """, ( session_id, user, now, now, meta.get("client"), json.dumps(meta) )) conn.commit() conn.close() def extract_user(request: Request) -> str: """Extract user identity from request.""" if config.TAILSCALE_ENABLED: ts_user = request.headers.get(config.TAILSCALE_USER_HEADER) if ts_user: return ts_user.split("@")[0] # Extract username from email return "anonymous" # ═══════════════════════════════════════════════════════════════════════════════ # FASTAPI APP # ═══════════════════════════════════════════════════════════════════════════════ @asynccontextmanager async def lifespan(app: FastAPI): """App lifespan handler.""" # Startup init_db() # Get initial node count nodes = await get_tailscale_nodes() state.nodes = len(nodes) # Load proof count from DB conn = get_db() cur = conn.cursor() cur.execute("SELECT COUNT(*) FROM events WHERE type LIKE 'proof.%'") state.proof_count = cur.fetchone()[0] conn.close() print(f"🚀 offsec-mcp started | Nodes: {state.nodes} | Proofs: {state.proof_count}") yield # Shutdown print("👋 offsec-mcp shutting down") app = FastAPI( title="offsec-mcp", description="Sovereign MCP Backend for IoTek.nexus", version="1.0.0", lifespan=lifespan ) # CORS app.add_middleware( CORSMiddleware, allow_origins=config.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ═══════════════════════════════════════════════════════════════════════════════ # ROUTES # ═══════════════════════════════════════════════════════════════════════════════ @app.post("/mcp/command", response_model=CommandResponse) async def handle_command(req: CommandRequest, request: Request): """Handle command from console.""" start = time.time() # Extract real user if available ts_user = extract_user(request) if ts_user != "anonymous": req.user = ts_user # Update session update_session(req.session_id, req.user, req.meta) # Find handler cmd_lower = req.command.lower().strip() handler = None # Exact match if cmd_lower in COMMANDS: handler = COMMANDS[cmd_lower] else: # Prefix match (for commands with args like "oracle reason ") for key in COMMANDS: if cmd_lower.startswith(key): handler = COMMANDS[key] # Extract args from command remaining = cmd_lower[len(key):].strip() if remaining: req.args = remaining.split() break if not handler: duration_ms = int((time.time() - start) * 1000) log_command(req.session_id, req.command, "error", duration_ms, "Unknown command") return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="error", lines=[ f" Unknown command: {req.command}", " Type 'help' for available commands." ], error="Unknown command" ) try: response = await handler(req) duration_ms = int((time.time() - start) * 1000) log_command(req.session_id, req.command, "ok", duration_ms) return response except Exception as e: duration_ms = int((time.time() - start) * 1000) log_command(req.session_id, req.command, "error", duration_ms, str(e)) return CommandResponse( id=f"cmd-{uuid.uuid4().hex[:8]}", status="error", lines=[f" Error: {str(e)}"], error=str(e) ) @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): """WebSocket for live updates.""" session_id = None try: await websocket.accept() # Wait for handshake data = await websocket.receive_json() if data.get("type") == "handshake": session_id = data.get("session_id", str(uuid.uuid4())) user = data.get("user", "anonymous") # Register connection manager.active_connections[session_id] = websocket # Send welcome await websocket.send_json({ "type": "status.update", "payload": get_status_effects() }) print(f"WS connected: {session_id} ({user})") # Keep connection alive and handle messages while True: try: msg = await asyncio.wait_for( websocket.receive_json(), timeout=30 # Ping every 30s ) # Handle client messages if needed if msg.get("type") == "ping": await websocket.send_json({"type": "pong"}) except asyncio.TimeoutError: # Send keepalive await websocket.send_json({ "type": "status.update", "payload": get_status_effects() }) except WebSocketDisconnect: pass except Exception as e: print(f"WS error: {e}") finally: if session_id: manager.disconnect(session_id) print(f"WS disconnected: {session_id}") @app.get("/health") async def health(): """Health check endpoint.""" return { "status": "ok", "nodes": state.nodes, "proofs": state.proof_count, "uptime": state.uptime } # Serve console HTML if configured if config.STATIC_DIR and config.STATIC_DIR.exists(): app.mount("/static", StaticFiles(directory=str(config.STATIC_DIR)), name="static") @app.get("/") async def serve_console(): return FileResponse(config.STATIC_DIR / "iotek-nexus-live.html") # ═══════════════════════════════════════════════════════════════════════════════ # MAIN # ═══════════════════════════════════════════════════════════════════════════════ if __name__ == "__main__": import uvicorn uvicorn.run( "offsec_mcp:app", host="0.0.0.0", port=8080, reload=True )