-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.py
More file actions
157 lines (121 loc) · 5.45 KB
/
Copy pathmain.py
File metadata and controls
157 lines (121 loc) · 5.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
"""FastAPI application entrypoint.
Run from repository root with PYTHONPATH set to project root, e.g.:
PYTHONPATH=. uv run uvicorn simulation.api.main:app --reload
"""
import asyncio
import logging
import os
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from slowapi.errors import RateLimitExceeded
from starlette.datastructures import MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send
from db.adapters.sqlite.sqlite import get_db_path, initialize_database
from lib.env_utils import is_local_mode, parse_bool_env
from lib.rate_limiting import limiter, rate_limit_exceeded_handler
from lib.request_logging import log_request_start
from lib.security_headers import _hsts_enabled
from simulation.api.context import build_app_context
from simulation.api.dependencies.auth import disallow_auth_bypass_in_production
from simulation.api.exception_handlers import EXCEPTION_HANDLERS
from simulation.api.routes.simulation import router as simulation_router
from simulation.local_dev.local_mode import disallow_local_mode_in_production
from simulation.local_dev.seed_loader import seed_local_db_if_needed
DEFAULT_ALLOWED_ORIGINS: str = "http://localhost:3000,http://127.0.0.1:3000"
logger = logging.getLogger(__name__)
def _reset_local_db_if_requested() -> None:
"""When LOCAL_RESET_DB=1 in local mode, delete the local dummy DB file."""
db_path = get_db_path()
if os.path.exists(db_path):
logger.warning("LOCAL_RESET_DB=1: deleting local dummy DB at %s", db_path)
os.remove(db_path)
else:
logger.info(
"LOCAL_RESET_DB=1: dummy DB did not exist (nothing to delete): %s",
db_path,
)
def _ensure_local_seed_data() -> None:
"""When in local mode, ensure seed data is loaded in the dummy DB."""
db_path = get_db_path()
logger.info("LOCAL=true: ensuring seed data in dummy DB at %s", db_path)
seed_local_db_if_needed(db_path=db_path)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize database and simulation engine on startup."""
await asyncio.to_thread(disallow_auth_bypass_in_production)
await asyncio.to_thread(disallow_local_mode_in_production)
if is_local_mode() and parse_bool_env("LOCAL_RESET_DB"):
await asyncio.to_thread(_reset_local_db_if_requested)
await asyncio.to_thread(initialize_database)
if is_local_mode():
await asyncio.to_thread(_ensure_local_seed_data)
app.state.deps = await asyncio.to_thread(build_app_context)
yield
class SecurityHeadersMiddleware:
"""Pure ASGI middleware that adds security headers to every HTTP response."""
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def send_with_headers(message: Message) -> None:
if message["type"] == "http.response.start":
headers = MutableHeaders(scope=message)
headers["X-Content-Type-Options"] = "nosniff"
headers["X-Frame-Options"] = "DENY"
if _hsts_enabled():
headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
await send(message)
await self.app(scope, receive, send_with_headers)
class RequestIdMiddleware:
"""Pure ASGI middleware that assigns a request ID and logs request start."""
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
request = Request(scope)
request_id = request.headers.get("X-Request-ID") or uuid.uuid4().hex
request.state.request_id = request_id
log_request_start(
request_id=request_id,
method=request.method,
path=request.url.path,
)
async def send_with_request_id(message: Message) -> None:
if message["type"] == "http.response.start":
headers = MutableHeaders(scope=message)
headers["X-Request-ID"] = request_id
await send(message)
await self.app(scope, receive, send_with_request_id)
app = FastAPI(
title="Agent Simulation Platform API",
lifespan=lifespan,
)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler) # type: ignore[reportArgumentType]
for exc_class, handler_func in EXCEPTION_HANDLERS.items():
app.add_exception_handler(exc_class, handler_func) # type: ignore[reportArgumentType]
_allowed_origins_raw: str = os.environ.get("ALLOWED_ORIGINS", DEFAULT_ALLOWED_ORIGINS)
_allowed_origins: list[str] = [
origin.strip() for origin in _allowed_origins_raw.split(",") if origin.strip()
]
app.add_middleware(
CORSMiddleware,
allow_origins=_allowed_origins,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(RequestIdMiddleware)
app.include_router(simulation_router, prefix="/v1")
@app.get("/health")
def health():
"""Health check endpoint. Returns 200 when the service is up."""
return {"status": "ok"}