"""FastAPI application factory. Nothing in the inner layers imports FastAPI; this module wires them together. """ from __future__ import annotations import logging from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.config import settings from app.core.database import Base, engine from app.interfaces.http.errors import register_exception_handlers from app.interfaces.http.request_context import RequestContextMiddleware from app.interfaces.http.routers import ( admin, agy, auth, billing, health, knowledge, organizations, projects, sandbox, workforce, ) logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)-7s %(name)s: %(message)s", ) logger = logging.getLogger("agy.api") @asynccontextmanager async def lifespan(_: FastAPI): try: # In development we create any missing tables so `pnpm dev` works on a # fresh clone. Production schema changes go through Alembic # (`pnpm db:migrate`). if not settings.is_production: import app.infrastructure.db.models # noqa: F401 - register metadata async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) logger.info("Development schema ensured.") logger.info( "Adapters -> ai=%s git=%s domain=%s sandbox=%s payment=%s", settings.ai_default_provider, settings.git_provider, settings.domain_router, settings.sandbox_provider, settings.payment_provider, ) except Exception as exc: # Startup failures are the hardest to diagnose after the fact: the # process is gone and all that remains is whatever reached stderr, which # a container runtime routinely discards. Record it, then still fail # loudly — a half-started API must not accept traffic. await _record_startup_failure(exc) raise yield async def _record_startup_failure(exc: BaseException) -> None: """Persist a boot failure. Never raises; the caller re-raises regardless.""" try: from app.domain.audit.models import ErrorSeverity, ErrorSource from app.infrastructure.error_log import record_error await record_error( kind="startup_failed", message=str(exc) or type(exc).__name__, exc=exc, source=ErrorSource.STARTUP, severity=ErrorSeverity.CRITICAL, metadata={"app_env": settings.app_env}, ) except Exception: # pragma: no cover - boot is already failing logger.exception("Could not record the startup failure.") def create_app() -> FastAPI: application = FastAPI( title=settings.app_name, version="0.1.0", description="AI Workforce Platform — standalone multi-tenant SaaS.", lifespan=lifespan, docs_url="/docs", openapi_url="/openapi.json", ) # Starlette applies middleware outermost-first in reverse registration # order, so the last one added wraps everything else. The request-context # middleware goes last on purpose: that way even a CORS rejection gets an # ``X-Request-ID`` — and a CORS rejection is exactly the kind of opaque # failure where a correlatable id is worth having. application.add_middleware( CORSMiddleware, allow_origins=settings.cors_origin_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], # Without this the browser cannot read the header off a cross-origin # response, and the id would be useless to the web client. expose_headers=["X-Request-ID"], ) application.add_middleware(RequestContextMiddleware) register_exception_handlers(application) application.include_router(health.router) application.include_router(auth.router, prefix="/api") application.include_router(organizations.router, prefix="/api") application.include_router(projects.router, prefix="/api") application.include_router(workforce.router, prefix="/api") application.include_router(billing.router, prefix="/api") application.include_router(knowledge.router, prefix="/api") application.include_router(sandbox.router, prefix="/api") application.include_router(admin.router, prefix="/api") application.include_router(agy.router, prefix="/api") @application.get("/", tags=["meta"]) async def root() -> dict: return { "name": settings.app_name, "version": "0.1.0", "docs": "/docs", "health": "/health", } return application app = create_app()