import asyncio import logging import os import sys # ── No flashing console windows in the shipped app ──────────────────────────── # On Windows every subprocess started from a frozen (PyInstaller) build pops a # console window unless CREATE_NO_WINDOW is set. This backend shells out from # ~85 call sites, several on short timers (the USB monitor runs every 2s), so # the user sees a terminal flashing continuously while JARVIS is running. # Rather than annotate every call site (and silently regress on the next one), # force the flag globally for the packaged build. if sys.platform == "win32": import subprocess as _subprocess _CREATE_NO_WINDOW = 0x08000000 _orig_popen_init = _subprocess.Popen.__init__ def _no_window_popen_init(self, *args, **kwargs): # shell=True routes through cmd.exe, which is the loudest offender. if not kwargs.get("close_fds_is_dummy"): kwargs["creationflags"] = kwargs.get("creationflags", 0) | _CREATE_NO_WINDOW return _orig_popen_init(self, *args, **kwargs) _subprocess.Popen.__init__ = _no_window_popen_init # --- resolve the app-data dir BEFORE any import that touches the database --- # JARVIS_APP_DATA_DIR used to be set only inside main(), i.e. after every module had # already been imported. backend/services/usb_vault.py initialises the vault at import # time (`MASTER_KEY = _init_vault_db()`), so on the exe it created vault_keys / # vault_secrets / vault_credentials in the FALLBACK database while every later request # resolved the real app-data database — two different SQLite files in one process. # # Verified on a fresh install in Part 26: the app-data memory.db had 17 tables and # neither vault table, so /security/vault/credentials and /sentinel/credentials/captured # answered 500 ("no such table: vault_credentials") and every secret decrypt logged # "no such table: vault_secrets". Setting the variable here, before the imports, points # both halves at the same file. _app_data = next( (sys.argv[i + 1] for i, a in enumerate(sys.argv) if a == '--app-data-dir' and i + 1 < len(sys.argv)), None ) if _app_data: os.environ.setdefault("JARVIS_APP_DATA_DIR", _app_data) # --- load .env early: token_manager.py executes at import time, before main() --- try: from dotenv import load_dotenv as _load_dotenv if getattr(sys, 'frozen', False): if _app_data: _load_dotenv(os.path.join(_app_data, '.env'), override=False) else: _load_dotenv() else: _load_dotenv() del _load_dotenv except ImportError: pass # ----------------------------------------------------------------------- # Insert root directory to sys.path to allow 'import modules.x' to work natively from Cloud parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if parent_dir not in sys.path: sys.path.insert(0, parent_dir) os.environ["TYPEGUARD_DISABLE"] = "1" # Torch load monkey patch moved to backend/voice/vad.py for lazy-loading import inspect _orig_getsource = inspect.getsource def _safe_getsource(obj): try: return _orig_getsource(obj) except OSError: return "" inspect.getsource = _safe_getsource _orig_getsourcelines = inspect.getsourcelines def _safe_getsourcelines(obj): try: return _orig_getsourcelines(obj) except OSError: return ([""], 0) inspect.getsourcelines = _safe_getsourcelines import uvicorn # Ensure backend module is in sys.path when running from compiled PyInstaller executable if getattr(sys, 'frozen', False): sys.path.append(sys._MEIPASS) from backend.ws.agent_ws import ws_manager # --- STATIC IMPORTS FOR PYINSTALLER --- # ---------------------------------------- from backend.db.migrations import run_all_migrations from backend.routes.agent_routes import router as agent_router from backend.routes.memory_routes import router as memory_router from backend.routes.voice_routes import router as voice_router from backend.routes.system_routes import router as system_router from backend.routes.automation_routes import router as automation_router from backend.routes.security_routes import router as security_router from backend.routes.github_routes import router as github_router from backend.routes.xr_routes import router as xr_router from backend.routes.android_routes import router as android_router from backend.routes.config_routes import router as config_router from backend.routes.reliability_routes import router as reliability_router from backend.routes.easter_egg_routes import router as easter_egg_router from backend.routes.omega_routes import router as omega_router from backend.routes.persona_routes import router as persona_router from backend.routes.internet_routes import router as internet_router from backend.routes.sentinel_routes import router as sentinel_router from backend.routers.gaming_routes import router as gaming_router from backend.routes.model_proxy_routes import router as model_proxy_router # AR-FIX-SESSION from backend.routes.mobile_bridge_routes import router as mobile_bridge_router # S4: Guardian /api compat from backend.routes.distribution_routes import router as distribution_router # v21: real install + update path from backend.routers.family_device_routes import router as family_device_router # v21: was never mounted from backend.services.voice_service import start_wake_word_loop from backend.services.usb_monitor import start_usb_monitor from backend.services.system_monitor import start_system_stats_loop from backend.services.pc_mic_service import start_pc_mic_loop from backend.voice.audio_ws import run_in_thread as start_audio_ws # Single source of truth (backend/version.py). Six surfaces reported five different # version strings before this; nothing owned the value, so nothing reconciled it. from backend.version import VERSION as APP_VERSION from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, Request from backend.security.auth import verify_token try: from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware SLOWAPI_AVAILABLE = True except ImportError: logging.warning("slowapi package not found. Rate limiting is disabled.") SLOWAPI_AVAILABLE = False try: import sentry_sdk sentry_sdk.init( dsn="https://c48f4234bb502ff74a2c5e518a3a65b2@o4511579717369856.ingest.us.sentry.io/4511579850407936", traces_sample_rate=1.0, send_default_pii=True, ) except Exception: pass print(r""" ____. _____ ____________________.__ _________ | | / _ \______ \____ \ \ \ \ \ ___/ | |/ /_\ \| _// | \ \ \ \ \____ \ /\__| / | \ | \ | \ \ \_\ \ \ \ \________\____|__ /____|_ /_______/___/\______/____/ \/ \/ Fast Booting... Lazy Loading Heavy ML Models... """) app = FastAPI(title="JARVIS / FRIDAY OS Backend Sidecar") if SLOWAPI_AVAILABLE: limiter = Limiter(key_func=get_remote_address, default_limits=["100/minute"]) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) app.include_router(agent_router, prefix="/agent", dependencies=[Depends(verify_token)]) app.include_router(memory_router, prefix="/memory", dependencies=[Depends(verify_token)]) app.include_router(voice_router, prefix="/voice", dependencies=[Depends(verify_token)]) app.include_router(system_router, prefix="/system", dependencies=[Depends(verify_token)]) # v22: AUTOMATION is a paid feature in the plan catalog (plus/pro only — see # billing/plans.py), but no automation route enforced it: a caller on the `free` plan got # 200 + real scheduler data from /automation/list and /automation/history. The catalog # declared the boundary and nothing collected on it. Gated at the mount so all ten routes # are covered, rather than per-route where the next new route would silently miss it. from backend.billing.gating import require_feature as _require_feature from backend.billing.plans import Feature as _Feature app.include_router(automation_router, prefix="/automation", dependencies=[Depends(verify_token), Depends(_require_feature(_Feature.AUTOMATION))]) app.include_router(security_router, prefix="/security", dependencies=[Depends(verify_token)]) app.include_router(github_router, prefix="/github", dependencies=[Depends(verify_token)]) app.include_router(xr_router, prefix="/xr", dependencies=[Depends(verify_token)]) app.include_router(android_router, prefix="/android", dependencies=[Depends(verify_token)]) app.include_router(config_router, prefix="/config", dependencies=[Depends(verify_token)]) # v6 additions: crash telemetry + user-data backup/export (owner-facing, local-only). app.include_router(reliability_router, prefix="/reliability", dependencies=[Depends(verify_token)]) # Subscription / billing. /billing/webhook stays OUT of the token gate — it is # authenticated by the payment processor's own signature (payments.verify_webhook), # not the operator token, exactly like any real payment webhook. from backend.billing.routes import router as billing_router app.include_router(billing_router, prefix="/billing") app.include_router(omega_router, prefix="") # Prefix defined in router as /omega app.include_router(easter_egg_router, prefix="/easter") # Exclude easter egg from auth # v21: the install/update path. Unauthenticated by design — gating the installer behind the app # you are trying to install is a bootstrap the product cannot survive. Entitlement is enforced on # features, server-side, not on installer bytes. app.include_router(distribution_router, prefix="") # v21: family_device_router existed with real routes but was never include_router'd, so the only # enrollment endpoint in the codebase was unreachable at runtime on every deployment. app.include_router(family_device_router, prefix="") app.include_router(persona_router, prefix="/persona", dependencies=[Depends(verify_token)]) app.include_router(internet_router, prefix="/internet", dependencies=[Depends(verify_token)]) app.include_router(sentinel_router, prefix="/sentinel", dependencies=[Depends(verify_token)]) app.include_router(gaming_router, prefix="", dependencies=[Depends(verify_token)]) # gaming routes already have /gaming prefix app.include_router(model_proxy_router, prefix="/api") # AR-FIX-SESSION: model provider proxy stubs (no auth — WebAR calls these from browser) # S4: the WebAR client's documented contract (README, service-worker, vite proxy) is # /api/ar_config, /api/ar_task, /api/ar_scene_patch — the backend only ever mounted # them under /xr, so every OMEGA link from the AR client 404'd. Serve both prefixes. app.include_router(xr_router, prefix="/api", dependencies=[Depends(verify_token)]) # S4: the JARVIS Mobile Guardian APK's remaining /api/* REST contract (status, caps, # link_info, pair_confirm, command, phone_observe, jarvis/mobile_event, # max_autonomy/task) had no cloud handler and 404'd against the Space. This compat # router serves exactly those, delegating to modules/max_autonomy + phone/crypto. app.include_router(mobile_bridge_router, prefix="/api", dependencies=[Depends(verify_token)]) # S4: serve generated 3D models. xr_tools.push_model_to_ar_scene has always broadcast # /static/models3d/ URLs, but nothing ever mounted them — every spawned GLB 404'd. from fastapi.staticfiles import StaticFiles _MODELS3D_DIR = os.path.join("storage", "models3d") os.makedirs(_MODELS3D_DIR, exist_ok=True) app.mount("/static/models3d", StaticFiles(directory=_MODELS3D_DIR), name="models3d") # S4: serve the WebAR client from the Space so mobile AR loads directly from # jarvis-cloud.hf.space/webar/ (previously only shipped in cloud_deployment + APK assets). for _webar_candidate in ( os.environ.get("JARVIS_WEBAR_DIR", ""), os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "webar"), os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "cloud_deployment", "webar"), ): if _webar_candidate and os.path.isdir(_webar_candidate): app.mount("/webar", StaticFiles(directory=_webar_candidate, html=True), name="webar") logging.info(f"[S4] WebAR client mounted at /webar from {_webar_candidate}") break # Public front door of the Space. Everything on it is either static or rendered # server-side from real state (uptime) — nothing fabricated, nothing token-gated # leaks here. _ROOT_STATUS_PAGE = """ JARVIS Cloud OS

Stark Industries · Cloud Node

JARVIS / FRIDAY

OMEGA Cloud OS — all API surfaces are token-gated.

StatusOnline
Uptime{{UPTIME}}
ModeCloud
AssistantJARVIS

Authorized clients: JARVIS desktop, Guardian mobile, WebAR.

""" @app.get("/") async def root_health_check(request: Request): # Browsers (the Space's public front door) get a real status page; # programmatic callers keep the JSON contract unchanged. if "text/html" in (request.headers.get("accept") or ""): from backend.routes.mobile_bridge_routes import _uptime_str from fastapi.responses import HTMLResponse return HTMLResponse(_ROOT_STATUS_PAGE.replace("{{UPTIME}}", _uptime_str())) return {"status": "JARVIS / FRIDAY Cloud OS Online", "message": "All systems nominal."} @app.get("/wake") @app.post("/wake") async def wake_server(): """Public endpoint — anyone can hit this from anywhere in the world to wake the cloud server. HF Spaces sleep after inactivity; this wakes them up instantly. Only claims what is actually true: the fact this handler is running means the Space process is awake. Per-database claims were previously hardcoded "online" without any check — that lie is gone.""" import time return { "status": "online", "message": "JARVIS / FRIDAY Cloud Brain is awake and operational.", "timestamp": time.time(), } @app.get("/ping") async def ping(): """Ultra-lightweight public ping endpoint for keepalive from mobile app.""" return {"pong": True} from fastapi import Request from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware from backend.dependencies.auth import verify_master_token # Configure CORS for production (Tauri and Android network) app.add_middleware( CORSMiddleware, allow_origins=["tauri://localhost", "http://localhost", "http://localhost:1420", "*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): logging.error(f"Global Error on {request.url.path}: {exc}") return JSONResponse( status_code=500, content={"error": "Internal Server Error", "detail": str(exc)}, ) @app.middleware("http") async def auth_middleware(request: Request, call_next): allowed_paths = ["/", "/health", "/wake", "/ping", "/android/pair", "/voice/upload_log"] # S4: read-only static surfaces the AR client fetches without headers — # GLTFLoader cannot attach Authorization, and the client app itself is public. # /static/models3d files are uuid-named GLBs; /webar is the shipped client bundle. # v21: /distribution is public by necessity, not by oversight. A subscriber standing at the # download page does not yet have the app that holds their token, and Tauri's updater sends # no Authorization header at all — so gating these behind auth means the install path and # every update check 401 for exactly the people they exist to serve. Verified: without this # prefix the live Space answered 401 (not 404) to /distribution/manifest. # Nothing sensitive is exposed: these serve public installer bytes and a version number. # Entitlement is still enforced server-side on features, per the standing plan-gating rule. allowed_prefixes = ("/static/models3d/", "/webar", "/distribution") if (request.url.path in allowed_paths or request.url.path.startswith(allowed_prefixes) or request.url.path.startswith("/ws") or request.url.path.endswith("/ws")): return await call_next(request) # Allow local connections without auth if request.client.host in ["127.0.0.1", "localhost", "::1"]: return await call_next(request) token = request.headers.get("Authorization") if not token or not verify_master_token(token): return JSONResponse(status_code=401, content={"error": "Unauthorized Access"}) return await call_next(request) IS_READY = False audio_ws_server = None BACKGROUND_TASKS = [] @app.on_event("startup") async def startup_event(): global IS_READY, audio_ws_server logging.info("Starting JARVIS / FRIDAY backend sidecar...") # Start Audio WS in background thread (no-op stub in cloud mode) audio_ws_server = start_audio_ws() # S4: start the AR scene bus (port 5050) in-process. On the Space nothing else # ever launched it — /scene/ws proxied into a dead port and every # push_model_to_ar_scene broadcast was silently lost. Desktop entry points # (tray, local_server) may have started it already; bind failure is fine then. try: from phone.ws_scene_bus import run_in_thread as start_scene_bus start_scene_bus(host="127.0.0.1", port=5050) logging.info("AR scene bus thread started on 127.0.0.1:5050") except Exception as scene_bus_error: logging.warning(f"AR scene bus not started: {scene_bus_error}") # Start automations scheduler from backend.services.automation_service import init_automations await init_automations() # Track all asyncio background loops # pc_mic_loop, wake_word_loop, usb, stats are local-only — skip in cloud (HF has no mic/usb) cloud_mode = os.environ.get("CLOUD_ENV", "false").lower() == "true" if not cloud_mode: BACKGROUND_TASKS.append(asyncio.create_task(start_pc_mic_loop())) BACKGROUND_TASKS.append(asyncio.create_task(start_wake_word_loop())) BACKGROUND_TASKS.append(asyncio.create_task(start_usb_monitor())) BACKGROUND_TASKS.append(asyncio.create_task(start_system_stats_loop())) # --- JARVIS 10X Universal Gaming Coach --- try: from backend.gaming.coach_engine import coach_engine from backend.gaming.overlay_renderer import run_overlay_in_background run_overlay_in_background() # Spawns PyQt5 window in a background OS thread BACKGROUND_TASKS.append(asyncio.create_task(coach_engine.start_loop())) except Exception as e: logging.warning(f"JARVIS 10X Coach not active (missing deps / Qt crash?): {e}") else: logging.info("[Startup] Cloud mode — skipping local hardware loops (mic, wake, usb, stats).") # 1. Models Loader Placeholder (Wait for models) # TTS model loading is offloaded to the Audio WS background thread to prevent blocking Uvicorn. # 2. mDNS Registration from backend.services.mdns_discovery import register_mdns_service port = int(os.environ.get("JARVIS_PORT_BOUND", os.environ.get("JARVIS_PORT", "7474"))) register_mdns_service(port) # 3. Auto-start PC Relay Client (Local Windows Only) cloud_env = os.environ.get("CLOUD_ENV", "false").lower() == "true" if not cloud_env: if getattr(sys, "frozen", False): # Frozen sidecar: pc_relay_client.py is not on disk (__file__ resolves into # the _MEIPASS extraction dir) and sys.executable is this exe itself, so the # subprocess path below would either silently no-op or relaunch a second # full sidecar. Run the relay in-process on the existing event loop instead. try: import pc_relay_client BACKGROUND_TASKS.append(asyncio.create_task(pc_relay_client.connect_to_cloud())) logging.info("Started PC relay in-process (frozen sidecar).") except Exception as e: logging.error(f"Could not start in-process PC relay: {e}") else: try: import subprocess parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) relay_script = os.path.join(parent_dir, "pc_relay_client.py") if os.path.exists(relay_script): si = subprocess.STARTUPINFO() si.dwFlags |= subprocess.STARTF_USESHOWWINDOW si.wShowWindow = 0 subprocess.Popen([sys.executable, relay_script], cwd=parent_dir, startupinfo=si, creationflags=0x08000000) logging.info("Auto-started pc_relay_client.py natively from main server.") except Exception as e: logging.error(f"Could not auto-start pc_relay_client.py: {e}") # 4. §2.3+2.4 — Start Continuous Research Mode (both environments) from backend.omega.research_engine import start_continuous_research_mode BACKGROUND_TASKS.append(asyncio.create_task(start_continuous_research_mode("jarvis"))) logging.info("[Startup] Continuous Research Mode activated. JARVIS will monitor and queue proposals.") # 5. §Token Safety — Start Token Limit Checkpoint & Resume Monitor from backend.services.token_manager import start_token_refresh_monitor BACKGROUND_TASKS.append(asyncio.create_task(start_token_refresh_monitor())) logging.info("[Startup] Token Refresh Monitor activated. All Gemini 3.5 Flash tasks will auto-resume on 429.") # 6b. Billing/subscription store (additive tables in the same persistent DB) try: from backend.billing.store import init_db as _billing_init _billing_init() logging.info("[Startup] Billing/subscription tables verified.") except Exception as e: logging.error(f"[Startup] Billing init failed: {e}") # 6. Broadcast Ready (Health check passes) IS_READY = True @app.on_event("shutdown") async def shutdown_event(): logging.info("Shutting down JARVIS backend sidecar...") if audio_ws_server: audio_ws_server.stop() from backend.services.mdns_discovery import unregister_mdns_service unregister_mdns_service() from backend.services.automation_service import shutdown_automations await shutdown_automations() from backend.omega.research_engine import stop_continuous_research_mode stop_continuous_research_mode() from backend.services.token_manager import stop_token_refresh_monitor stop_token_refresh_monitor() # Gracefully cancel all background loops for task in BACKGROUND_TASKS: task.cancel() await asyncio.gather(*BACKGROUND_TASKS, return_exceptions=True) @app.get("/health") async def health(): if not IS_READY: from fastapi import HTTPException raise HTTPException(status_code=503, detail="Backend starting up") return {"status": "ok", "version": APP_VERSION} def _ws_authorized(websocket: WebSocket) -> bool: """S4: HTTP auth middleware never sees websocket scopes, so every WS endpoint was open to the internet on the public Space. Native clients send an Authorization header; browsers cannot, so they pass ?token= instead.""" try: if websocket.client and websocket.client.host in ("127.0.0.1", "localhost", "::1"): return True token = websocket.query_params.get("token") or websocket.headers.get("Authorization", "") return bool(token) and verify_master_token(token) except Exception: return False @app.websocket("/ws") async def websocket_hub(websocket: WebSocket): if not _ws_authorized(websocket): await websocket.close(code=1008) return await ws_manager.connect(websocket) async def heartbeat(): while True: await asyncio.sleep(30) try: await websocket.send_json({"event": "ping"}) except Exception: logging.warning("Heartbeat failed, closing dead WS connection.") try: await websocket.close() except Exception as e: # NO `import logging` here: it would make `logging` local to # heartbeat() and turn the warning three lines above into an # UnboundLocalError — silently killing the WS heartbeat. logging.getLogger(__name__).error(f"Swallowed exception: {e}") break heartbeat_task = asyncio.create_task(heartbeat()) try: while True: data = await websocket.receive_json() await ws_manager.handle_client_event(websocket, data) except WebSocketDisconnect: pass finally: heartbeat_task.cancel() ws_manager.disconnect(websocket) @app.websocket("/agent/ws") async def agent_websocket_alias(websocket: WebSocket): await websocket_hub(websocket) @app.websocket("/voice/ws") async def voice_websocket_proxy(websocket: WebSocket): if not _ws_authorized(websocket): await websocket.close(code=1008) return await websocket.accept() import websockets try: async with websockets.connect("ws://127.0.0.1:8767") as target_ws: async def forward_to_target(): try: while True: msg = await websocket.receive() if "bytes" in msg: await target_ws.send(msg["bytes"]) elif "text" in msg: await target_ws.send(msg["text"]) except Exception as e: import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") async def forward_to_client(): try: async for msg in target_ws: if isinstance(msg, bytes): await websocket.send_bytes(msg) else: await websocket.send_text(msg) except Exception as e: import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") await asyncio.gather(forward_to_target(), forward_to_client()) except Exception as e: logging.error(f"Voice WS Proxy Error: {e}") @app.websocket("/scene-ws") async def scene_websocket_alias(websocket: WebSocket): # S4: the WebAR client dials /scene-ws (its documented contract); the backend # only ever exposed /scene/ws, so the cloud scene link never connected. await scene_websocket_proxy(websocket) @app.websocket("/scene/ws") async def scene_websocket_proxy(websocket: WebSocket): if not _ws_authorized(websocket): await websocket.close(code=1008) return await websocket.accept() import websockets try: async with websockets.connect("ws://127.0.0.1:5050") as target_ws: async def forward_to_target(): try: while True: msg = await websocket.receive() if "bytes" in msg: await target_ws.send(msg["bytes"]) elif "text" in msg: await target_ws.send(msg["text"]) except Exception as e: import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") async def forward_to_client(): try: async for msg in target_ws: if isinstance(msg, bytes): await websocket.send_bytes(msg) else: await websocket.send_text(msg) except Exception as e: import logging; logging.getLogger(__name__).error(f"Swallowed exception: {e}") await asyncio.gather(forward_to_target(), forward_to_client()) except Exception as e: logging.error(f"Scene WS Proxy Error: {e}") def main(): import argparse parser = argparse.ArgumentParser() parser.add_argument("--app-data-dir", default="") parser.add_argument("--resource-dir", default="") args, unknown = parser.parse_known_args() if args.app_data_dir: os.environ["JARVIS_APP_DATA_DIR"] = args.app_data_dir if args.resource_dir: os.environ["JARVIS_RESOURCE_DIR"] = args.resource_dir import re import json class JSONMaskingFormatter(logging.Formatter): key_patterns = [ re.compile(r'(sk-[a-zA-Z0-9]{20,})'), re.compile(r'(AIza[0-9A-Za-z-_]{30,})'), re.compile(r'\b([A-Za-z0-9-_]{32,})\b') ] def format(self, record): msg = record.getMessage() for pattern in self.key_patterns: def repl(m): s = m.group(1) if len(s) > 8: return '*' * (len(s) - 4) + s[-4:] return s msg = pattern.sub(repl, msg) log_record = { "timestamp": self.formatTime(record, self.datefmt), "level": record.levelname, "name": record.name, "message": msg, } if record.exc_info: log_record["exception"] = self.formatException(record.exc_info) return json.dumps(log_record) root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) handler = logging.StreamHandler() handler.setFormatter(JSONMaskingFormatter()) root_logger.addHandler(handler) # Run migrations on the same DB path that routes query (get_db_path()) from backend.services.usb_monitor import get_db_path as _get_migration_db_path db_path = _get_migration_db_path() asyncio.run(run_all_migrations(db_path)) cloud_env = os.environ.get("CLOUD_ENV", "false").lower() == "true" host = "0.0.0.0" if cloud_env else "127.0.0.1" base_port = int(os.environ.get("PORT", "7860")) if cloud_env else int(os.environ.get("JARVIS_PORT", "7474")) for port in range(base_port, base_port + 11): try: os.environ["JARVIS_PORT_BOUND"] = str(port) print(f"JARVIS_PORT_BOUND={port}", flush=True) uvicorn.run(app, host=host, port=port, log_config=None, ws_ping_interval=20, ws_ping_timeout=10) break except OSError as e: if "WinError 10048" in str(e) or "address already in use" in str(e).lower(): logging.warning(f"Port {port} in use, trying next...") continue raise if __name__ == "__main__": main()