212 lines
6.8 KiB
Python
Executable File
212 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Cortex Public API — lightweight endpoints for external Claude sessions.
|
|
Runs on port 8112, exposed via Caddy at cortex.hydrascale.net/api/
|
|
|
|
Auth: pass API key via X-API-Key header, Authorization: Bearer, or ?key= query param.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sqlite3
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from fastapi import FastAPI, HTTPException, Header, Depends, Query, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
API_KEY = "cortex-pub-a7f3e9c1d4b8"
|
|
|
|
app = FastAPI(title="Cortex Public API", version="0.2.0")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_methods=["GET", "POST"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
def verify_key(
|
|
request: Request,
|
|
x_api_key: Optional[str] = Header(None),
|
|
authorization: Optional[str] = Header(None),
|
|
key: Optional[str] = Query(None),
|
|
):
|
|
"""Accept key via X-API-Key header, Authorization: Bearer, or ?key= query param."""
|
|
token = x_api_key or key
|
|
if not token and authorization and authorization.startswith("Bearer "):
|
|
token = authorization[7:]
|
|
if token != API_KEY:
|
|
raise HTTPException(status_code=401, detail="Invalid API key. Pass via X-API-Key header or ?key= param.")
|
|
return token
|
|
|
|
|
|
# ──────────── /health (no auth) ────────────
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok", "timestamp": datetime.utcnow().isoformat() + "Z"}
|
|
|
|
|
|
# ──────────── /info — system specs ────────────
|
|
@app.get("/info")
|
|
async def system_info(auth: str = Depends(verify_key)):
|
|
"""Return full system specs — CPU, RAM, disk, GPU, OS, etc."""
|
|
specs = {}
|
|
|
|
# OS
|
|
try:
|
|
out = subprocess.check_output(["lsb_release", "-ds"], text=True).strip()
|
|
specs["os"] = out
|
|
except Exception:
|
|
specs["os"] = "unknown"
|
|
|
|
# Kernel
|
|
specs["kernel"] = subprocess.check_output(["uname", "-r"], text=True).strip()
|
|
|
|
# Hostname
|
|
specs["hostname"] = subprocess.check_output(["hostname"], text=True).strip()
|
|
|
|
# CPU
|
|
try:
|
|
cpuinfo = Path("/proc/cpuinfo").read_text()
|
|
model_names = [l.split(":")[1].strip() for l in cpuinfo.splitlines() if "model name" in l]
|
|
specs["cpu"] = {
|
|
"model": model_names[0] if model_names else "unknown",
|
|
"cores": len(model_names),
|
|
}
|
|
except Exception:
|
|
specs["cpu"] = "unknown"
|
|
|
|
# RAM
|
|
try:
|
|
meminfo = Path("/proc/meminfo").read_text()
|
|
for line in meminfo.splitlines():
|
|
if line.startswith("MemTotal:"):
|
|
kb = int(line.split()[1])
|
|
specs["ram_gb"] = round(kb / 1024 / 1024, 1)
|
|
break
|
|
except Exception:
|
|
specs["ram_gb"] = "unknown"
|
|
|
|
# Disk
|
|
try:
|
|
df = subprocess.check_output(["df", "-h", "/"], text=True).strip().split("\n")[1].split()
|
|
specs["disk"] = {"total": df[1], "used": df[2], "available": df[3], "use_pct": df[4]}
|
|
except Exception:
|
|
specs["disk"] = "unknown"
|
|
|
|
# GPU
|
|
try:
|
|
gpu = subprocess.check_output(["lspci"], text=True)
|
|
gpu_lines = [l for l in gpu.splitlines() if "VGA" in l or "3D" in l or "Display" in l]
|
|
specs["gpu"] = gpu_lines if gpu_lines else "none detected"
|
|
except Exception:
|
|
specs["gpu"] = "lspci not available"
|
|
|
|
# Uptime
|
|
try:
|
|
uptime = Path("/proc/uptime").read_text().split()[0]
|
|
specs["uptime_hours"] = round(float(uptime) / 3600, 1)
|
|
except Exception:
|
|
specs["uptime_hours"] = "unknown"
|
|
|
|
# Docker containers
|
|
try:
|
|
containers = subprocess.check_output(
|
|
["docker", "ps", "--format", "{{.Names}}: {{.Status}}"], text=True
|
|
).strip()
|
|
specs["docker_containers"] = containers.splitlines() if containers else []
|
|
except Exception:
|
|
specs["docker_containers"] = "docker not available"
|
|
|
|
# Services
|
|
services = {}
|
|
for svc in ["caddy", "fail2ban", "docker", "symbiont-api", "cortex-public-api"]:
|
|
try:
|
|
status = subprocess.check_output(
|
|
["systemctl", "is-active", svc], text=True
|
|
).strip()
|
|
services[svc] = status
|
|
except Exception:
|
|
services[svc] = "unknown"
|
|
specs["services"] = services
|
|
|
|
specs["timestamp"] = datetime.utcnow().isoformat() + "Z"
|
|
return specs
|
|
|
|
|
|
# ──────────── /engram/sitrep — session awareness ────────────
|
|
@app.get("/engram/sitrep")
|
|
async def engram_sitrep(auth: str = Depends(verify_key)):
|
|
"""Return situation report from engram DB."""
|
|
db_path = Path("/data/symbiont/engram.db")
|
|
if not db_path.exists():
|
|
return {"error": "engram.db not found", "sessions": []}
|
|
|
|
conn = sqlite3.connect(str(db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
cur = conn.cursor()
|
|
|
|
# Get table info first
|
|
cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
tables = [r["name"] for r in cur.fetchall()]
|
|
|
|
if "sessions" not in tables:
|
|
conn.close()
|
|
return {"error": "sessions table not found", "tables": tables}
|
|
|
|
# Get active sessions
|
|
cur.execute("SELECT * FROM sessions WHERE status = 'active' ORDER BY started_at DESC")
|
|
active = [dict(r) for r in cur.fetchall()]
|
|
|
|
# Get recent completed (last 24h)
|
|
cutoff = (datetime.utcnow() - timedelta(hours=24)).isoformat()
|
|
cur.execute(
|
|
"SELECT * FROM sessions WHERE status = 'completed' AND started_at > ? ORDER BY started_at DESC",
|
|
(cutoff,)
|
|
)
|
|
recent = [dict(r) for r in cur.fetchall()]
|
|
|
|
conn.close()
|
|
return {"active_sessions": active, "recent_24h": recent}
|
|
|
|
|
|
# ──────────── /engram/register — register a session ────────────
|
|
@app.post("/engram/register")
|
|
async def engram_register(
|
|
session_type: str = Query(default="claude-chat"),
|
|
description: str = Query(default="External Claude session"),
|
|
auth: str = Depends(verify_key),
|
|
):
|
|
"""Register a new session in engram."""
|
|
import sys
|
|
sys.path.insert(0, "/data/symbiont")
|
|
from symbiont.engram import Engram
|
|
eng = Engram()
|
|
sid = eng.register(session_type, description)
|
|
return {"session_id": sid, "status": "registered"}
|
|
|
|
|
|
# ──────────── /engram/log — log to a session ────────────
|
|
@app.post("/engram/log")
|
|
async def engram_log(
|
|
session_id: str = Query(...),
|
|
message: str = Query(...),
|
|
auth: str = Depends(verify_key),
|
|
):
|
|
"""Log a message to an existing session."""
|
|
import sys
|
|
sys.path.insert(0, "/data/symbiont")
|
|
from symbiont.engram import Engram
|
|
eng = Engram()
|
|
eng.log(session_id, message)
|
|
return {"status": "logged"}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="127.0.0.1", port=8112)
|