Aswin480
fix(devices): add genuine scannable QR code endpoint and wire JWT auth to device actions
21dbbb4 Download apps/api/main.py from sui234as/llm: direct link, hf CLI and curl.
- Browser
- Download file 21.9 kB
-
https://huggingface.co/spaces/sui234as/llm/resolve/main/apps/api/main.py
- Command line
-
hf download hf://spaces/sui234as/llm/apps/api/main.py
-
curl -L -o main.py https://huggingface.co/spaces/sui234as/llm/resolve/main/apps/api/main.py
21.9 kB
| """ | |
| Personal Assistant (my-local-bot) - Primary API Entrypoint. | |
| Single-process FastAPI server with SQLite and local RAG. | |
| Production-ready with Security Headers, Audit Ledger, Multi-Agent Supervisors, | |
| Cloud Connectors, Headless Browser Automation, and Resilient Error Handling. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import uuid | |
| from collections.abc import AsyncGenerator, Awaitable, Callable | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| from typing import Any | |
| from fastapi import FastAPI, Request, Response | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse, HTMLResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from pydantic import BaseModel | |
| import backend.compat # noqa: F401 | |
| from apps.api.routers.v1 import ( | |
| agents, | |
| audit, | |
| auth, | |
| browser, | |
| caldav, | |
| calendar, | |
| career, | |
| connectors, | |
| devices, | |
| rag, | |
| reminders, | |
| settings, | |
| tasks, | |
| training, | |
| voice, | |
| widget, | |
| ) | |
| from apps.rag import db as rag_db | |
| from apps.rag.generator import get_model_name, model_is_available | |
| from apps.rag.retriever import get_chunk_count | |
| from backend.agents.supervisor import ensure_agent_tables | |
| from backend.audit.service import audit_service | |
| from backend.browser.service import ensure_browser_tables | |
| from backend.calendar.caldav_service import caldav_service | |
| from backend.career.service import career_service | |
| from backend.security import auth as security_auth | |
| from backend.tasks.service import ensure_tasks_tables | |
| from backend.training.service import TrainingService | |
| class RagStatus(BaseModel): | |
| model_found: bool | |
| chunk_count: int | |
| model_name: str | None = None | |
| class HealthResponse(BaseModel): | |
| status: str | |
| database: bool | |
| postgres: bool = False | |
| rag: RagStatus = RagStatus(model_found=False, chunk_count=0) | |
| async def lifespan(fastapi_app: FastAPI) -> AsyncGenerator[None, None]: | |
| """Startup: initialise SQLite database schemas and background services.""" | |
| await rag_db.init_db() | |
| await security_auth.ensure_auth_tables() | |
| await ensure_tasks_tables() | |
| await caldav_service.ensure_tables() | |
| await ensure_agent_tables() | |
| await ensure_browser_tables() | |
| await audit_service.ensure_tables() | |
| await career_service.ensure_tables() | |
| await TrainingService.ensure_schema() | |
| # Ensure reminders tables exist and start scheduler | |
| from backend.reminders.service import ( # noqa: PLC0415 | |
| ensure_reminders_tables, | |
| start_scheduler, | |
| stop_scheduler, | |
| ) | |
| await ensure_reminders_tables() | |
| start_scheduler() | |
| audit_service.record( | |
| actor="system", | |
| action="SERVER_STARTUP", | |
| resource="api_server", | |
| risk_level="LOW", | |
| status="SUCCESS", | |
| details={"version": "0.2.0", "mode": "production-ready"}, | |
| ) | |
| import asyncio # noqa: PLC0415 | |
| async def _safe_prewarm_models() -> None: | |
| """Gentle background pre-warming of AI models (laptop-friendly, zero initial UI wait).""" | |
| await asyncio.sleep(2.5) | |
| try: | |
| from starlette.concurrency import run_in_threadpool # noqa: PLC0415 | |
| from apps.rag.generator import get_llm, model_is_available # noqa: PLC0415 | |
| from apps.rag.retriever import get_embed_model # noqa: PLC0415 | |
| await run_in_threadpool(get_embed_model) | |
| if model_is_available(): | |
| await run_in_threadpool(get_llm) | |
| except Exception: | |
| pass | |
| asyncio.create_task(_safe_prewarm_models()) | |
| yield | |
| # Shutdown β stop background scheduler gracefully | |
| stop_scheduler() | |
| audit_service.record( | |
| actor="system", | |
| action="SERVER_SHUTDOWN", | |
| resource="api_server", | |
| risk_level="LOW", | |
| status="SUCCESS", | |
| ) | |
| app = FastAPI( | |
| title="Personal Assistant (my-local-bot) API", | |
| description="Privacy-First Local Personal Assistant with SQLite and Offline RAG", | |
| version="0.2.0", | |
| lifespan=lifespan, | |
| ) | |
| app.state.test_mode_allow_default = False | |
| # ββ CORS Middleware βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Restrict CORS to trusted local origins and private mesh network by default. | |
| # Prevents arbitrary internet websites from executing cross-origin attacks against localhost. | |
| cors_origins_env = os.environ.get("ALLOWED_ORIGINS", "") | |
| allowed_origins = [o.strip() for o in cors_origins_env.split(",") if o.strip()] if cors_origins_env else [ | |
| "http://localhost:8000", | |
| "http://127.0.0.1:8000", | |
| "http://localhost:3000", | |
| "http://127.0.0.1:3000", | |
| "http://localhost:5173", | |
| "http://127.0.0.1:5173", | |
| ] | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=allowed_origins, | |
| allow_origin_regex=r"(https://.*\.vercel\.app|http://(localhost|127\.0\.0\.1|100\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?)", | |
| allow_credentials=True, | |
| allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], | |
| allow_headers=["*"], | |
| ) | |
| # ββ Security Headers & Tracing Middleware ββββββββββββββββββββββββββββββββββββ | |
| async def security_and_tracing_middleware( | |
| request: Request, call_next: Callable[[Request], Awaitable[Response]] | |
| ) -> Response: | |
| """ | |
| Applies security headers and correlation ID (X-Request-ID) to all HTTP responses. | |
| Implements OWASP security recommendations: nosniff, framing protection, CSP. | |
| """ | |
| request_id = request.headers.get("X-Request-ID") or uuid.uuid4().hex | |
| request.state.request_id = request_id | |
| response = await call_next(request) | |
| response.headers["X-Request-ID"] = request_id | |
| response.headers["X-Content-Type-Options"] = "nosniff" | |
| response.headers["X-Frame-Options"] = "SAMEORIGIN" | |
| response.headers["X-XSS-Protection"] = "1; mode=block" | |
| response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" | |
| response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" | |
| response.headers["Content-Security-Policy"] = ( | |
| "default-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com data: blob:; " | |
| "img-src 'self' data: blob:; connect-src 'self' ws: wss:; worker-src 'self' blob:;" | |
| ) | |
| response.headers["Permissions-Policy"] = "camera=(), geolocation=(), microphone=(self)" | |
| return response | |
| # ββ Auth Enforcement Middleware ββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def auth_enforcement_middleware( | |
| request: Request, call_next: Callable[[Request], Awaitable[Response]] | |
| ) -> Response: | |
| """ | |
| Enforces JWT or Device Token authentication for all non-public endpoints. | |
| Public endpoints: | |
| - /health | |
| - /privacy, /terms | |
| - /api/v1/auth/setup, /api/v1/auth/login, /api/v1/auth/status | |
| - /api/v1/auth/recovery-token, /api/v1/auth/recovery-reset | |
| - Static assets & HTML pages (/, /static/*, /favicon.ico, /manifest.json, /sw.js) | |
| - OpenAPI docs (/docs, /openapi.json, /redoc) | |
| - Device pairing endpoints (verify master password or admin JWT internally) | |
| """ | |
| path = request.url.path | |
| # Allow CORS preflight OPTIONS requests unconditionally | |
| if request.method == "OPTIONS": | |
| return await call_next(request) | |
| # Allow public endpoints | |
| if ( | |
| path == "/health" | |
| or path in ["/privacy", "/terms"] | |
| or path.startswith("/api/v1/auth/") | |
| or path in ["/", "/manifest.json", "/sw.js", "/favicon.ico", "/widget.html", "/widget", "/orbit_calendar.html"] | |
| or path.startswith("/api/v1/widget") | |
| or path.startswith("/static/") | |
| or path.startswith("/docs") | |
| or path.startswith("/openapi.json") | |
| or path.startswith("/redoc") | |
| or path in ["/api/v1/devices/pair/request", "/api/v1/devices/pair/verify", "/api/v1/devices/qr"] | |
| or (path.startswith("/api/v1/training/jobs/") and path.endswith("/sync")) | |
| ): | |
| return await call_next(request) | |
| # In test mode, allow unauthenticated access for legacy test suite if enabled | |
| if getattr(request.app.state, "test_mode_allow_default", False): | |
| auth_hdr = request.headers.get("Authorization") | |
| x_dev_hdr = request.headers.get("X-Device-Token") | |
| if not auth_hdr and not x_dev_hdr: | |
| return await call_next(request) | |
| # Extract credential token | |
| token = None | |
| auth_hdr = request.headers.get("Authorization") | |
| if auth_hdr and auth_hdr.startswith("Bearer "): | |
| token = auth_hdr[7:].strip() | |
| elif "X-Device-Token" in request.headers: | |
| token = request.headers.get("X-Device-Token", "").strip() | |
| if not token: | |
| return JSONResponse( | |
| status_code=401, | |
| content={ | |
| "detail": "Authentication credentials were not provided. Provide a valid Bearer JWT or X-Device-Token." | |
| }, | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| # Check revocation blacklist | |
| if security_auth.is_token_revoked(token): | |
| return JSONResponse( | |
| status_code=401, | |
| content={"detail": "Authentication token has been revoked. Please log in again."}, | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| # 1. Validate as JWT | |
| try: | |
| payload = security_auth.decode_access_token(token) | |
| request.state.auth_user = payload | |
| return await call_next(request) | |
| except Exception: | |
| pass | |
| # 2. Validate as Device Token | |
| from backend.devices.registry import device_registry | |
| rec = device_registry.find_device_by_token(token) | |
| if rec is not None: | |
| request.state.auth_device = rec | |
| return await call_next(request) | |
| return JSONResponse( | |
| status_code=401, | |
| content={"detail": "Invalid or expired authentication credentials."}, | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| # ββ Register active API v1 routers βββββββββββββββββββββββββββββββββββββββββββ | |
| app.include_router(auth.router, prefix="/api/v1") | |
| app.include_router(caldav.router, prefix="/api/v1") | |
| app.include_router(calendar.router, prefix="/api/v1") | |
| app.include_router(devices.router, prefix="/api/v1") | |
| app.include_router(rag.router, prefix="/api/v1") | |
| app.include_router(reminders.router, prefix="/api/v1") | |
| app.include_router(tasks.router, prefix="/api/v1") | |
| app.include_router(voice.router, prefix="/api/v1") | |
| app.include_router(agents.router, prefix="/api/v1") | |
| app.include_router(connectors.router, prefix="/api/v1") | |
| app.include_router(browser.router, prefix="/api/v1") | |
| app.include_router(audit.router, prefix="/api/v1") | |
| app.include_router(settings.router, prefix="/api/v1") | |
| app.include_router(training.router, prefix="/api/v1") | |
| app.include_router(career.router, prefix="/api/v1") | |
| app.include_router(widget.router, prefix="/api/v1") | |
| # ββ Custom 404 Exception Handler βββββββββββββββββββββββββββββββββββββββββββββ | |
| async def custom_404_handler(request: Request, exc: Any) -> Response: | |
| accept = request.headers.get("Accept", "") | |
| if "text/html" in accept and not request.url.path.startswith("/api/"): | |
| return HTMLResponse( | |
| status_code=404, | |
| content="""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>404 β Page Not Found | PABOT</title> | |
| <style> | |
| body { background: #0A0D12; color: #E7EAEE; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; text-align: center; } | |
| .box { background: #12171F; border: 1px solid #242C38; border-radius: 12px; padding: 48px 36px; max-width: 480px; box-shadow: 0 12px 36px rgba(0,0,0,0.4); } | |
| h1 { font-size: 56px; color: #C89B5C; margin: 0 0 8px; font-weight: 700; } | |
| h2 { font-size: 20px; color: #E7EAEE; margin: 0 0 16px; font-weight: 600; } | |
| p { color: #96A1B0; margin-bottom: 28px; font-size: 14px; line-height: 1.6; } | |
| a { display: inline-block; background: #C89B5C; color: #0A0D12; text-decoration: none; padding: 12px 24px; border-radius: 6px; font-weight: 600; font-size: 14px; transition: opacity 0.2s; } | |
| a:hover { opacity: 0.9; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="box"> | |
| <h1>404</h1> | |
| <h2>Resource Not Found</h2> | |
| <p>The requested endpoint or document is not available on this personal assistant server.</p> | |
| <a href="/">Return to Dashboard</a> | |
| </div> | |
| </body> | |
| </html>""", | |
| ) | |
| return JSONResponse( | |
| status_code=404, | |
| content={ | |
| "detail": f"Resource not found: {request.url.path}", | |
| "status_code": 404, | |
| "path": request.url.path, | |
| }, | |
| ) | |
| # ββ Legal & Privacy Endpoints ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def get_privacy_policy() -> HTMLResponse: | |
| """Privacy Policy page detailing local-first zero-telemetry architecture and data rights.""" | |
| return HTMLResponse( | |
| content="""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Privacy Policy β PABOT</title> | |
| <style> | |
| body { background: #0A0D12; color: #E7EAEE; font-family: -apple-system, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 40px 20px; } | |
| h1 { color: #C89B5C; font-size: 28px; margin-bottom: 8px; } | |
| h2 { color: #E7EAEE; font-size: 18px; margin-top: 24px; border-bottom: 1px solid #242C38; padding-bottom: 6px; } | |
| p, li { color: #96A1B0; font-size: 14px; } | |
| ul { padding-left: 20px; margin-bottom: 16px; } | |
| .badge { display: inline-block; background: rgba(47, 168, 138, 0.2); color: #2FA88A; padding: 4px 10px; border-radius: 4px; font-size: 12px; font-weight: 600; margin-bottom: 20px; } | |
| a { color: #C89B5C; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="badge">100% LOCAL-FIRST β’ ZERO EXTERNAL TELEMETRY</div> | |
| <h1>Privacy Policy</h1> | |
| <p>Last updated: October 2026</p> | |
| <h2>1. Core Privacy Invariant</h2> | |
| <p>PABOT operates exclusively within a <strong>Local-First Trust Boundary</strong>. All personal data, notes, calendar events, tasks, chat messages, and vector embeddings reside on your own primary machine in SQLite databases. No data is transmitted to cloud vendors, telemetry brokers, or third parties unless you explicitly configure an external connector (e.g. Gmail, GitHub, Notion) with your own API tokens.</p> | |
| <h2>2. AI Model & Inference</h2> | |
| <p>All language model inference executes locally using offline quantized models (GGUF via llama-cpp-python) and local CPU voice synthesis (Piper ONNX). Your queries and documents are processed entirely on your hardware and never leave your machine.</p> | |
| <h2>3. Data Collection and Retention</h2> | |
| <p>The system stores only the data you provide to manage your personal schedule and knowledge base. You maintain complete sovereignty over your data and can perform point-in-time backups or permanently purge all personal information via <code>POST /api/v1/auth/wipe-data</code>.</p> | |
| <h2>4. Authentication and Device Security</h2> | |
| <p>Passwords are hashed using bcrypt with salt rounds. Device pairing tokens are stored exclusively as deterministic SHA-256 digests. Active sessions can be revoked immediately via the Devices tab or logout endpoint.</p> | |
| <p style="margin-top: 32px;"><a href="/">β Return to Dashboard</a></p> | |
| </body> | |
| </html>""" | |
| ) | |
| async def get_terms_of_service() -> HTMLResponse: | |
| """Terms of Service describing local usage, human authority, and license.""" | |
| return HTMLResponse( | |
| content="""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Terms of Service β PABOT</title> | |
| <style> | |
| body { background: #0A0D12; color: #E7EAEE; font-family: -apple-system, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 40px 20px; } | |
| h1 { color: #C89B5C; font-size: 28px; margin-bottom: 8px; } | |
| h2 { color: #E7EAEE; font-size: 18px; margin-top: 24px; border-bottom: 1px solid #242C38; padding-bottom: 6px; } | |
| p, li { color: #96A1B0; font-size: 14px; } | |
| ul { padding-left: 20px; margin-bottom: 16px; } | |
| a { color: #C89B5C; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Terms of Service</h1> | |
| <p>Last updated: October 2026</p> | |
| <h2>1. Self-Hosted Software License</h2> | |
| <p>PABOT is self-hosted software intended for personal productivity and private information management. The software is provided "as is", without warranty of any kind, express or implied.</p> | |
| <h2>2. Human In-The-Loop Authority</h2> | |
| <p>As established in the system's architectural invariants, the language model provides recommendations and drafting intelligence, while deterministic human authorization is strictly required for any consequential action (such as email dispatch, external form submission, or file overwrites).</p> | |
| <h2>3. Local Storage Responsibility</h2> | |
| <p>Because PABOT is non-custodial and stores data locally on your storage drives, you are responsible for maintaining regular backups using the provided backup tools (<code>scripts/backup.py</code>) and securing access to your host machine.</p> | |
| <p style="margin-top: 32px;"><a href="/">β Return to Dashboard</a></p> | |
| </body> | |
| </html>""" | |
| ) | |
| # ββ Health & Diagnostics βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def health_check() -> HealthResponse: | |
| # ββ Local SQLite check ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| db_healthy = False | |
| try: | |
| import aiosqlite | |
| async with aiosqlite.connect(str(rag_db.APP_DB)) as conn: | |
| async with conn.execute("SELECT 1;") as cursor: | |
| row = await cursor.fetchone() | |
| db_healthy = row is not None and row[0] == 1 | |
| except Exception: | |
| db_healthy = False | |
| # ββ PostgreSQL check ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| pg_healthy = False | |
| try: | |
| import os | |
| import asyncpg | |
| db_url = os.environ.get("DATABASE_URL", "") | |
| if not db_url: | |
| try: | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| db_url = os.environ.get("DATABASE_URL", "") | |
| except Exception: | |
| pass | |
| if db_url: | |
| clean_url = db_url.replace("postgresql+asyncpg://", "postgresql://") | |
| conn = await asyncpg.connect(clean_url, timeout=2.0) | |
| pg_healthy = (await conn.fetchval("SELECT 1;") == 1) | |
| await conn.close() | |
| except Exception: | |
| pg_healthy = False | |
| # ββ RAG subsystem check βββββββββββββββββββββββββββββββββββββββββββββββ | |
| rag_status = RagStatus( | |
| model_found=model_is_available(), | |
| chunk_count=get_chunk_count(), | |
| model_name=get_model_name() if model_is_available() else None, | |
| ) | |
| overall_status = "ok" if (db_healthy or pg_healthy) else "degraded" | |
| return HealthResponse( | |
| status=overall_status, | |
| database=db_healthy, | |
| postgres=pg_healthy, | |
| rag=rag_status, | |
| ) | |
| async def get_system_info() -> dict[str, str]: | |
| return { | |
| "app_name": "my-local-bot", | |
| "version": "0.2.0", | |
| "mode": "production-ready-local-first", | |
| } | |
| # ββ Static Web Assets & PWA Serving ββββββββββββββββββββββββββββββββββββββββββ | |
| public_dir = Path(__file__).resolve().parents[1] / "web" / "public" | |
| if public_dir.exists(): | |
| app.mount("/static", StaticFiles(directory=str(public_dir)), name="static") | |
| async def serve_dashboard() -> Any: | |
| index_file = public_dir / "index.html" | |
| if index_file.exists(): | |
| return FileResponse( | |
| index_file, | |
| headers={ | |
| "Cache-Control": "no-cache, no-store, must-revalidate", | |
| "Pragma": "no-cache", | |
| "Expires": "0", | |
| }, | |
| ) | |
| return {"message": "Personal Assistant API running. Static dashboard present."} | |
| async def serve_widget() -> Any: | |
| widget_file = public_dir / "widget.html" | |
| if widget_file.exists(): | |
| return FileResponse( | |
| widget_file, | |
| headers={ | |
| "Cache-Control": "no-cache, no-store, must-revalidate", | |
| "Pragma": "no-cache", | |
| "Expires": "0", | |
| }, | |
| ) | |
| return FileResponse(public_dir / "index.html") | |