import logging import os from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from src.api.config import config_profiles_router, user_config_router from src.api.project import git_repositories_router, projects_router from src.api.system import ( dashboard_router, events_router, health_router, instance_proxy_router, notifications_router, terminal_router, ) from src.api.tool import ( sessions_router, tool_definitions_router, tool_instances_router, tool_lifecycle_router, tool_types_router, ) from src.api.user import auth_router, ssh_keys_router, users_router from src.api.workspace import ( all_workspaces_router, workspace_files_router, workspace_git_router, workspace_instances_router, workspaces_router, ) from src.config import Settings from src.models import Notification # noqa: F401 – Alembic model discovery from src.models import TerminalSessionModel # noqa: F401 – Alembic model discovery from src.database import init_database from src.logging_config import ( ExceptionLoggingMiddleware, RequestLoggingMiddleware, configure_logging, ) from src.seeds.builtin_tool_types import seed_builtin_tool_types from src.services.instance import InstanceEventBus, HealthMonitor from src.services.shared import CorrelationIdMiddleware # Configure logging early log_level = os.getenv("LOG_LEVEL", "INFO").upper() configure_logging(level=getattr(logging, log_level, logging.INFO)) logger = logging.getLogger(__name__) settings = Settings() app = FastAPI(title="Headquarter API") # Configure CORS - must be before other middleware # Build allowed origins list including web and api domains cors_origins = [settings.web_base_url] if settings.api_base_url != settings.web_base_url: cors_origins.append(settings.api_base_url) logger.info("CORS configured with origins: %s", cors_origins) app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(CorrelationIdMiddleware) app.add_middleware(RequestLoggingMiddleware) app.add_middleware(ExceptionLoggingMiddleware) def _sanitize_validation_errors(errors): """Convert validation errors to JSON-safe format.""" sanitized = [] for error in errors: safe_error = { "type": error.get("type"), "loc": error.get("loc"), "msg": error.get("msg"), "input": str(error.get("input")) if error.get("input") is not None else None, } # Convert ctx to safe format ctx = error.get("ctx") if ctx: safe_ctx = {} for key, value in ctx.items(): if isinstance(value, Exception): safe_ctx[key] = str(value) elif isinstance(value, (str, int, float, bool, type(None))): safe_ctx[key] = value else: safe_ctx[key] = str(value) safe_error["ctx"] = safe_ctx sanitized.append(safe_error) return sanitized @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Log validation errors and return detailed response.""" errors = exc.errors() logger.warning( "Validation error for %s %s: %s", request.method, request.url.path, errors, ) safe_errors = _sanitize_validation_errors(errors) return JSONResponse( status_code=422, content={"detail": safe_errors}, ) # Global services _event_bus = InstanceEventBus() _health_monitor = HealthMonitor(_event_bus) @app.on_event("startup") async def on_startup(): logger.info("Starting up Headquarter API...") # Initialize database (run migrations) db_ready = await init_database() if not db_ready: logger.error("Database initialization failed. Shutting down.") import sys sys.exit(1) # Start background health monitor _health_monitor.start() logger.info("Health monitor started") # Seed built-in tool types await seed_builtin_tool_types() logger.info("Built-in tool types seeded") logger.info("Startup complete.") @app.on_event("shutdown") async def on_shutdown(): logger.info("Shutting down Headquarter API...") _health_monitor.stop() logger.info("Health monitor stopped") logger.info("Shutdown complete.") app.include_router(health_router) app.include_router(auth_router) app.include_router(dashboard_router) app.include_router(projects_router) app.include_router(users_router) app.include_router(ssh_keys_router) app.include_router(git_repositories_router) app.include_router(user_config_router) app.include_router(tool_types_router) app.include_router(tool_definitions_router) app.include_router(config_profiles_router) app.include_router(tool_instances_router) app.include_router(tool_lifecycle_router) app.include_router(sessions_router) app.include_router(instance_proxy_router) app.include_router(terminal_router) app.include_router(events_router) app.include_router(notifications_router) app.include_router(all_workspaces_router) app.include_router(workspaces_router) app.include_router(workspace_files_router) app.include_router(workspace_git_router) app.include_router(workspace_instances_router) app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")