import asyncio import html import logging import os from uuid import uuid4 # App-level INFO logs (fast-path gates, skill recording, replay decisions) were invisible because nothing configured the 'backend' logger; every debugging session re-paid that blindness. Idempotent so uvicorn reloads don't stack handlers; uvicorn's own access logs are untouched. p_backend_logger = logging.getLogger("backend") if not p_backend_logger.handlers: p_h = logging.StreamHandler() p_h.setFormatter(logging.Formatter("%(asctime)s %(levelname).1s %(name)s: %(message)s", "%H:%M:%S")) p_backend_logger.addHandler(p_h) p_backend_logger.setLevel(logging.INFO) p_backend_logger.propagate = False logger = logging.getLogger(__name__) from fastapi.responses import JSONResponse, HTMLResponse from fastapi import Request from backend.apps.oauth_state import ( pending_oauth, completed_oauth, MAX_COMPLETED_OAUTH, mark_oauth_completed, ) from backend.config.Apps import MainApp from backend.apps.health.health import health from backend.apps.agents.agents import agents from backend.apps.agents.core.ws_manager import ws_manager from backend.apps.skills.skills import skills from backend.apps.tools_lib.tools_lib import tools_lib from backend.apps.modes.modes import modes from backend.apps.settings.settings import settings from backend.apps.mcp_registry.mcp_registry import mcp_registry from backend.apps.skill_registry.skill_registry import skill_registry from backend.apps.outputs.outputs import outputs from backend.apps.outputs.versions_routes import output_versions from backend.apps.dashboards.dashboards import dashboards from backend.apps.swarm.swarm import swarm from backend.apps.service.service import service from backend.apps.subscription.router import subscription from backend.apps.auth.router import auth from backend.apps.web.web import web from backend.apps.agents.proxy.anthropic_proxy import anthropic_proxy from backend.apps.agents.core.openai_passthrough import openai_passthrough from backend.apps.workflows.workflows import workflows from fastapi.middleware.cors import CORSMiddleware from fastapi import WebSocket, WebSocketDisconnect import json main_app = MainApp([health, agents, skills, tools_lib, modes, settings, mcp_registry, skill_registry, outputs, output_versions, dashboards, swarm, service, subscription, auth, web, anthropic_proxy, workflows, openai_passthrough]) app = main_app.app # Generate per-install auth token BEFORE we bind the HTTP port. By the time any request lands, the token file exists. See backend/auth.py. from backend.auth import ( init_auth_token, install_token_scrubber, is_path_exempt, request_matches_token, is_origin_allowed, ) init_auth_token() # Install the log scrubber AFTER the token exists so any log line that accidentally embeds it (subprocess env dumps, urllib retry traces, proxied-request error bodies) gets redacted before hitting handlers. install_token_scrubber() # Generate the per-install id (installation_id) at the same pre-bind moment as the auth token. It is otherwise created lazily on the first analytics submission, so on a clean install the sign-in window can render and build its Google/email OAuth URL (which embeds install_id) before that submission fires, producing an empty install_id that the cloud rejects. Generating here guarantees the very first GET /api/settings already carries it. Platform-agnostic; wrapped so a settings hiccup never blocks startup, and the lazy path stays as a fallback. try: import uuid as p_uuid from backend.apps.settings.store import load_settings as p_load_boot_settings, save_settings as p_save_boot_settings p_boot_settings = p_load_boot_settings() if not getattr(p_boot_settings, "installation_id", None): p_boot_settings.installation_id = p_uuid.uuid4().hex p_save_boot_settings(p_boot_settings) except Exception: pass # CORS: previously wide open (`allow_origins=["*"]`), which combined with `allow_credentials=True` was a security footgun, any external origin could CORS-preflight us. Now restricted to Electron renderer origins + localhost dev servers. The token middleware below provides the *primary* defense; CORS is defense-in-depth so a misconfigured page can't even reach us. app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:3000", "http://127.0.0.1:3000", "https://api.openswarm.com", "https://openswarm.com", ], allow_origin_regex=r"^(file://.*|http://localhost:\d+|http://127\.0\.0\.1:\d+)$", allow_credentials=True, allow_methods=["*"], allow_headers=["*"], # Every cross-origin POST from the Electron renderer (file:// → http://localhost:8324) carries Authorization: Bearer, which CORS classifies as non-simple and forces a preflight OPTIONS before EACH POST. With no max_age the browser re-preflights on a tight schedule (~5 s in Chromium); under heavy interaction we observed a 1:1 OPTIONS-to-POST ratio in the dev log, doubling roundtrip count for no reason. Caching the preflight result for 10 minutes drops that to one OPTIONS per ~600 POSTs. max_age=600, ) @app.middleware("http") async def p_auth_middleware(request: Request, call_next): """Reject HTTP requests without our per-install bearer token. Exemptions (see `auth.is_path_exempt`): - `/api/subscriptions/callback`, external OAuth redirects - `/api/health`, `/api/version`, Electron boot handshake - `OPTIONS` preflights, browsers don't send Authorization on them Anything else requires `Authorization: Bearer ` OR `x-openswarm-token: `. Failure responds with 401 and a short JSON error, no upstream handler sees the request. The anthropic-proxy route (`/api/anthropic-proxy/v1/*`) is NOT exempt. Its caller (the Claude Code CLI we spawn) is configured with `ANTHROPIC_API_KEY=` so the CLI's `x-api-key` header carries our token, which `request_matches_token` accepts via its auth-header branches. """ # Preflights never carry Authorization. if request.method == "OPTIONS": response = await call_next(request) elif is_path_exempt(request.url.path): response = await call_next(request) else: # Accept Authorization Bearer, x-openswarm-token, OR x-api-key (CLI path, CLI sends x-api-key with our token as value). headers = dict(request.headers) x_api_key = headers.get("x-api-key") or headers.get("X-API-Key") # Accept `?token=` query param too. Required for browser-driven GETs that can't set headers, notably the App Builder iframe loading /api/outputs/.../serve/index.html via