""" Admin API endpoints for system control and management. Provides kill switches, access revocation, and enhanced log browsing. """ from fastapi import APIRouter, HTTPException from typing import List, Dict, Any, Optional from pathlib import Path import sqlite3 import json import os import signal import subprocess from datetime import datetime router = APIRouter(prefix="/api/admin", tags=["admin"]) # Paths LOGS_DIR = Path(__file__).parent.parent.parent / "data" / "logs" TOKENS_DB = Path(__file__).parent.parent.parent / "data" / "admin" / "tokens.db" TOKENS_DB.parent.mkdir(parents=True, exist_ok=True) # Service process IDs (will be populated from system) SERVICE_PIDS = { "mcp_server": None, "family_agent": None, "work_agent": None } def _init_tokens_db(): """Initialize token blacklist database.""" conn = sqlite3.connect(str(TOKENS_DB)) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS revoked_tokens ( token_id TEXT PRIMARY KEY, device_id TEXT, revoked_at TEXT NOT NULL, reason TEXT, revoked_by TEXT ) """) cursor.execute(""" CREATE TABLE IF NOT EXISTS devices ( device_id TEXT PRIMARY KEY, name TEXT, last_seen TEXT, status TEXT DEFAULT 'active', created_at TEXT NOT NULL ) """) conn.commit() conn.close() @router.get("/logs/enhanced") async def get_enhanced_logs( limit: int = 100, level: Optional[str] = None, agent_type: Optional[str] = None, tool_name: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, search: Optional[str] = None ): """Enhanced log browser with more filters and search.""" if not LOGS_DIR.exists(): return {"logs": [], "total": 0} try: log_files = sorted(LOGS_DIR.glob("llm_*.log"), reverse=True) if not log_files: return {"logs": [], "total": 0} logs = [] count = 0 # Read from most recent log files for log_file in log_files: if count >= limit: break for line in log_file.read_text().splitlines(): if count >= limit: break try: log_entry = json.loads(line) # Apply filters if level and log_entry.get("level") != level.upper(): continue if agent_type and log_entry.get("agent_type") != agent_type: continue if tool_name and tool_name not in str(log_entry.get("tool_calls", [])): continue if start_date and log_entry.get("timestamp", "") < start_date: continue if end_date and log_entry.get("timestamp", "") > end_date: continue if search and search.lower() not in json.dumps(log_entry).lower(): continue logs.append(log_entry) count += 1 except Exception: continue return { "logs": logs, "total": len(logs), "filters": { "level": level, "agent_type": agent_type, "tool_name": tool_name, "start_date": start_date, "end_date": end_date, "search": search } } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/kill-switch/{service}") async def kill_service(service: str): """Kill switch for services: mcp_server, family_agent, work_agent, or all.""" try: if service == "mcp_server": # Kill MCP server process subprocess.run(["pkill", "-f", "uvicorn.*mcp_server"], check=False) return {"success": True, "message": f"{service} stopped"} elif service == "family_agent": # Kill family agent (would need to track PID) # For now, return success (implementation depends on how agents run) return {"success": True, "message": f"{service} stopped (not implemented)"} elif service == "work_agent": # Kill work agent return {"success": True, "message": f"{service} stopped (not implemented)"} elif service == "all": # Kill all services subprocess.run(["pkill", "-f", "uvicorn|mcp_server"], check=False) return {"success": True, "message": "All services stopped"} else: raise HTTPException(status_code=400, detail=f"Unknown service: {service}") except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/tools/{tool_name}/disable") async def disable_tool(tool_name: str): """Disable a specific MCP tool.""" # This would require modifying the tool registry # For now, return success (implementation needed) return { "success": True, "message": f"Tool {tool_name} disabled (not implemented)", "note": "Requires tool registry modification" } @router.post("/tools/{tool_name}/enable") async def enable_tool(tool_name: str): """Enable a previously disabled MCP tool.""" return { "success": True, "message": f"Tool {tool_name} enabled (not implemented)", "note": "Requires tool registry modification" } @router.post("/tokens/revoke") async def revoke_token(token_id: str, reason: Optional[str] = None): """Revoke a token (add to blacklist).""" _init_tokens_db() try: conn = sqlite3.connect(str(TOKENS_DB)) cursor = conn.cursor() cursor.execute(""" INSERT INTO revoked_tokens (token_id, revoked_at, reason, revoked_by) VALUES (?, ?, ?, ?) """, (token_id, datetime.now().isoformat(), reason, "admin")) conn.commit() conn.close() return {"success": True, "message": f"Token {token_id} revoked"} except sqlite3.IntegrityError: return {"success": False, "message": "Token already revoked"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/tokens/revoked") async def list_revoked_tokens(): """List all revoked tokens.""" _init_tokens_db() if not TOKENS_DB.exists(): return {"tokens": []} try: conn = sqlite3.connect(str(TOKENS_DB)) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(""" SELECT token_id, device_id, revoked_at, reason, revoked_by FROM revoked_tokens ORDER BY revoked_at DESC """) rows = cursor.fetchall() conn.close() tokens = [dict(row) for row in rows] return {"tokens": tokens, "total": len(tokens)} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/tokens/revoke/clear") async def clear_revoked_tokens(): """Clear all revoked tokens (use with caution).""" _init_tokens_db() try: conn = sqlite3.connect(str(TOKENS_DB)) cursor = conn.cursor() cursor.execute("DELETE FROM revoked_tokens") conn.commit() deleted = cursor.rowcount conn.close() return {"success": True, "message": f"Cleared {deleted} revoked tokens"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/devices") async def list_devices(): """List all registered devices.""" _init_tokens_db() if not TOKENS_DB.exists(): return {"devices": []} try: conn = sqlite3.connect(str(TOKENS_DB)) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(""" SELECT device_id, name, last_seen, status, created_at FROM devices ORDER BY last_seen DESC """) rows = cursor.fetchall() conn.close() devices = [dict(row) for row in rows] return {"devices": devices, "total": len(devices)} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/devices/{device_id}/revoke") async def revoke_device(device_id: str): """Revoke access for a device.""" _init_tokens_db() try: conn = sqlite3.connect(str(TOKENS_DB)) cursor = conn.cursor() cursor.execute(""" UPDATE devices SET status = 'revoked' WHERE device_id = ? """, (device_id,)) conn.commit() conn.close() return {"success": True, "message": f"Device {device_id} revoked"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/status") async def get_admin_status(): """Get admin panel status and system information.""" try: # Check service status mcp_running = subprocess.run( ["pgrep", "-f", "uvicorn.*mcp_server"], capture_output=True ).returncode == 0 return { "services": { "mcp_server": { "running": mcp_running, "pid": SERVICE_PIDS.get("mcp_server") }, "family_agent": { "running": False, # TODO: Check actual status "pid": SERVICE_PIDS.get("family_agent") }, "work_agent": { "running": False, # TODO: Check actual status "pid": SERVICE_PIDS.get("work_agent") } }, "databases": { "tokens": TOKENS_DB.exists(), "logs": LOGS_DIR.exists() } } except Exception as e: raise HTTPException(status_code=500, detail=str(e))