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.onboarding.onboarding import onboarding from backend.apps.voice.polish import voice from backend.apps.memory.router import memory from backend.apps.help.bundle import help_app 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 backend.apps.workflows.cloud.routes import cloud_workflows from backend.apps.apps_sdk.apps_sdk import apps_sdk 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, onboarding, voice, memory, help_app, anthropic_proxy, workflows, cloud_workflows, openai_passthrough, apps_sdk]) 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 re as p_re 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): # Electron resolves this before startup so analytics and affiliate attribution share one install id. p_env_iid = os.environ.get("OPENSWARM_INSTALLATION_ID", "") p_boot_settings.installation_id = ( p_env_iid if p_re.fullmatch(r"[A-Za-z0-9_-]{8,128}", p_env_iid) else 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