Contains: - 1m-brag - tem - VaultMesh_Catalog_v1 - VAULTMESH-ETERNAL-PATTERN 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
880 lines
27 KiB
Python
880 lines
27 KiB
Python
#!/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 <query>")
|
|
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
|
|
)
|