Skydata001 commited on
Commit
3471442
·
verified ·
1 Parent(s): ec139bd

Upload 4 files

Browse files
Files changed (4) hide show
  1. entrypoint.sh +55 -0
  2. main.py +1854 -0
  3. package-lock.json +6 -0
  4. requirements.txt +36 -0
entrypoint.sh ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # NEXUS AI — Container entrypoint
4
+ # 1) Detects the GPU and launches the vLLM OpenAI-compatible server
5
+ # in the background (skipped automatically when MOCK_MODE=1 or no GPU).
6
+ # 2) Starts the FastAPI/uvicorn orchestrator on port 7860.
7
+ # ============================================================================
8
+ set -euo pipefail
9
+
10
+ log() { echo "[entrypoint $(date +%H:%M:%S)] $*"; }
11
+
12
+ log "=========================================================="
13
+ log " NEXUS AI platform boot sequence"
14
+ log " Model : ${MODEL_NAME}"
15
+ log " Max context : ${MAX_MODEL_LEN}"
16
+ log " GPU mem util : ${GPU_MEMORY_UTILIZATION}"
17
+ log " Mock mode : ${MOCK_MODE:-auto}"
18
+ log "=========================================================="
19
+
20
+ GPU_PRESENT=0
21
+ if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L 2>/dev/null | grep -qi "GPU"; then
22
+ GPU_PRESENT=1
23
+ fi
24
+
25
+ if [[ "${MOCK_MODE:-auto}" == "1" || "${MOCK_MODE:-auto}" == "true" ]]; then
26
+ log "MOCK_MODE forced -> built-in simulation engine will answer."
27
+ elif [[ "${GPU_PRESENT}" == "1" ]]; then
28
+ log "GPU detected -> launching vLLM OpenAI server on ${VLLM_HOST}:${VLLM_PORT}"
29
+ # shellcheck disable=SC2086
30
+ nohup vllm serve "${MODEL_NAME}" \
31
+ --host "${VLLM_HOST}" \
32
+ --port "${VLLM_PORT}" \
33
+ --dtype "${VLLM_DTYPE}" \
34
+ --max-model-len "${MAX_MODEL_LEN}" \
35
+ --gpu-memory-utilization "${GPU_MEMORY_UTILIZATION}" \
36
+ --enable-prefix-caching \
37
+ --disable-log-requests \
38
+ ${EXTRA_VLLM_ARGS:-} \
39
+ > /home/user/app/vllm_server.log 2>&1 &
40
+ VLLM_PID=$!
41
+ log "vLLM server started (pid=${VLLM_PID}). Logs: vllm_server.log"
42
+ log "FastAPI will answer with the simulation engine until vLLM is healthy."
43
+ else
44
+ log "No GPU visible -> running with the built-in simulation engine."
45
+ fi
46
+
47
+ log "Starting FastAPI orchestrator on 0.0.0.0:${PORT}"
48
+ exec uvicorn main:app \
49
+ --host 0.0.0.0 \
50
+ --port "${PORT}" \
51
+ --workers 1 \
52
+ --loop uvloop \
53
+ --http httptools \
54
+ --timeout-keep-alive 75 \
55
+ --log-level info
main.py ADDED
@@ -0,0 +1,1854 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # ============================================================================
3
+ # NEXUS AI — Elite AI Chat Platform · FastAPI Orchestrator
4
+ # ----------------------------------------------------------------------------
5
+ # • Three-tier AI engine ........ Flash / Max (deep reasoning) / Agents swarm
6
+ # • Local inference ............. vLLM OpenAI-compatible server (A100 tuned)
7
+ # • Persistence ................. Turso (libSQL) with automatic local fallback
8
+ # • Security .................... master-token + IP + GPS audit + JWT sessions
9
+ # • Observability ............... live CPU/RAM/GPU/token telemetry (WebSocket)
10
+ # • Self-healing ................ AI log analyzer + on-the-fly patch applier
11
+ # • Sandbox ..................... jailed web terminal + file manager + uploads
12
+ # ============================================================================
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import collections
17
+ import hashlib
18
+ import hmac
19
+ import json
20
+ import logging
21
+ import math
22
+ import os
23
+ import re
24
+ import secrets
25
+ import shutil
26
+ import sqlite3
27
+ import sys
28
+ import threading
29
+ import time
30
+ import traceback
31
+ import uuid
32
+ from datetime import datetime, timedelta, timezone
33
+ from pathlib import Path
34
+ from typing import Any, AsyncGenerator, Deque, Dict, List, Optional, Tuple
35
+
36
+ import jwt
37
+ import psutil
38
+ import requests
39
+ from fastapi import (
40
+ Body,
41
+ Depends,
42
+ FastAPI,
43
+ File,
44
+ HTTPException,
45
+ Query,
46
+ Request,
47
+ UploadFile,
48
+ WebSocket,
49
+ WebSocketDisconnect,
50
+ )
51
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
52
+ from fastapi.staticfiles import StaticFiles
53
+ from pydantic import BaseModel, Field
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Optional dependencies (never crash the boot if one is missing)
57
+ # ---------------------------------------------------------------------------
58
+ try:
59
+ import GPUtil # type: ignore
60
+ except Exception: # pragma: no cover
61
+ GPUtil = None
62
+
63
+ try:
64
+ import libsql_client # type: ignore
65
+ except Exception: # pragma: no cover
66
+ libsql_client = None
67
+
68
+ try:
69
+ from bs4 import BeautifulSoup # type: ignore
70
+ except Exception: # pragma: no cover
71
+ BeautifulSoup = None
72
+
73
+ try:
74
+ from pypdf import PdfReader # type: ignore
75
+ except Exception: # pragma: no cover
76
+ PdfReader = None
77
+
78
+ try:
79
+ import pytesseract # type: ignore
80
+ from PIL import Image # type: ignore
81
+ except Exception: # pragma: no cover
82
+ pytesseract = None
83
+ Image = None
84
+
85
+ # ============================================================================
86
+ # 1. CONFIGURATION
87
+ # ============================================================================
88
+ APP_ROOT = Path(__file__).resolve().parent
89
+ STATIC_DIR = APP_ROOT / "static"
90
+ SANDBOX_DIR = Path(os.environ.get("SANDBOX_DIR", str(APP_ROOT / "sandbox_workspace"))).resolve()
91
+ UPLOAD_DIR = SANDBOX_DIR / "uploads"
92
+ LOCAL_DB_PATH = os.environ.get("LOCAL_DB_PATH", str(APP_ROOT / "nexus_local.db"))
93
+
94
+ for _d in (SANDBOX_DIR, UPLOAD_DIR):
95
+ _d.mkdir(parents=True, exist_ok=True)
96
+
97
+
98
+ class CFG:
99
+ """Central runtime configuration (all overridable via environment)."""
100
+
101
+ APP_NAME: str = os.environ.get("APP_NAME", "NEXUS AI")
102
+ VERSION: str = "3.1.0"
103
+ PORT: int = int(os.environ.get("PORT", "7860"))
104
+
105
+ # --- Security -----------------------------------------------------------
106
+ ADMIN_API_KEY: str = os.environ.get("ADMIN_API_KEY", "").strip()
107
+ JWT_SECRET: str = os.environ.get("JWT_SECRET", "").strip()
108
+ JWT_ALG: str = "HS256"
109
+ JWT_TTL_HOURS: int = int(os.environ.get("JWT_TTL_HOURS", "168")) # 7 days
110
+ SESSION_COOKIE: str = "nexus_session"
111
+
112
+ # --- Geo shield ----------------------------------------------------------
113
+ SAFE_LAT: Optional[float] = (
114
+ float(os.environ["SAFE_LAT"]) if os.environ.get("SAFE_LAT") else None
115
+ )
116
+ SAFE_LON: Optional[float] = (
117
+ float(os.environ["SAFE_LON"]) if os.environ.get("SAFE_LON") else None
118
+ )
119
+ SAFE_RADIUS_KM: float = float(os.environ.get("SAFE_RADIUS_KM", "50"))
120
+ STRICT_GEO: bool = os.environ.get("STRICT_GEO", "0") in ("1", "true", "yes")
121
+
122
+ # --- Database ------------------------------------------------------------
123
+ TURSO_URL: str = os.environ.get("TURSO_DATABASE_URL", "").strip()
124
+ TURSO_TOKEN: str = os.environ.get("TURSO_AUTH_TOKEN", "").strip()
125
+
126
+ # --- Inference -----------------------------------------------------------
127
+ VLLM_URL: str = (
128
+ f"http://{os.environ.get('VLLM_HOST', '127.0.0.1')}:"
129
+ f"{os.environ.get('VLLM_PORT', '8000')}/v1"
130
+ )
131
+ MODEL_NAME: str = os.environ.get(
132
+ "MODEL_NAME", "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct"
133
+ )
134
+ HF_TOKEN: str = os.environ.get("HF_TOKEN", "").strip()
135
+ MOCK_MODE: bool = os.environ.get("MOCK_MODE", "auto").lower() in ("1", "true", "yes")
136
+
137
+ # --- Generation defaults -------------------------------------------------
138
+ DEFAULT_MAX_TOKENS: int = int(os.environ.get("DEFAULT_MAX_TOKENS", "4096"))
139
+ DEFAULT_CONTEXT_WINDOW: int = int(os.environ.get("MAX_MODEL_LEN", "32768"))
140
+ LLM_TIMEOUT_S: int = int(os.environ.get("LLM_TIMEOUT_S", "600"))
141
+
142
+ # --- Sandbox terminal ----------------------------------------------------
143
+ TERMINAL_TIMEOUT_S: int = int(os.environ.get("TERMINAL_TIMEOUT_S", "180"))
144
+ TERMINAL_MAX_OUTPUT: int = int(os.environ.get("TERMINAL_MAX_OUTPUT", str(256 * 1024)))
145
+
146
+ # --- Web research ---------------------------------------------------------
147
+ SEARCH_TIMEOUT_S: int = int(os.environ.get("SEARCH_TIMEOUT_S", "15"))
148
+ FETCH_MAX_BYTES: int = int(os.environ.get("FETCH_MAX_BYTES", str(120 * 1024)))
149
+
150
+ LOG_BUFFER_SIZE: int = 2000
151
+
152
+
153
+ if not CFG.ADMIN_API_KEY:
154
+ CFG.ADMIN_API_KEY = secrets.token_urlsafe(24)
155
+ _AUTO_KEY = True
156
+ else:
157
+ _AUTO_KEY = False
158
+
159
+ if not CFG.JWT_SECRET:
160
+ CFG.JWT_SECRET = hashlib.sha256(f"nexus::{CFG.ADMIN_API_KEY}".encode()).hexdigest()
161
+
162
+ # ============================================================================
163
+ # 2. LOGGING — console + rolling in-memory buffer + async DB mirroring
164
+ # ============================================================================
165
+ LOG_BUFFER: Deque[Dict[str, Any]] = collections.deque(maxlen=CFG.LOG_BUFFER_SIZE)
166
+
167
+
168
+ class RingBufferHandler(logging.Handler):
169
+ """Keeps the last N records in RAM and mirrors them into Turso (best effort)."""
170
+
171
+ def emit(self, record: logging.LogRecord) -> None:
172
+ try:
173
+ entry = {
174
+ "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"),
175
+ "level": record.levelname,
176
+ "logger": record.name,
177
+ "message": self.format(record),
178
+ }
179
+ LOG_BUFFER.append(entry)
180
+ try:
181
+ loop = asyncio.get_running_loop()
182
+ loop.create_task(self._persist(entry))
183
+ except RuntimeError:
184
+ pass # no running loop yet (early boot)
185
+ except Exception:
186
+ pass
187
+
188
+ @staticmethod
189
+ async def _persist(entry: Dict[str, Any]) -> None:
190
+ try:
191
+ if DB.ready:
192
+ await DB.execute(
193
+ "INSERT INTO system_logs(level, logger, message) VALUES(?,?,?)",
194
+ (entry["level"], entry["logger"], entry["message"][:4000]),
195
+ )
196
+ except Exception:
197
+ pass
198
+
199
+
200
+ logging.basicConfig(
201
+ level=logging.INFO,
202
+ format="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s",
203
+ stream=sys.stdout,
204
+ )
205
+ _ring = RingBufferHandler()
206
+ _ring.setFormatter(logging.Formatter("%(message)s"))
207
+ logging.getLogger().addHandler(_ring)
208
+ log = logging.getLogger("nexus")
209
+
210
+ # ============================================================================
211
+ # 3. DATABASE — Turso (libSQL) with automatic local-file fallback
212
+ # ============================================================================
213
+ SCHEMA_STATEMENTS: Tuple[str, ...] = (
214
+ """CREATE TABLE IF NOT EXISTS users (
215
+ id TEXT PRIMARY KEY,
216
+ username TEXT UNIQUE,
217
+ created_at TEXT DEFAULT (datetime('now'))
218
+ )""",
219
+ """CREATE TABLE IF NOT EXISTS chats (
220
+ id TEXT PRIMARY KEY,
221
+ user_id TEXT,
222
+ title TEXT NOT NULL DEFAULT 'New chat',
223
+ mode TEXT NOT NULL DEFAULT 'flash',
224
+ created_at TEXT DEFAULT (datetime('now')),
225
+ updated_at TEXT DEFAULT (datetime('now'))
226
+ )""",
227
+ """CREATE TABLE IF NOT EXISTS messages (
228
+ id TEXT PRIMARY KEY,
229
+ chat_id TEXT NOT NULL,
230
+ role TEXT NOT NULL,
231
+ content TEXT NOT NULL,
232
+ mode TEXT DEFAULT 'flash',
233
+ tokens INTEGER DEFAULT 0,
234
+ created_at TEXT DEFAULT (datetime('now'))
235
+ )""",
236
+ """CREATE INDEX IF NOT EXISTS idx_messages_chat ON messages(chat_id)""",
237
+ """CREATE TABLE IF NOT EXISTS memories (
238
+ id TEXT PRIMARY KEY,
239
+ key TEXT UNIQUE NOT NULL,
240
+ value TEXT NOT NULL,
241
+ updated_at TEXT DEFAULT (datetime('now'))
242
+ )""",
243
+ """CREATE TABLE IF NOT EXISTS system_logs (
244
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
245
+ level TEXT,
246
+ logger TEXT,
247
+ message TEXT,
248
+ created_at TEXT DEFAULT (datetime('now'))
249
+ )""",
250
+ """CREATE TABLE IF NOT EXISTS sandbox_projects (
251
+ id TEXT PRIMARY KEY,
252
+ name TEXT UNIQUE NOT NULL,
253
+ files_json TEXT NOT NULL DEFAULT '{}',
254
+ updated_at TEXT DEFAULT (datetime('now'))
255
+ )""",
256
+ """CREATE TABLE IF NOT EXISTS auth_audit (
257
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
258
+ ip TEXT,
259
+ latitude REAL,
260
+ longitude REAL,
261
+ success INTEGER DEFAULT 0,
262
+ reason TEXT,
263
+ user_agent TEXT,
264
+ created_at TEXT DEFAULT (datetime('now'))
265
+ )""",
266
+ )
267
+
268
+
269
+ class _SQLiteShim:
270
+ """Tiny stdlib fallback exposing the subset of the libsql API we use."""
271
+
272
+ def __init__(self, path: str) -> None:
273
+ self.conn = sqlite3.connect(path, check_same_thread=False)
274
+ self._lock = threading.Lock()
275
+
276
+ def execute(self, sql: str, params: Any = None):
277
+ with self._lock:
278
+ cur = self.conn.execute(sql, tuple(params or ()))
279
+ self.conn.commit()
280
+
281
+ class RS:
282
+ columns = [d[0] for d in (cur.description or [])]
283
+ rows = cur.fetchall()
284
+ rows_affected = cur.rowcount
285
+
286
+ return RS()
287
+
288
+ def close(self) -> None:
289
+ self.conn.close()
290
+
291
+
292
+ class _Database:
293
+ """Async façade around the (synchronous) libsql/sqlite clients."""
294
+
295
+ def __init__(self) -> None:
296
+ self.client: Any = None
297
+ self.backend: str = "none"
298
+ self.ready: bool = False
299
+
300
+ async def connect(self) -> None:
301
+ if CFG.TURSO_URL and libsql_client is not None:
302
+ try:
303
+ self.client = libsql_client.create_client(
304
+ CFG.TURSO_URL, auth_token=CFG.TURSO_TOKEN or None
305
+ )
306
+ await asyncio.to_thread(self.client.execute, "SELECT 1")
307
+ self.backend = "turso"
308
+ log.info("Connected to Turso database: %s", CFG.TURSO_URL)
309
+ except Exception as exc: # pragma: no cover
310
+ log.error("Turso connection failed (%s) — falling back to local DB", exc)
311
+ self.client = None
312
+
313
+ if self.client is None:
314
+ if libsql_client is not None:
315
+ self.client = libsql_client.create_client(f"file:{LOCAL_DB_PATH}")
316
+ else:
317
+ self.client = _SQLiteShim(LOCAL_DB_PATH)
318
+ self.backend = "local"
319
+ log.warning("Using LOCAL embedded database at %s", LOCAL_DB_PATH)
320
+
321
+ for stmt in SCHEMA_STATEMENTS:
322
+ await asyncio.to_thread(self.client.execute, stmt)
323
+ self.ready = True
324
+ log.info("Database schema verified (%s backend)", self.backend)
325
+
326
+ # -- helpers -------------------------------------------------------------
327
+ async def execute(self, sql: str, params: Any = None):
328
+ if not self.ready:
329
+ raise RuntimeError("database not ready")
330
+ return await asyncio.to_thread(self.client.execute, sql, tuple(params or ()))
331
+
332
+ async def query(self, sql: str, params: Any = None) -> List[Dict[str, Any]]:
333
+ rs = await self.execute(sql, params)
334
+ cols = list(rs.columns)
335
+ return [dict(zip(cols, row)) for row in rs.rows]
336
+
337
+ async def query_one(self, sql: str, params: Any = None) -> Optional[Dict[str, Any]]:
338
+ rows = await self.query(sql, params)
339
+ return rows[0] if rows else None
340
+
341
+ def close(self) -> None:
342
+ try:
343
+ self.client.close()
344
+ except Exception:
345
+ pass
346
+
347
+
348
+ DB = _Database()
349
+
350
+ # ============================================================================
351
+ # 4. SMALL UTILITIES
352
+ # ============================================================================
353
+ def new_id() -> str:
354
+ return uuid.uuid4().hex
355
+
356
+
357
+ def utcnow() -> str:
358
+ return datetime.now(timezone.utc).isoformat(timespec="seconds")
359
+
360
+
361
+ def approx_tokens(text: str) -> int:
362
+ """Fast heuristic token estimate (≈4 chars/token, min 1 for non-empty)."""
363
+ if not text:
364
+ return 0
365
+ return max(1, math.ceil(len(text) / 4))
366
+
367
+
368
+ def clamp(value: float, lo: float, hi: float) -> float:
369
+ return max(lo, min(hi, value))
370
+
371
+
372
+ def haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
373
+ r = 6371.0
374
+ p1, p2 = math.radians(lat1), math.radians(lat2)
375
+ dp = math.radians(lat2 - lat1)
376
+ dl = math.radians(lon2 - lon1)
377
+ a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
378
+ return 2 * r * math.asin(math.sqrt(a))
379
+
380
+
381
+ def client_ip(request: Request) -> str:
382
+ fwd = request.headers.get("x-forwarded-for", "")
383
+ if fwd:
384
+ return fwd.split(",")[0].strip()
385
+ return request.client.host if request.client else "unknown"
386
+
387
+ # ============================================================================
388
+ # 5. SECURITY — JWT sessions, geo shield, dependency guards
389
+ # ============================================================================
390
+ def issue_token(ip: str) -> str:
391
+ now = datetime.now(timezone.utc)
392
+ payload = {
393
+ "sub": "nexus-admin",
394
+ "ip": ip,
395
+ "iat": int(now.timestamp()),
396
+ "exp": int((now + timedelta(hours=CFG.JWT_TTL_HOURS)).timestamp()),
397
+ "jti": new_id(),
398
+ }
399
+ return jwt.encode(payload, CFG.JWT_SECRET, algorithm=CFG.JWT_ALG)
400
+
401
+
402
+ def decode_token(token: str) -> Optional[Dict[str, Any]]:
403
+ try:
404
+ return jwt.decode(token, CFG.JWT_SECRET, algorithms=[CFG.JWT_ALG])
405
+ except Exception:
406
+ return None
407
+
408
+
409
+ def token_from_request(request: Request) -> Optional[str]:
410
+ auth = request.headers.get("authorization", "")
411
+ if auth.lower().startswith("bearer "):
412
+ return auth.split(" ", 1)[1].strip()
413
+ cookie = request.cookies.get(CFG.SESSION_COOKIE)
414
+ if cookie:
415
+ return cookie
416
+ return request.query_params.get("token")
417
+
418
+
419
+ async def require_session(request: Request) -> Dict[str, Any]:
420
+ token = token_from_request(request)
421
+ session = decode_token(token) if token else None
422
+ if not session:
423
+ raise HTTPException(status_code=401, detail="authentication required")
424
+ return session
425
+
426
+
427
+ def ws_session(ws: WebSocket) -> Optional[Dict[str, Any]]:
428
+ token = ws.query_params.get("token") or ws.cookies.get(CFG.SESSION_COOKIE)
429
+ return decode_token(token) if token else None
430
+
431
+
432
+ def geo_verdict(lat: Optional[float], lon: Optional[float]) -> Tuple[bool, str]:
433
+ """Returns (accepted, reason). Registers everything for audit either way."""
434
+ if CFG.SAFE_LAT is None or CFG.SAFE_LON is None:
435
+ return True, "geo-fence not configured — audited"
436
+ if lat is None or lon is None:
437
+ return (not CFG.STRICT_GEO), "no coordinates supplied"
438
+ dist = haversine_km(CFG.SAFE_LAT, CFG.SAFE_LON, lat, lon)
439
+ if dist <= CFG.SAFE_RADIUS_KM:
440
+ return True, f"inside safe zone ({dist:.1f} km)"
441
+ if CFG.STRICT_GEO:
442
+ return False, f"outside safe zone ({dist:.1f} km > {CFG.SAFE_RADIUS_KM} km)"
443
+ return True, f"outside safe zone ({dist:.1f} km) — audited"
444
+
445
+
446
+ class AuthVerifyIn(BaseModel):
447
+ token: str = Field(..., min_length=1, max_length=512)
448
+ latitude: Optional[float] = None
449
+ longitude: Optional[float] = None
450
+
451
+
452
+ # ============================================================================
453
+ # 6. SAFETY ENGINE — three strictness profiles
454
+ # ============================================================================
455
+ class SafetyEngine:
456
+ """Pattern-based protective filter pipeline.
457
+
458
+ Profiles
459
+ --------
460
+ very_severe : hyper-sensitive — blocks injection, malware, violence, NSFW,
461
+ jailbreak attempts and redacts PII on input AND output.
462
+ balanced : default protection — blocks malware/violence/injection,
463
+ warns + redacts PII.
464
+ none : every filter disabled (unrestricted developer operations).
465
+ """
466
+
467
+ PATTERNS: Dict[str, str] = {
468
+ "prompt_injection": (
469
+ r"(?i)\b(ignore\s+(all|any|previous|prior)\s+(instructions|prompts|rules)"
470
+ r"|disregard\s+(your|all)\s+(instructions|training)"
471
+ r"|reveal\s+(your|the)\s+(system\s+prompt|instructions)"
472
+ r"|\bDAN\b|\bjailbreak\b|bypass\s+(the\s+)?(safety|filters?))"
473
+ ),
474
+ "malware": (
475
+ r"(?i)\b(ransomware|keylogger|botnet|rat\s+trojan|exploit\s+kit"
476
+ r"|reverse\s+shell\s+payload|dropper\s+script|rootkit|cryptominer\s+payload)\b"
477
+ ),
478
+ "violence": (
479
+ r"(?i)\b(build|make|assemble)\s+(a\s+)?(bomb|explosive|pipe\s+bomb|molotov)"
480
+ r"|\b(assassination\s+plan|school\s+shooting\s+plan)\b"
481
+ ),
482
+ "nsfw": r"(?i)\b(pornograph|explicit\s+sexual\s+content|\bxxx\b)\b",
483
+ "pii_email": r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}",
484
+ "pii_ssn": r"\b\d{3}-\d{2}-\d{4}\b",
485
+ "pii_card": r"\b(?:\d[ -]*?){13,16}\b",
486
+ }
487
+
488
+ BLOCK_RULES: Dict[str, Tuple[str, ...]] = {
489
+ "very_severe": (
490
+ "prompt_injection", "malware", "violence", "nsfw",
491
+ "pii_email", "pii_ssn", "pii_card",
492
+ ),
493
+ "balanced": ("prompt_injection", "malware", "violence"),
494
+ "none": (),
495
+ }
496
+ REDACT_RULES: Dict[str, Tuple[str, ...]] = {
497
+ "very_severe": ("pii_email", "pii_ssn", "pii_card"),
498
+ "balanced": ("pii_email", "pii_ssn", "pii_card"),
499
+ "none": (),
500
+ }
501
+
502
+ def __init__(self) -> None:
503
+ self._compiled = {k: re.compile(v) for k, v in self.PATTERNS.items()}
504
+
505
+ def scan(
506
+ self, text: str, profile: str
507
+ ) -> Tuple[bool, List[str], str]:
508
+ profile = (profile or "balanced").lower()
509
+ if profile not in self.BLOCK_RULES:
510
+ profile = "balanced"
511
+ violations: List[str] = []
512
+ sanitized = text
513
+ for name in self.BLOCK_RULES[profile]:
514
+ if self._compiled[name].search(text):
515
+ violations.append(name)
516
+ for name in self.REDACT_RULES[profile]:
517
+ sanitized = self._compiled[name].sub(f"[REDACTED-{name.upper()}]", sanitized)
518
+ blocked = bool(violations) and profile != "none"
519
+ return (not blocked), violations, sanitized
520
+
521
+
522
+ SAFETY = SafetyEngine()
523
+
524
+ # ============================================================================
525
+ # 7. WEB RESEARCH — DuckDuckGo HTML search + lightweight page fetching
526
+ # ============================================================================
527
+ TRUSTED_DOMAINS = (
528
+ "github.com", "stackoverflow.com", "stackexchange.com", "arxiv.org",
529
+ "docs.python.org", "developer.mozilla.org", "huggingface.co",
530
+ "pytorch.org", "wikipedia.org",
531
+ )
532
+
533
+
534
+ def ddg_search(query: str, max_results: int = 5, trusted_only: bool = False) -> List[Dict[str, str]]:
535
+ """Scrape DuckDuckGo's HTML endpoint. Returns [{title, url, snippet}]."""
536
+ results: List[Dict[str, str]] = []
537
+ try:
538
+ resp = requests.get(
539
+ "https://html.duckduckgo.com/html/",
540
+ params={"q": query},
541
+ headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) NEXUS-Research/3.0"},
542
+ timeout=CFG.SEARCH_TIMEOUT_S,
543
+ )
544
+ resp.raise_for_status()
545
+ if BeautifulSoup is None:
546
+ return results
547
+ soup = BeautifulSoup(resp.text, "lxml")
548
+ for res in soup.select(".result"):
549
+ a = res.select_one(".result__a")
550
+ sn = res.select_one(".result__snippet")
551
+ if not a or not a.get("href"):
552
+ continue
553
+ url = a["href"]
554
+ m = re.search(r"uddg=([^&]+)", url)
555
+ if m:
556
+ from urllib.parse import unquote
557
+ url = unquote(m.group(1))
558
+ if trusted_only and not any(d in url for d in TRUSTED_DOMAINS):
559
+ continue
560
+ results.append(
561
+ {
562
+ "title": a.get_text(strip=True)[:200],
563
+ "url": url,
564
+ "snippet": (sn.get_text(strip=True)[:400] if sn else ""),
565
+ }
566
+ )
567
+ if len(results) >= max_results:
568
+ break
569
+ except Exception as exc:
570
+ log.warning("web search failed: %s", exc)
571
+ return results
572
+
573
+
574
+ def fetch_page_text(url: str, max_chars: int = 6000) -> str:
575
+ """Fetch a page and return cleaned visible text (bounded)."""
576
+ try:
577
+ resp = requests.get(
578
+ url,
579
+ headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) NEXUS-Research/3.0"},
580
+ timeout=CFG.SEARCH_TIMEOUT_S,
581
+ stream=True,
582
+ )
583
+ resp.raise_for_status()
584
+ raw = resp.raw.read(CFG.FETCH_MAX_BYTES, decode_content=True)
585
+ if BeautifulSoup is None:
586
+ return raw.decode("utf-8", "ignore")[:max_chars]
587
+ soup = BeautifulSoup(raw, "lxml")
588
+ for tag in soup(["script", "style", "noscript", "header", "footer", "nav"]):
589
+ tag.decompose()
590
+ text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True))
591
+ return text[:max_chars]
592
+ except Exception as exc:
593
+ log.warning("fetch failed for %s: %s", url, exc)
594
+ return ""
595
+
596
+ # ============================================================================
597
+ # 8. TOKEN METER — cumulative generation-rate accounting
598
+ # ============================================================================
599
+ class TokenMeter:
600
+ def __init__(self) -> None:
601
+ self.events: Deque[Tuple[float, int]] = collections.deque(maxlen=10000)
602
+ self.total: int = 0
603
+
604
+ def record(self, n: int) -> None:
605
+ if n <= 0:
606
+ return
607
+ self.events.append((time.time(), n))
608
+ self.total += n
609
+
610
+ def rate(self, window_s: float = 30.0) -> float:
611
+ cutoff = time.time() - window_s
612
+ return sum(n for ts, n in self.events if ts >= cutoff) / window_s
613
+
614
+
615
+ METER = TokenMeter()
616
+
617
+ # ============================================================================
618
+ # 9. LLM CLIENT — vLLM OpenAI-compatible backend + resilient simulation engine
619
+ # ============================================================================
620
+ class LLMClient:
621
+ def __init__(self) -> None:
622
+ self._health_cache: Tuple[float, bool] = (0.0, False)
623
+
624
+ # -- health ---------------------------------------------------------------
625
+ def _health_sync(self) -> bool:
626
+ try:
627
+ r = requests.get(f"{CFG.VLLM_URL}/models", timeout=2.5)
628
+ return r.status_code == 200
629
+ except Exception:
630
+ return False
631
+
632
+ async def healthy(self) -> bool:
633
+ if CFG.MOCK_MODE:
634
+ return False
635
+ ts, cached = self._health_cache
636
+ if time.time() - ts < 15:
637
+ return cached
638
+ ok = await asyncio.to_thread(self._health_sync)
639
+ self._health_cache = (time.time(), ok)
640
+ return ok
641
+
642
+ # -- payload ---------------------------------------------------------------
643
+ @staticmethod
644
+ def _payload(messages: List[Dict[str, str]], s: Dict[str, Any], stream: bool) -> Dict[str, Any]:
645
+ return {
646
+ "model": s.get("model") or CFG.MODEL_NAME,
647
+ "messages": messages,
648
+ "temperature": clamp(float(s.get("temperature", 0.7)), 0.0, 2.0),
649
+ "top_p": clamp(float(s.get("top_p", 0.95)), 0.0, 1.0),
650
+ "top_k": int(s.get("top_k", 40)),
651
+ "min_p": clamp(float(s.get("min_p", 0.0)), 0.0, 1.0),
652
+ "max_tokens": int(clamp(int(s.get("max_tokens", CFG.DEFAULT_MAX_TOKENS)), 16, 131072)),
653
+ "frequency_penalty": clamp(float(s.get("frequency_penalty", 0.0)), -2.0, 2.0),
654
+ "presence_penalty": clamp(float(s.get("presence_penalty", 0.0)), -2.0, 2.0),
655
+ "repetition_penalty": clamp(float(s.get("repetition_penalty", 1.0)), 0.0, 2.0),
656
+ "stream": stream,
657
+ }
658
+
659
+ @staticmethod
660
+ def _headers() -> Dict[str, str]:
661
+ key = CFG.HF_TOKEN or "EMPTY"
662
+ return {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
663
+
664
+ # -- non-streaming completion ----------------------------------------------
665
+ def _complete_sync(self, messages: List[Dict[str, str]], s: Dict[str, Any]) -> str:
666
+ r = requests.post(
667
+ f"{CFG.VLLM_URL}/chat/completions",
668
+ headers=self._headers(),
669
+ json=self._payload(messages, s, stream=False),
670
+ timeout=CFG.LLM_TIMEOUT_S,
671
+ )
672
+ r.raise_for_status()
673
+ data = r.json()
674
+ usage = data.get("usage") or {}
675
+ METER.record(int(usage.get("completion_tokens", 0)))
676
+ return (data["choices"][0]["message"].get("content") or "").strip()
677
+
678
+ async def acomplete(self, messages: List[Dict[str, str]], s: Dict[str, Any]) -> str:
679
+ if not await self.healthy():
680
+ return self._mock_response(messages)
681
+ try:
682
+ return await asyncio.to_thread(self._complete_sync, messages, s)
683
+ except Exception as exc:
684
+ log.error("LLM completion failed: %s", exc)
685
+ return self._mock_response(messages)
686
+
687
+ # -- streaming completion ----------------------------------------------------
688
+ def _stream_sync(self, messages: List[Dict[str, str]], s: Dict[str, Any], q: "queue.Queue"):
689
+ import queue as _q # local alias
690
+ try:
691
+ with requests.post(
692
+ f"{CFG.VLLM_URL}/chat/completions",
693
+ headers=self._headers(),
694
+ json=self._payload(messages, s, stream=True),
695
+ timeout=CFG.LLM_TIMEOUT_S,
696
+ stream=True,
697
+ ) as r:
698
+ r.raise_for_status()
699
+ for line in r.iter_lines(decode_unicode=True):
700
+ if not line or not line.startswith("data:"):
701
+ continue
702
+ data = line[5:].strip()
703
+ if data == "[DONE]":
704
+ break
705
+ try:
706
+ chunk = json.loads(data)
707
+ delta = (chunk["choices"][0].get("delta") or {}).get("content")
708
+ if delta:
709
+ q.put(delta)
710
+ except Exception:
711
+ continue
712
+ except Exception as exc:
713
+ q.put(exc)
714
+ finally:
715
+ q.put(None)
716
+
717
+ async def astream(
718
+ self, messages: List[Dict[str, str]], s: Dict[str, Any]
719
+ ) -> AsyncGenerator[str, None]:
720
+ if not await self.healthy():
721
+ async for piece in self._mock_stream(messages):
722
+ yield piece
723
+ return
724
+
725
+ import queue
726
+ q: "queue.Queue" = queue.Queue()
727
+ loop = asyncio.get_running_loop()
728
+ threading.Thread(target=self._stream_sync, args=(messages, s, q), daemon=True).start()
729
+ while True:
730
+ item = await asyncio.to_thread(q.get)
731
+ if item is None:
732
+ break
733
+ if isinstance(item, Exception):
734
+ log.error("LLM stream failed: %s", item)
735
+ async for piece in self._mock_stream(messages):
736
+ yield piece
737
+ return
738
+ METER.record(approx_tokens(item))
739
+ yield item
740
+
741
+ # -- simulation engine (no-GPU fallback, always available) -------------------
742
+ @staticmethod
743
+ def _last_user(messages: List[Dict[str, str]]) -> str:
744
+ for m in reversed(messages):
745
+ if m.get("role") == "user":
746
+ return m.get("content", "")
747
+ return ""
748
+
749
+ def _mock_response(self, messages: List[Dict[str, str]]) -> str:
750
+ user = self._last_user(messages).strip()
751
+ excerpt = user[:300] + ("…" if len(user) > 300 else "")
752
+ return (
753
+ "⚙️ **Simulation engine** — the vLLM server is not reachable yet "
754
+ "(model still loading, or no GPU attached).\n\n"
755
+ f"**You asked:**\n> {excerpt or '(empty prompt)'}\n\n"
756
+ "**What I can tell you right now:**\n"
757
+ "- The NEXUS orchestrator is healthy and all APIs are live.\n"
758
+ "- Once the local model finishes loading, Flash/Max/Agents will stream "
759
+ "real tokens from it automatically.\n"
760
+ "- Set `MOCK_MODE=0` (default) to keep waiting for vLLM, or check "
761
+ "`vllm_server.log` in the sandbox for boot progress.\n\n"
762
+ "```python\n# quick probe you can run in the NEXUS console\n"
763
+ "curl -s http://127.0.0.1:8000/v1/models | head\n```"
764
+ )
765
+
766
+ async def _mock_stream(self, messages: List[Dict[str, str]]) -> AsyncGenerator[str, None]:
767
+ text = self._mock_response(messages)
768
+ for i in range(0, len(text), 6):
769
+ piece = text[i : i + 6]
770
+ METER.record(approx_tokens(piece))
771
+ yield piece
772
+ await asyncio.sleep(0.012)
773
+
774
+
775
+ LLM = LLMClient()
776
+
777
+ # ============================================================================
778
+ # 10. PROMPT ASSEMBLY
779
+ # ============================================================================
780
+ BASE_SYSTEM = (
781
+ "You are NEXUS, an elite principal-level AI pair-programmer and systems architect "
782
+ "running locally on dedicated GPU infrastructure. You answer with precision, "
783
+ "write production-grade code with no placeholders, and explain trade-offs briefly. "
784
+ "Format answers in Markdown with fenced code blocks that include the language."
785
+ )
786
+
787
+ AGENT_ROLES: Dict[str, str] = {
788
+ "project_manager": (
789
+ "You are the PROJECT MANAGER agent. Decompose the request into a concrete, "
790
+ "ordered task list. Reply ONLY with a JSON array like "
791
+ '[{"agent":"coder","task":"..."}]. Valid agents: researcher, coder, debugger, '
792
+ "security, database, ui_ux. Max 5 tasks, no prose."
793
+ ),
794
+ "researcher": (
795
+ "You are the RESEARCHER agent. Gather the key facts, APIs, and prior art "
796
+ "relevant to the task. Output concise bullet points with source names."
797
+ ),
798
+ "coder": (
799
+ "You are the CODER agent. Write complete, production-ready code. Every file "
800
+ "in its own fenced block whose first line is a `# path: <filename>` comment."
801
+ ),
802
+ "debugger": (
803
+ "You are the DEBUGGER agent. Review the produced artifacts for runtime errors, "
804
+ "edge cases, race conditions and logic bugs. List each issue with its fix."
805
+ ),
806
+ "security": (
807
+ "You are the SECURITY agent. Audit the artifacts for injection, auth, secrets "
808
+ "handling, and unsafe defaults. Give a verdict plus concrete hardening steps."
809
+ ),
810
+ "database": (
811
+ "You are the DATABASE agent. Design schemas, write optimized SQL/libSQL "
812
+ "queries and indexes for the task."
813
+ ),
814
+ "ui_ux": (
815
+ "You are the UI/UX agent. Produce refined, responsive, accessible interface "
816
+ "code following the dark glassmorphic design system (#0B0F19, cyan accents)."
817
+ ),
818
+ }
819
+
820
+
821
+ async def build_messages(payload: Dict[str, Any], s: Dict[str, Any]) -> List[Dict[str, str]]:
822
+ parts: List[str] = [BASE_SYSTEM]
823
+
824
+ if s.get("persistent_instructions_enabled") and s.get("persistent_instructions"):
825
+ parts.append(f"Persistent instructions:\n{s['persistent_instructions']}")
826
+ if s.get("system_prompt_enabled") and s.get("system_prompt"):
827
+ parts.append(str(s["system_prompt"]))
828
+ if s.get("developer_prompt_enabled") and s.get("developer_prompt"):
829
+ parts.append(f"[Developer directive] {s['developer_prompt']}")
830
+ if s.get("user_profile"):
831
+ parts.append(f"[User profile] {s['user_profile']}")
832
+ if s.get("strict_instructions"):
833
+ parts.append("Follow every user instruction strictly and literally.")
834
+ lang = (s.get("language") or "auto").lower()
835
+ if lang != "auto":
836
+ parts.append(f"Always answer in {lang}.")
837
+
838
+ if s.get("use_memory"):
839
+ try:
840
+ rows = await DB.query("SELECT key, value FROM memories ORDER BY updated_at DESC LIMIT 10")
841
+ if rows:
842
+ mem = "\n".join(f"- {r['key']}: {r['value']}" for r in rows)
843
+ parts.append(f"[Long-term memory]\n{mem}")
844
+ except Exception:
845
+ pass
846
+
847
+ if s.get("citations") and s.get("web_search"):
848
+ parts.append("Cite sources inline as [1], [2]… matching the provided source list.")
849
+
850
+ messages: List[Dict[str, str]] = [{"role": "system", "content": "\n\n".join(parts)}]
851
+
852
+ # --- context window management (approximate) ------------------------------
853
+ budget = int(s.get("context_window", CFG.DEFAULT_CONTEXT_WINDOW)) - int(
854
+ s.get("max_tokens", CFG.DEFAULT_MAX_TOKENS)
855
+ )
856
+ budget_chars = max(4000, budget * 4)
857
+ history = list(payload.get("messages", []))
858
+ kept: List[Dict[str, str]] = []
859
+ total = 0
860
+ for m in reversed(history):
861
+ c = len(m.get("content", ""))
862
+ if total + c > budget_chars and kept:
863
+ break
864
+ kept.append(m)
865
+ total += c
866
+ kept.reverse()
867
+ for m in kept:
868
+ role = m.get("role") if m.get("role") in ("user", "assistant", "system") else "user"
869
+ messages.append({"role": role, "content": str(m.get("content", ""))[:60000]})
870
+ return messages
871
+
872
+ # ============================================================================
873
+ # 11. WEBSOCKET SEND HELPER
874
+ # ============================================================================
875
+ async def ws_send(ws: WebSocket, event: Dict[str, Any]) -> bool:
876
+ try:
877
+ await ws.send_text(json.dumps(event, ensure_ascii=False))
878
+ return True
879
+ except Exception:
880
+ return False
881
+
882
+
883
+ # ============================================================================
884
+ # 12. ENGINE — FLASH (direct low-latency streaming)
885
+ # ============================================================================
886
+ async def engine_flash(ws: WebSocket, payload: Dict[str, Any], s: Dict[str, Any]) -> str:
887
+ messages = await build_messages(payload, s)
888
+ acc = ""
889
+ async for piece in LLM.astream(messages, s):
890
+ acc += piece
891
+ if not await ws_send(ws, {"type": "token", "content": piece}):
892
+ break
893
+ return acc
894
+
895
+
896
+ # ============================================================================
897
+ # 13. ENGINE — MAX (visible deep-reasoning pipeline)
898
+ # ============================================================================
899
+ async def engine_max(ws: WebSocket, payload: Dict[str, Any], s: Dict[str, Any]) -> Tuple[str, List[Dict[str, str]]]:
900
+ user_q = ""
901
+ for m in reversed(payload.get("messages", [])):
902
+ if m.get("role") == "user":
903
+ user_q = m.get("content", "")
904
+ break
905
+
906
+ thinking_budget = int(s.get("thinking_budget", 1200))
907
+ think_s = dict(s)
908
+ think_s["max_tokens"] = thinking_budget
909
+ think_s["temperature"] = min(float(s.get("temperature", 0.7)), 0.5)
910
+ sources: List[Dict[str, str]] = []
911
+
912
+ # -- Step 1 · Planning ------------------------------------------------------
913
+ await ws_send(ws, {"type": "thinking", "step": "planning", "status": "working",
914
+ "content": "Decomposing the problem into a reasoning plan…"})
915
+ plan = await LLM.acomplete(
916
+ [
917
+ {"role": "system", "content": "You are a meticulous planning engine. "
918
+ "Produce a numbered step-by-step plan to solve the user's request. "
919
+ "Be concrete and technical."},
920
+ {"role": "user", "content": user_q},
921
+ ],
922
+ think_s,
923
+ )
924
+ await ws_send(ws, {"type": "thinking", "step": "planning", "status": "done", "content": plan})
925
+
926
+ # -- Step 2 · Web research (optional) ---------------------------------------
927
+ research_digest = ""
928
+ if s.get("web_search"):
929
+ await ws_send(ws, {"type": "thinking", "step": "searching", "status": "working",
930
+ "content": f"Searching the web for: {user_q[:120]}"})
931
+ depth = int(clamp(int(s.get("search_depth", 5)), 1, 10))
932
+ trusted_only = bool(s.get("trusted_sources")) and not s.get("deep_research")
933
+ sources = await asyncio.to_thread(ddg_search, user_q, depth, trusted_only)
934
+
935
+ digests: List[str] = []
936
+ if s.get("deep_research"):
937
+ for item in sources[:4]:
938
+ text = await asyncio.to_thread(fetch_page_text, item["url"], 4000)
939
+ if text:
940
+ item["fetched"] = True
941
+ digests.append(f"### {item['title']}\n{text[:2500]}")
942
+ else:
943
+ digests.append(f"### {item['title']}\n{item['snippet']}")
944
+ else:
945
+ digests = [f"- {r['title']} — {r['snippet']} ({r['url']})" for r in sources]
946
+ research_digest = "\n\n".join(digests)
947
+ await ws_send(ws, {"type": "thinking", "step": "searching", "status": "done",
948
+ "content": f"Collected {len(sources)} source(s)."
949
+ + (f"\n\n{research_digest[:2000]}" if research_digest else "")})
950
+ await ws_send(ws, {"type": "citations", "items": sources})
951
+
952
+ # -- Step 3 · Deep reasoning --------------------------------------------------
953
+ await ws_send(ws, {"type": "thinking", "step": "reasoning", "status": "working",
954
+ "content": "Reasoning through the plan with gathered evidence…"})
955
+ reasoning_input = (
956
+ f"PLAN:\n{plan}\n\n"
957
+ + (f"WEB EVIDENCE:\n{research_digest[:8000]}\n\n" if research_digest else "")
958
+ + f"USER REQUEST:\n{user_q}"
959
+ )
960
+ draft = await LLM.acomplete(
961
+ [
962
+ {"role": "system", "content": "You are a deep reasoning engine. Work through "
963
+ "the plan step by step and produce a thorough draft answer. Show your work."},
964
+ {"role": "user", "content": reasoning_input},
965
+ ],
966
+ think_s,
967
+ )
968
+ await ws_send(ws, {"type": "thinking", "step": "reasoning", "status": "done", "content": draft})
969
+
970
+ # -- Step 4 · Self-correction / reflection --------------------------------------
971
+ if s.get("reflection", True):
972
+ await ws_send(ws, {"type": "thinking", "step": "reflection", "status": "working",
973
+ "content": "Critiquing the draft for errors and gaps…"})
974
+ critique = await LLM.acomplete(
975
+ [
976
+ {"role": "system", "content": "You are a ruthless self-critic. Review the "
977
+ "draft answer: find factual errors, missing edge cases, and weak spots. "
978
+ "Then list precise corrections."},
979
+ {"role": "user", "content": f"REQUEST:\n{user_q}\n\nDRAFT:\n{draft}"},
980
+ ],
981
+ think_s,
982
+ )
983
+ await ws_send(ws, {"type": "thinking", "step": "reflection", "status": "done", "content": critique})
984
+ else:
985
+ critique = ""
986
+
987
+ # -- Step 5 · Final streamed answer ----------------------------------------------
988
+ final_msgs = await build_messages(payload, s)
989
+ final_msgs.append(
990
+ {"role": "system", "content":
991
+ "You have already produced a plan, research digest, draft and self-critique "
992
+ "(provided below). Now write the FINAL polished answer for the user. "
993
+ "Incorporate the corrections; do not mention this meta-process.\n\n"
994
+ f"PLAN:\n{plan[:3000]}\n\nDRAFT:\n{draft[:6000]}\n\nCRITIQUE:\n{critique[:3000]}"}
995
+ )
996
+ acc = ""
997
+ async for piece in LLM.astream(final_msgs, s):
998
+ acc += piece
999
+ if not await ws_send(ws, {"type": "token", "content": piece}):
1000
+ break
1001
+ return acc, sources
1002
+
1003
+
1004
+ # ============================================================================
1005
+ # 14. ENGINE — AGENTS (autonomous specialist swarm)
1006
+ # ============================================================================
1007
+ DEFAULT_PLAN = [
1008
+ {"agent": "researcher", "task": "Clarify requirements and gather key technical facts."},
1009
+ {"agent": "coder", "task": "Implement the complete solution as production-ready files."},
1010
+ {"agent": "debugger", "task": "Review the implementation for bugs and edge cases."},
1011
+ {"agent": "security", "task": "Audit the implementation for vulnerabilities."},
1012
+ ]
1013
+
1014
+
1015
+ def _parse_plan(text: str) -> List[Dict[str, str]]:
1016
+ try:
1017
+ m = re.search(r"\[.*\]", text, re.S)
1018
+ if m:
1019
+ data = json.loads(m.group(0))
1020
+ plan = [
1021
+ {"agent": str(t.get("agent", "coder")), "task": str(t.get("task", ""))[:500]}
1022
+ for t in data if isinstance(t, dict) and t.get("task")
1023
+ ]
1024
+ if plan:
1025
+ return plan[:5]
1026
+ except Exception:
1027
+ pass
1028
+ return list(DEFAULT_PLAN)
1029
+
1030
+
1031
+ async def engine_agents(ws: WebSocket, payload: Dict[str, Any], s: Dict[str, Any]) -> str:
1032
+ user_q = ""
1033
+ for m in reversed(payload.get("messages", [])):
1034
+ if m.get("role") == "user":
1035
+ user_q = m.get("content", "")
1036
+ break
1037
+
1038
+ agent_s = dict(s)
1039
+ agent_s["max_tokens"] = min(int(s.get("max_tokens", CFG.DEFAULT_MAX_TOKENS)), 3000)
1040
+ agent_s["temperature"] = min(float(s.get("temperature", 0.7)), 0.4)
1041
+
1042
+ # -- PM routing ---------------------------------------------------------------
1043
+ await ws_send(ws, {"type": "agent", "agent": "project_manager", "status": "working",
1044
+ "content": "Analyzing the mission and assembling the swarm…"})
1045
+ if s.get("multi_agent", True):
1046
+ plan_text = await LLM.acomplete(
1047
+ [
1048
+ {"role": "system", "content": AGENT_ROLES["project_manager"]},
1049
+ {"role": "user", "content": user_q},
1050
+ ],
1051
+ agent_s,
1052
+ )
1053
+ plan = _parse_plan(plan_text)
1054
+ else:
1055
+ plan = [{"agent": "coder", "task": user_q[:400]}]
1056
+ pretty = "\n".join(f"{i+1}. **{t['agent']}** → {t['task']}" for i, t in enumerate(plan))
1057
+ await ws_send(ws, {"type": "agent", "agent": "project_manager", "status": "done",
1058
+ "content": f"Mission plan ({len(plan)} task(s)):\n{pretty}"})
1059
+
1060
+ # -- Execution routing loop -----------------------------------------------------
1061
+ artifacts: List[Dict[str, str]] = []
1062
+ for idx, task in enumerate(plan, 1):
1063
+ role = task["agent"] if task["agent"] in AGENT_ROLES else "coder"
1064
+ await ws_send(ws, {"type": "agent", "agent": role, "status": "working",
1065
+ "content": f"[{idx}/{len(plan)}] {task['task']}"})
1066
+ prior = ""
1067
+ if artifacts:
1068
+ prior = "\n\n".join(
1069
+ f"[Output of {a['agent']}]\n{a['output'][:1500]}" for a in artifacts[-3:]
1070
+ )
1071
+ out = await LLM.acomplete(
1072
+ [
1073
+ {"role": "system", "content": AGENT_ROLES[role]},
1074
+ {"role": "user", "content":
1075
+ f"MISSION:\n{user_q}\n\nYOUR TASK:\n{task['task']}\n\n"
1076
+ + (f"PRIOR AGENT OUTPUTS:\n{prior}" if prior else "")},
1077
+ ],
1078
+ agent_s,
1079
+ )
1080
+ artifacts.append({"agent": role, "task": task["task"], "output": out})
1081
+ await ws_send(ws, {"type": "agent", "agent": role, "status": "done", "content": out})
1082
+
1083
+ # -- Final compilation ------------------------------------------------------------
1084
+ await ws_send(ws, {"type": "status", "content": "Compiling the swarm's final deliverable…"})
1085
+ compiled_context = "\n\n".join(
1086
+ f"===== AGENT: {a['agent']} (task: {a['task']}) =====\n{a['output']}" for a in artifacts
1087
+ )
1088
+ final_msgs = [
1089
+ {"role": "system", "content":
1090
+ "You are the lead integrator of an AI agent swarm. Compile all agent outputs "
1091
+ "into ONE coherent final deliverable: a short executive summary followed by the "
1092
+ "complete artifacts (code files in fenced blocks with `# path:` headers). "
1093
+ "Do not lose any code."},
1094
+ {"role": "user", "content": f"MISSION:\n{user_q}\n\n{compiled_context[:20000]}"},
1095
+ ]
1096
+ acc = ""
1097
+ async for piece in LLM.astream(final_msgs, s):
1098
+ acc += piece
1099
+ if not await ws_send(ws, {"type": "token", "content": piece}):
1100
+ break
1101
+
1102
+ # -- Persist the project snapshot ---------------------------------------------------
1103
+ try:
1104
+ files = dict(re.findall(r"```[\w+-]*\n#\s*path:\s*(\S+)\n(.*?)```", acc, re.S))
1105
+ await DB.execute(
1106
+ "INSERT INTO sandbox_projects(id, name, files_json, updated_at) VALUES(?,?,?,datetime('now')) "
1107
+ "ON CONFLICT(name) DO UPDATE SET files_json=excluded.files_json, updated_at=datetime('now')",
1108
+ (new_id(), f"mission-{int(time.time())}", json.dumps(files, ensure_ascii=False)),
1109
+ )
1110
+ except Exception as exc:
1111
+ log.warning("sandbox project persist failed: %s", exc)
1112
+ return acc
1113
+
1114
+ # ============================================================================
1115
+ # 15. FASTAPI APPLICATION
1116
+ # ============================================================================
1117
+ app = FastAPI(title=CFG.APP_NAME, version=CFG.VERSION, docs_url=None, redoc_url=None)
1118
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
1119
+
1120
+ METRICS_CLIENTS: set = set()
1121
+ APP_START_TS = time.time()
1122
+
1123
+
1124
+ # ---------------------------------------------------------------------------
1125
+ # Middleware: hard-block any direct .html access without a valid session
1126
+ # ---------------------------------------------------------------------------
1127
+ @app.middleware("http")
1128
+ async def html_guard(request: Request, call_next):
1129
+ path = request.url.path
1130
+ if path.startswith("/static/") and path.endswith(".html") and not path.endswith("lockscreen.html"):
1131
+ token = token_from_request(request)
1132
+ if not token or not decode_token(token):
1133
+ return FileResponse(str(STATIC_DIR / "lockscreen.html"), status_code=200)
1134
+ return await call_next(request)
1135
+
1136
+
1137
+ # ---------------------------------------------------------------------------
1138
+ # Page serving (every page requires a valid session cookie/token)
1139
+ # ---------------------------------------------------------------------------
1140
+ def _page_or_lock(filename: str, request: Request):
1141
+ token = token_from_request(request)
1142
+ if token and decode_token(token):
1143
+ return FileResponse(str(STATIC_DIR / filename))
1144
+ return FileResponse(str(STATIC_DIR / "lockscreen.html"))
1145
+
1146
+
1147
+ @app.get("/", response_class=HTMLResponse)
1148
+ async def page_index(request: Request):
1149
+ return _page_or_lock("index.html", request)
1150
+
1151
+
1152
+ @app.get("/usage", response_class=HTMLResponse)
1153
+ async def page_usage(request: Request):
1154
+ return _page_or_lock("usage.html", request)
1155
+
1156
+
1157
+ @app.get("/logs", response_class=HTMLResponse)
1158
+ async def page_logs(request: Request):
1159
+ return _page_or_lock("logs.html", request)
1160
+
1161
+
1162
+ @app.get("/console", response_class=HTMLResponse)
1163
+ async def page_console(request: Request):
1164
+ return _page_or_lock("console.html", request)
1165
+
1166
+
1167
+ # ---------------------------------------------------------------------------
1168
+ # Auth API
1169
+ # ---------------------------------------------------------------------------
1170
+ @app.post("/api/auth/verify")
1171
+ async def auth_verify(body: AuthVerifyIn, request: Request):
1172
+ ip = client_ip(request)
1173
+ ua = request.headers.get("user-agent", "")[:300]
1174
+ ok_token = hmac.compare_digest(body.token.strip(), CFG.ADMIN_API_KEY)
1175
+ geo_ok, geo_reason = geo_verdict(body.latitude, body.longitude)
1176
+ success = ok_token and geo_ok
1177
+ reason = "ok" if success else ("bad master token" if not ok_token else geo_reason)
1178
+
1179
+ try:
1180
+ await DB.execute(
1181
+ "INSERT INTO auth_audit(ip, latitude, longitude, success, reason, user_agent) "
1182
+ "VALUES(?,?,?,?,?,?)",
1183
+ (ip, body.latitude, body.longitude, 1 if success else 0, reason, ua),
1184
+ )
1185
+ except Exception as exc:
1186
+ log.warning("auth audit insert failed: %s", exc)
1187
+
1188
+ if not success:
1189
+ log.warning("AUTH FAIL ip=%s reason=%s", ip, reason)
1190
+ raise HTTPException(status_code=403, detail=f"access denied: {reason}")
1191
+
1192
+ token = issue_token(ip)
1193
+ log.info("AUTH OK ip=%s geo=%s", ip, geo_reason)
1194
+ resp = JSONResponse(
1195
+ {
1196
+ "ok": True,
1197
+ "token": token,
1198
+ "expires_in_hours": CFG.JWT_TTL_HOURS,
1199
+ "geo": geo_reason,
1200
+ "ip": ip,
1201
+ }
1202
+ )
1203
+ resp.set_cookie(
1204
+ CFG.SESSION_COOKIE, token,
1205
+ max_age=CFG.JWT_TTL_HOURS * 3600, httponly=False, samesite="lax",
1206
+ )
1207
+ return resp
1208
+
1209
+
1210
+ @app.get("/api/auth/session")
1211
+ async def auth_session(session: Dict[str, Any] = Depends(require_session)):
1212
+ return {
1213
+ "ok": True,
1214
+ "sub": session.get("sub"),
1215
+ "ip": session.get("ip"),
1216
+ "exp": session.get("exp"),
1217
+ "model": CFG.MODEL_NAME,
1218
+ "mock_mode": CFG.MOCK_MODE,
1219
+ "db": DB.backend,
1220
+ "vllm": await LLM.healthy(),
1221
+ "version": CFG.VERSION,
1222
+ }
1223
+
1224
+
1225
+ @app.post("/api/auth/logout")
1226
+ async def auth_logout():
1227
+ resp = JSONResponse({"ok": True})
1228
+ resp.delete_cookie(CFG.SESSION_COOKIE)
1229
+ return resp
1230
+
1231
+
1232
+ # ---------------------------------------------------------------------------
1233
+ # Health / info
1234
+ # ---------------------------------------------------------------------------
1235
+ @app.get("/api/health")
1236
+ async def health():
1237
+ return {
1238
+ "status": "ok",
1239
+ "app": CFG.APP_NAME,
1240
+ "version": CFG.VERSION,
1241
+ "uptime_s": round(time.time() - APP_START_TS, 1),
1242
+ "db": DB.backend,
1243
+ "vllm": await LLM.healthy(),
1244
+ "mock_mode": CFG.MOCK_MODE,
1245
+ }
1246
+
1247
+
1248
+ # ---------------------------------------------------------------------------
1249
+ # Chat persistence API
1250
+ # ---------------------------------------------------------------------------
1251
+ class ChatCreateIn(BaseModel):
1252
+ title: str = Field("New chat", max_length=200)
1253
+ mode: str = Field("flash", max_length=20)
1254
+
1255
+
1256
+ @app.get("/api/chats")
1257
+ async def list_chats(session: Dict[str, Any] = Depends(require_session)):
1258
+ return await DB.query(
1259
+ "SELECT id, title, mode, created_at, updated_at FROM chats "
1260
+ "ORDER BY updated_at DESC LIMIT 100"
1261
+ )
1262
+
1263
+
1264
+ @app.post("/api/chats")
1265
+ async def create_chat(body: ChatCreateIn, session: Dict[str, Any] = Depends(require_session)):
1266
+ cid = new_id()
1267
+ await DB.execute(
1268
+ "INSERT INTO chats(id, user_id, title, mode) VALUES(?,?,?,?)",
1269
+ (cid, session.get("sub", "admin"), body.title.strip() or "New chat", body.mode),
1270
+ )
1271
+ return {"id": cid, "title": body.title, "mode": body.mode}
1272
+
1273
+
1274
+ @app.get("/api/chats/{chat_id}/messages")
1275
+ async def chat_messages(chat_id: str, session: Dict[str, Any] = Depends(require_session)):
1276
+ return await DB.query(
1277
+ "SELECT id, role, content, mode, tokens, created_at FROM messages "
1278
+ "WHERE chat_id=? ORDER BY created_at ASC, rowid ASC LIMIT 500",
1279
+ (chat_id,),
1280
+ )
1281
+
1282
+
1283
+ @app.delete("/api/chats/{chat_id}")
1284
+ async def delete_chat(chat_id: str, session: Dict[str, Any] = Depends(require_session)):
1285
+ await DB.execute("DELETE FROM messages WHERE chat_id=?", (chat_id,))
1286
+ await DB.execute("DELETE FROM chats WHERE id=?", (chat_id,))
1287
+ return {"ok": True}
1288
+
1289
+
1290
+ @app.patch("/api/chats/{chat_id}")
1291
+ async def rename_chat(chat_id: str, body: ChatCreateIn, session: Dict[str, Any] = Depends(require_session)):
1292
+ await DB.execute(
1293
+ "UPDATE chats SET title=?, updated_at=datetime('now') WHERE id=?",
1294
+ (body.title.strip() or "New chat", chat_id),
1295
+ )
1296
+ return {"ok": True}
1297
+
1298
+
1299
+ # ---------------------------------------------------------------------------
1300
+ # Memory API (Turso-backed long-term memory)
1301
+ # ---------------------------------------------------------------------------
1302
+ class MemoryIn(BaseModel):
1303
+ key: str = Field(..., min_length=1, max_length=120)
1304
+ value: str = Field(..., min_length=1, max_length=4000)
1305
+
1306
+
1307
+ @app.get("/api/memory")
1308
+ async def list_memory(session: Dict[str, Any] = Depends(require_session)):
1309
+ return await DB.query("SELECT id, key, value, updated_at FROM memories ORDER BY updated_at DESC LIMIT 100")
1310
+
1311
+
1312
+ @app.post("/api/memory")
1313
+ async def upsert_memory(body: MemoryIn, session: Dict[str, Any] = Depends(require_session)):
1314
+ await DB.execute(
1315
+ "INSERT INTO memories(id, key, value, updated_at) VALUES(?,?,?,datetime('now')) "
1316
+ "ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=datetime('now')",
1317
+ (new_id(), body.key.strip(), body.value.strip()),
1318
+ )
1319
+ return {"ok": True}
1320
+
1321
+
1322
+ @app.delete("/api/memory/{key}")
1323
+ async def delete_memory(key: str, session: Dict[str, Any] = Depends(require_session)):
1324
+ await DB.execute("DELETE FROM memories WHERE key=?", (key,))
1325
+ return {"ok": True}
1326
+
1327
+
1328
+ # ---------------------------------------------------------------------------
1329
+ # Logs API + AI diagnostics + self-healing patch
1330
+ # ---------------------------------------------------------------------------
1331
+ @app.get("/api/logs")
1332
+ async def get_logs(
1333
+ limit: int = Query(300, ge=1, le=2000),
1334
+ level: str = Query("", max_length=10),
1335
+ session: Dict[str, Any] = Depends(require_session),
1336
+ ):
1337
+ entries = list(LOG_BUFFER)
1338
+ if level:
1339
+ lv = level.upper()
1340
+ entries = [e for e in entries if e["level"] == lv]
1341
+ return entries[-limit:]
1342
+
1343
+
1344
+ class LogAnalyzeIn(BaseModel):
1345
+ focus: str = Field("", max_length=1000)
1346
+
1347
+
1348
+ @app.post("/api/logs/analyze")
1349
+ async def analyze_logs(body: LogAnalyzeIn, session: Dict[str, Any] = Depends(require_session)):
1350
+ entries = [e for e in LOG_BUFFER if e["level"] in ("ERROR", "WARNING", "CRITICAL")][-80:]
1351
+ if not entries:
1352
+ entries = list(LOG_BUFFER)[-40:]
1353
+ blob = "\n".join(f"[{e['ts']}] {e['level']} {e['logger']}: {e['message']}" for e in entries)
1354
+ analysis = await LLM.acomplete(
1355
+ [
1356
+ {"role": "system", "content":
1357
+ "You are the NEXUS self-healing diagnostics agent. Analyze the system logs "
1358
+ "below. Identify root causes, then propose concrete fixes (Python, Docker, "
1359
+ "or configuration). If a fix requires rewriting a file, output it in a fenced "
1360
+ "code block whose first line is `# path: <absolute path>`. Be surgical."
1361
+ + (f"\nUser focus: {body.focus}" if body.focus else "")},
1362
+ {"role": "user", "content": blob[:20000]},
1363
+ ],
1364
+ {"max_tokens": 1500, "temperature": 0.3, "model": CFG.MODEL_NAME},
1365
+ )
1366
+ return {"analyzed_entries": len(entries), "analysis": analysis}
1367
+
1368
+
1369
+ class PatchIn(BaseModel):
1370
+ path: str = Field(..., min_length=1, max_length=500)
1371
+ content: str = Field(..., max_length=400000)
1372
+ confirm: bool = False
1373
+
1374
+
1375
+ ALLOWED_PATCH_ROOTS = (APP_ROOT, SANDBOX_DIR)
1376
+
1377
+
1378
+ def _resolve_jailed(path: str, roots=ALLOWED_PATCH_ROOTS) -> Path:
1379
+ p = Path(path).resolve()
1380
+ if not any(str(p).startswith(str(r)) for r in roots):
1381
+ raise HTTPException(status_code=403, detail="path outside allowed roots")
1382
+ return p
1383
+
1384
+
1385
+ @app.post("/api/logs/apply-patch")
1386
+ async def apply_patch(body: PatchIn, session: Dict[str, Any] = Depends(require_session)):
1387
+ if not body.confirm:
1388
+ raise HTTPException(status_code=400, detail="set confirm=true to apply the patch")
1389
+ target = _resolve_jailed(body.path)
1390
+ if not target.exists():
1391
+ raise HTTPException(status_code=404, detail="target file does not exist")
1392
+ backup = target.with_suffix(target.suffix + f".bak.{int(time.time())}")
1393
+ shutil.copy2(target, backup)
1394
+ target.write_text(body.content, encoding="utf-8")
1395
+ log.warning("SELF-HEAL patch applied to %s (backup: %s)", target, backup)
1396
+ return {"ok": True, "patched": str(target), "backup": str(backup)}
1397
+
1398
+
1399
+ @app.get("/api/logs/read-file")
1400
+ async def read_file_for_patch(path: str, session: Dict[str, Any] = Depends(require_session)):
1401
+ target = _resolve_jailed(path)
1402
+ if not target.is_file():
1403
+ raise HTTPException(status_code=404, detail="not a file")
1404
+ return {"path": str(target), "content": target.read_text(encoding="utf-8", errors="replace")[:400000]}
1405
+
1406
+
1407
+ # ---------------------------------------------------------------------------
1408
+ # Sandbox file manager + uploads (vision / OCR / PDF / code parsing)
1409
+ # ---------------------------------------------------------------------------
1410
+ TEXT_EXTS = {".py", ".js", ".ts", ".html", ".css", ".json", ".md", ".txt", ".sql",
1411
+ ".csv", ".log", ".sh", ".yml", ".yaml", ".toml", ".env", ".cfg", ".xml"}
1412
+ IMG_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"}
1413
+
1414
+
1415
+ @app.get("/api/sandbox/tree")
1416
+ async def sandbox_tree(session: Dict[str, Any] = Depends(require_session)):
1417
+ tree: List[Dict[str, Any]] = []
1418
+ for p in sorted(SANDBOX_DIR.rglob("*")):
1419
+ if p.is_file() and len(tree) < 500:
1420
+ tree.append({
1421
+ "path": str(p.relative_to(SANDBOX_DIR)),
1422
+ "size": p.stat().st_size,
1423
+ "modified": datetime.fromtimestamp(p.stat().st_mtime, timezone.utc).isoformat(timespec="seconds"),
1424
+ })
1425
+ return tree
1426
+
1427
+
1428
+ class SandboxFileIn(BaseModel):
1429
+ path: str = Field(..., min_length=1, max_length=300)
1430
+ content: str = Field("", max_length=400000)
1431
+
1432
+
1433
+ @app.get("/api/sandbox/file")
1434
+ async def sandbox_read(path: str, session: Dict[str, Any] = Depends(require_session)):
1435
+ target = _resolve_jailed(str(SANDBOX_DIR / path), roots=(SANDBOX_DIR,))
1436
+ if not target.is_file():
1437
+ raise HTTPException(status_code=404, detail="not a file")
1438
+ return {"path": path, "content": target.read_text(encoding="utf-8", errors="replace")[:400000]}
1439
+
1440
+
1441
+ @app.post("/api/sandbox/file")
1442
+ async def sandbox_write(body: SandboxFileIn, session: Dict[str, Any] = Depends(require_session)):
1443
+ target = _resolve_jailed(str(SANDBOX_DIR / body.path), roots=(SANDBOX_DIR,))
1444
+ target.parent.mkdir(parents=True, exist_ok=True)
1445
+ target.write_text(body.content, encoding="utf-8")
1446
+ log.info("sandbox file written: %s (%d bytes)", target, len(body.content))
1447
+ return {"ok": True, "path": str(target.relative_to(SANDBOX_DIR))}
1448
+
1449
+
1450
+ @app.post("/api/upload")
1451
+ async def upload_file(file: UploadFile = File(...), session: Dict[str, Any] = Depends(require_session)):
1452
+ safe_name = re.sub(r"[^A-Za-z0-9._-]+", "_", file.filename or "upload.bin")[:120]
1453
+ dest = UPLOAD_DIR / f"{int(time.time())}_{safe_name}"
1454
+ data = await file.read()
1455
+ if len(data) > 40 * 1024 * 1024:
1456
+ raise HTTPException(status_code=413, detail="file too large (40 MB max)")
1457
+ dest.write_bytes(data)
1458
+ ext = dest.suffix.lower()
1459
+ kind, preview = "binary", ""
1460
+
1461
+ try:
1462
+ if ext in TEXT_EXTS:
1463
+ kind = "text"
1464
+ preview = data.decode("utf-8", "replace")[:30000]
1465
+ elif ext == ".pdf" and PdfReader is not None:
1466
+ kind = "pdf"
1467
+ reader = PdfReader(str(dest))
1468
+ preview = "\n".join((pg.extract_text() or "") for pg in reader.pages[:20])[:30000]
1469
+ elif ext in IMG_EXTS:
1470
+ kind = "image"
1471
+ if pytesseract is not None and Image is not None:
1472
+ try:
1473
+ preview = pytesseract.image_to_string(Image.open(dest))[:10000]
1474
+ kind = "image+ocr"
1475
+ except Exception:
1476
+ preview = ""
1477
+ except Exception as exc:
1478
+ log.warning("upload parse failed (%s): %s", safe_name, exc)
1479
+
1480
+ log.info("upload: %s kind=%s size=%d", safe_name, kind, len(data))
1481
+ return {
1482
+ "ok": True, "filename": safe_name, "kind": kind,
1483
+ "size": len(data), "stored": str(dest.relative_to(SANDBOX_DIR)),
1484
+ "preview": preview,
1485
+ }
1486
+
1487
+
1488
+ # ---------------------------------------------------------------------------
1489
+ # Metrics snapshot + broadcaster
1490
+ # ---------------------------------------------------------------------------
1491
+ def _gpu_stats() -> List[Dict[str, Any]]:
1492
+ if GPUtil is None:
1493
+ return []
1494
+ try:
1495
+ return [
1496
+ {
1497
+ "name": g.name, "load": round(g.load * 100, 1),
1498
+ "mem_used_mb": round(g.memoryUsed, 1), "mem_total_mb": round(g.memoryTotal, 1),
1499
+ "mem_percent": round(g.memoryUtil * 100, 1),
1500
+ "temperature": round(g.temperature, 1),
1501
+ }
1502
+ for g in GPUtil.getGPUs()
1503
+ ]
1504
+ except Exception:
1505
+ return []
1506
+
1507
+
1508
+ async def metrics_snapshot() -> Dict[str, Any]:
1509
+ vm = psutil.virtual_memory()
1510
+ proc = psutil.Process()
1511
+ return {
1512
+ "ts": utcnow(),
1513
+ "cpu_percent": psutil.cpu_percent(interval=None),
1514
+ "cpu_count": psutil.cpu_count(logical=True),
1515
+ "ram": {"total_gb": round(vm.total / 1e9, 1), "used_gb": round(vm.used / 1e9, 1),
1516
+ "percent": vm.percent},
1517
+ "gpu": _gpu_stats(),
1518
+ "threads": proc.num_threads(),
1519
+ "proc_mem_mb": round(proc.memory_info().rss / 1e6, 1),
1520
+ "tokens_per_sec": round(METER.rate(), 1),
1521
+ "total_tokens": METER.total,
1522
+ "uptime_s": round(time.time() - APP_START_TS, 0),
1523
+ "vllm": await LLM.healthy(),
1524
+ "db": DB.backend,
1525
+ "mock_mode": CFG.MOCK_MODE,
1526
+ "model": CFG.MODEL_NAME,
1527
+ }
1528
+
1529
+
1530
+ @app.get("/api/metrics")
1531
+ async def metrics_http(session: Dict[str, Any] = Depends(require_session)):
1532
+ return await metrics_snapshot()
1533
+
1534
+
1535
+ async def metrics_broadcaster() -> None:
1536
+ psutil.cpu_percent(interval=None) # prime the counter
1537
+ while True:
1538
+ try:
1539
+ if METRICS_CLIENTS:
1540
+ snap = await metrics_snapshot()
1541
+ msg = json.dumps(snap)
1542
+ dead = []
1543
+ for ws in list(METRICS_CLIENTS):
1544
+ try:
1545
+ await ws.send_text(msg)
1546
+ except Exception:
1547
+ dead.append(ws)
1548
+ for ws in dead:
1549
+ METRICS_CLIENTS.discard(ws)
1550
+ except Exception as exc:
1551
+ log.warning("metrics broadcast error: %s", exc)
1552
+ await asyncio.sleep(2)
1553
+
1554
+
1555
+ @app.websocket("/ws/metrics")
1556
+ async def ws_metrics(ws: WebSocket):
1557
+ if not ws_session(ws):
1558
+ await ws.close(code=4401)
1559
+ return
1560
+ await ws.accept()
1561
+ METRICS_CLIENTS.add(ws)
1562
+ try:
1563
+ await ws.send_text(json.dumps(await metrics_snapshot()))
1564
+ while True:
1565
+ await ws.receive_text() # keepalive pings from the client
1566
+ except WebSocketDisconnect:
1567
+ pass
1568
+ except Exception:
1569
+ pass
1570
+ finally:
1571
+ METRICS_CLIENTS.discard(ws)
1572
+
1573
+ # ---------------------------------------------------------------------------
1574
+ # Chat WebSocket — one connection per generation, JSON event protocol
1575
+ # ---------------------------------------------------------------------------
1576
+ async def _persist_chat_turn(chat_id: str, mode: str, user_text: str,
1577
+ assistant_text: str, title_hint: str) -> None:
1578
+ try:
1579
+ exists = await DB.query_one("SELECT id, title FROM chats WHERE id=?", (chat_id,))
1580
+ if not exists:
1581
+ await DB.execute(
1582
+ "INSERT INTO chats(id, user_id, title, mode) VALUES(?,?,?,?)",
1583
+ (chat_id, "nexus-admin", (title_hint[:80] or "New chat"), mode),
1584
+ )
1585
+ elif exists.get("title") == "New chat" and title_hint:
1586
+ await DB.execute("UPDATE chats SET title=? WHERE id=?", (title_hint[:80], chat_id))
1587
+ await DB.execute(
1588
+ "INSERT INTO messages(id, chat_id, role, content, mode, tokens) VALUES(?,?,?,?,?,?)",
1589
+ (new_id(), chat_id, "user", user_text, mode, approx_tokens(user_text)),
1590
+ )
1591
+ await DB.execute(
1592
+ "INSERT INTO messages(id, chat_id, role, content, mode, tokens) VALUES(?,?,?,?,?,?)",
1593
+ (new_id(), chat_id, "assistant", assistant_text, mode, approx_tokens(assistant_text)),
1594
+ )
1595
+ await DB.execute("UPDATE chats SET updated_at=datetime('now'), mode=? WHERE id=?",
1596
+ (mode, chat_id))
1597
+ except Exception as exc:
1598
+ log.warning("chat persist failed: %s", exc)
1599
+
1600
+
1601
+ @app.websocket("/ws/chat")
1602
+ async def ws_chat(ws: WebSocket):
1603
+ session = ws_session(ws)
1604
+ if not session:
1605
+ await ws.close(code=4401)
1606
+ return
1607
+ await ws.accept()
1608
+ log.info("chat ws connected (%s)", session.get("sub"))
1609
+
1610
+ try:
1611
+ raw = await ws.receive_text()
1612
+ payload = json.loads(raw)
1613
+ mode = str(payload.get("mode", "flash")).lower()
1614
+ s: Dict[str, Any] = payload.get("settings") or {}
1615
+ profile = str(s.get("safety_profile", "balanced"))
1616
+
1617
+ # ---- Safety gate on the incoming user turn --------------------------
1618
+ user_text = ""
1619
+ for m in reversed(payload.get("messages", [])):
1620
+ if m.get("role") == "user":
1621
+ user_text = str(m.get("content", ""))
1622
+ break
1623
+ ok_in, violations, sanitized = SAFETY.scan(user_text, profile)
1624
+ if not ok_in:
1625
+ await ws_send(ws, {"type": "error",
1626
+ "content": f"🛡️ Blocked by the **{profile}** safety profile: "
1627
+ f"{', '.join(violations)}"})
1628
+ await ws_send(ws, {"type": "done", "chat_id": None, "usage": {}})
1629
+ await ws.close()
1630
+ return
1631
+ if sanitized != user_text:
1632
+ for m in payload.get("messages", []):
1633
+ if m.get("role") == "user" and m.get("content") == user_text:
1634
+ m["content"] = sanitized
1635
+ await ws_send(ws, {"type": "status", "content": "PII redacted by safety profile."})
1636
+
1637
+ await ws_send(ws, {"type": "status", "content": f"Engine `{mode}` engaged…"})
1638
+
1639
+ # ---- Route to the selected engine ------------------------------------
1640
+ sources: List[Dict[str, str]] = []
1641
+ if mode == "max":
1642
+ answer, sources = await engine_max(ws, payload, s)
1643
+ elif mode == "agents":
1644
+ answer = await engine_agents(ws, payload, s)
1645
+ else:
1646
+ mode = "flash"
1647
+ answer = await engine_flash(ws, payload, s)
1648
+
1649
+ # ---- Safety gate on the outgoing answer --------------------------------
1650
+ ok_out, out_violations, clean_answer = SAFETY.scan(answer, profile)
1651
+ if not ok_out:
1652
+ await ws_send(ws, {"type": "replace",
1653
+ "content": f"🛡️ The generated answer was withheld by the "
1654
+ f"**{profile}** safety profile ({', '.join(out_violations)})."})
1655
+ answer = ""
1656
+ elif clean_answer != answer:
1657
+ answer = clean_answer
1658
+ await ws_send(ws, {"type": "replace", "content": answer})
1659
+
1660
+ # ---- Persist + finish ------------------------------------------------------
1661
+ chat_id = str(payload.get("chat_id") or new_id())
1662
+ await _persist_chat_turn(chat_id, mode, user_text, answer, user_text[:80])
1663
+ await ws_send(ws, {
1664
+ "type": "done",
1665
+ "chat_id": chat_id,
1666
+ "mode": mode,
1667
+ "citations": sources,
1668
+ "usage": {
1669
+ "completion_tokens_est": approx_tokens(answer),
1670
+ "session_total_tokens": METER.total,
1671
+ "tokens_per_sec": round(METER.rate(), 1),
1672
+ },
1673
+ })
1674
+ except WebSocketDisconnect:
1675
+ log.info("chat ws disconnected mid-generation")
1676
+ except json.JSONDecodeError:
1677
+ await ws_send(ws, {"type": "error", "content": "Malformed payload (expected JSON)."})
1678
+ except Exception as exc:
1679
+ log.error("chat ws error: %s\n%s", exc, traceback.format_exc())
1680
+ await ws_send(ws, {"type": "error", "content": f"Internal error: {exc}"})
1681
+ finally:
1682
+ try:
1683
+ await ws.close()
1684
+ except Exception:
1685
+ pass
1686
+
1687
+
1688
+ # ---------------------------------------------------------------------------
1689
+ # Terminal WebSocket — jailed bash emulator
1690
+ # ---------------------------------------------------------------------------
1691
+ TERMINAL_BLOCKLIST = re.compile(
1692
+ r"(rm\s+-[rf]{1,2}\s+/(?:\s|$)|\bmkfs\b|\bdd\s+if=|\bshutdown\b|\breboot\b"
1693
+ r"|\bhalt\b|\bpoweroff\b|:\(\)\s*\{|\bfork\b\s*\|\s*\bfork\b"
1694
+ r"|>\s*/dev/sd|chmod\s+-R\s+777\s+/)",
1695
+ re.IGNORECASE,
1696
+ )
1697
+
1698
+ SECRET_ENV_KEYS = ("TURSO_AUTH_TOKEN", "ADMIN_API_KEY", "JWT_SECRET", "HF_TOKEN")
1699
+
1700
+
1701
+ def _terminal_env() -> Dict[str, str]:
1702
+ env = dict(os.environ)
1703
+ for k in list(env):
1704
+ if any(sec in k for sec in SECRET_ENV_KEYS):
1705
+ env.pop(k, None)
1706
+ env["PS1"] = ""
1707
+ env["TERM"] = "xterm-256color"
1708
+ return env
1709
+
1710
+
1711
+ def _clamp_cwd(path: Path) -> Path:
1712
+ try:
1713
+ resolved = path.resolve()
1714
+ resolved.relative_to(SANDBOX_DIR)
1715
+ return resolved
1716
+ except Exception:
1717
+ return SANDBOX_DIR
1718
+
1719
+
1720
+ @app.websocket("/ws/terminal")
1721
+ async def ws_terminal(ws: WebSocket):
1722
+ if not ws_session(ws):
1723
+ await ws.close(code=4401)
1724
+ return
1725
+ await ws.accept()
1726
+ cwd = SANDBOX_DIR
1727
+ env = _terminal_env()
1728
+ await ws_send(ws, {"type": "banner",
1729
+ "content": f"NEXUS secure shell — jailed to {SANDBOX_DIR}\n"
1730
+ f"Type commands. `cd` is supported. Ctrl-D or `exit` closes.\r\n"})
1731
+ try:
1732
+ while True:
1733
+ data = json.loads(await ws.receive_text())
1734
+ cmd = str(data.get("cmd", "")).rstrip()
1735
+ if not cmd:
1736
+ continue
1737
+ if cmd.strip() in ("exit", "logout"):
1738
+ await ws_send(ws, {"type": "exit", "code": 0})
1739
+ break
1740
+ if TERMINAL_BLOCKLIST.search(cmd):
1741
+ await ws_send(ws, {"type": "output",
1742
+ "data": f"\r\n\x1b[31m[shield] command blocked for safety: {cmd}\x1b[0m\r\n"})
1743
+ await ws_send(ws, {"type": "exit", "code": 126, "cwd": str(cwd)})
1744
+ continue
1745
+
1746
+ # ---- built-in cd (persistent per connection) -----------------------
1747
+ m = re.match(r"^\s*cd(?:\s+(.*))?$", cmd)
1748
+ if m:
1749
+ target = (m.group(1) or "").strip().strip('"').strip("'")
1750
+ new_cwd = _clamp_cwd((cwd / target) if target else SANDBOX_DIR)
1751
+ if new_cwd.is_dir():
1752
+ cwd = new_cwd
1753
+ await ws_send(ws, {"type": "exit", "code": 0, "cwd": str(cwd)})
1754
+ else:
1755
+ await ws_send(ws, {"type": "output",
1756
+ "data": f"\r\ncd: no such directory: {target}\r\n"})
1757
+ await ws_send(ws, {"type": "exit", "code": 1, "cwd": str(cwd)})
1758
+ continue
1759
+
1760
+ # ---- execute ----------------------------------------------------------
1761
+ log.info("terminal exec: %s (cwd=%s)", cmd[:200], cwd)
1762
+ try:
1763
+ proc = await asyncio.create_subprocess_shell(
1764
+ cmd,
1765
+ stdout=asyncio.subprocess.PIPE,
1766
+ stderr=asyncio.subprocess.STDOUT,
1767
+ cwd=str(cwd),
1768
+ env=env,
1769
+ limit=CFG.TERMINAL_MAX_OUTPUT,
1770
+ )
1771
+ except Exception as exc:
1772
+ await ws_send(ws, {"type": "output", "data": f"\r\nspawn failed: {exc}\r\n"})
1773
+ await ws_send(ws, {"type": "exit", "code": 127, "cwd": str(cwd)})
1774
+ continue
1775
+
1776
+ produced = 0
1777
+ timed_out = False
1778
+ start = time.time()
1779
+ assert proc.stdout is not None
1780
+ while True:
1781
+ if time.time() - start > CFG.TERMINAL_TIMEOUT_S:
1782
+ timed_out = True
1783
+ try:
1784
+ proc.kill()
1785
+ except Exception:
1786
+ pass
1787
+ break
1788
+ try:
1789
+ chunk = await asyncio.wait_for(proc.stdout.read(4096), timeout=1.0)
1790
+ except asyncio.TimeoutError:
1791
+ if proc.returncode is not None:
1792
+ break
1793
+ continue
1794
+ if not chunk:
1795
+ break
1796
+ produced += len(chunk)
1797
+ if produced > CFG.TERMINAL_MAX_OUTPUT:
1798
+ await ws_send(ws, {"type": "output",
1799
+ "data": "\r\n\x1b[33m[truncated: output limit reached]\x1b[0m\r\n"})
1800
+ try:
1801
+ proc.kill()
1802
+ except Exception:
1803
+ pass
1804
+ break
1805
+ await ws_send(ws, {"type": "output",
1806
+ "data": chunk.decode("utf-8", "replace")})
1807
+ try:
1808
+ code = await asyncio.wait_for(proc.wait(), timeout=5)
1809
+ except Exception:
1810
+ code = proc.returncode if proc.returncode is not None else -9
1811
+ if timed_out:
1812
+ await ws_send(ws, {"type": "output",
1813
+ "data": f"\r\n\x1b[33m[killed: timeout {CFG.TERMINAL_TIMEOUT_S}s]\x1b[0m\r\n"})
1814
+ code = 124
1815
+ await ws_send(ws, {"type": "exit", "code": code, "cwd": str(cwd)})
1816
+ except WebSocketDisconnect:
1817
+ pass
1818
+ except Exception as exc:
1819
+ log.error("terminal ws error: %s", exc)
1820
+ finally:
1821
+ try:
1822
+ await ws.close()
1823
+ except Exception:
1824
+ pass
1825
+
1826
+
1827
+ # ---------------------------------------------------------------------------
1828
+ # Startup / shutdown
1829
+ # ---------------------------------------------------------------------------
1830
+ @app.on_event("startup")
1831
+ async def on_startup() -> None:
1832
+ log.info("=" * 70)
1833
+ log.info(" %s v%s booting…", CFG.APP_NAME, CFG.VERSION)
1834
+ log.info(" model=%s mock=%s vllm=%s", CFG.MODEL_NAME, CFG.MOCK_MODE, CFG.VLLM_URL)
1835
+ if _AUTO_KEY:
1836
+ log.warning("!" * 70)
1837
+ log.warning("ADMIN_API_KEY was NOT set — generated an ephemeral master key:")
1838
+ log.warning(" >>> %s <<<", CFG.ADMIN_API_KEY)
1839
+ log.warning("Set the ADMIN_API_KEY secret for a stable credential.")
1840
+ log.warning("!" * 70)
1841
+ await DB.connect()
1842
+ asyncio.create_task(metrics_broadcaster())
1843
+ log.info("startup complete — serving on port %d", CFG.PORT)
1844
+
1845
+
1846
+ @app.on_event("shutdown")
1847
+ async def on_shutdown() -> None:
1848
+ log.info("shutting down…")
1849
+ DB.close()
1850
+
1851
+
1852
+ if __name__ == "__main__":
1853
+ import uvicorn
1854
+ uvicorn.run("main:app", host="0.0.0.0", port=CFG.PORT, log_level="info")
package-lock.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "name": "app",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
requirements.txt ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================================
2
+ # NEXUS AI — Python dependencies
3
+ # ============================================================================
4
+
5
+ # ---- Web framework / ASGI server ------------------------------------------
6
+ fastapi>=0.110.0,<1.0.0
7
+ uvicorn[standard]>=0.29.0
8
+ websockets>=12.0
9
+ python-multipart>=0.0.9 # file uploads (vision / code parsing)
10
+
11
+ # ---- Local LLM inference (OpenAI-compatible server, A100 optimized) -------
12
+ vllm>=0.6.3,<0.8.0 ; platform_system == "Linux"
13
+
14
+ # ---- Turso / libSQL database (falls back to local file when unset) --------
15
+ libsql-client>=0.3.1
16
+
17
+ # ---- Security --------------------------------------------------------------
18
+ pyjwt>=2.8.0
19
+ cryptography>=42.0.0
20
+
21
+ # ---- System monitoring -----------------------------------------------------
22
+ psutil>=5.9.8
23
+ GPUtil>=1.4.0
24
+
25
+ # ---- Web research (Max engine) --------------------------------------------
26
+ requests>=2.31.0
27
+ beautifulsoup4>=4.12.3
28
+ lxml>=5.1.0
29
+
30
+ # ---- File understanding (uploads) -----------------------------------------
31
+ pypdf>=4.2.0
32
+ pytesseract>=0.3.10
33
+ Pillow>=10.2.0
34
+
35
+ # ---- Validation ------------------------------------------------------------
36
+ pydantic>=2.6.0