| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- """
- agentpaas.api.app — FastAPI application entry point.
- """
- from __future__ import annotations
- import os
- import time
- from typing import Dict
- from collections import defaultdict
- import time as _time
- from fastapi import FastAPI, Request
- from fastapi.middleware.cors import CORSMiddleware
- from agentpaas.config import settings
- from agentpaas.api.v1 import agents, auth, admin, billing, traces, jobs, discovery, metrics, status, feishu
- from agentpaas.observability.logging import logger
- app = FastAPI(
- title="AgentPaaS",
- description="Agent Platform as a Service — powered by lambdagent",
- version="0.1.0",
- docs_url="/docs",
- redoc_url="/redoc",
- )
- # SEC-02: CORS whitelist (NOT allow_origins=["*"])
- _cors_origins = os.getenv("AGENTPAAS_CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000").split(",")
- app.add_middleware(
- CORSMiddleware,
- allow_origins=_cors_origins,
- allow_credentials=True,
- allow_methods=["GET", "POST", "PUT", "DELETE"],
- allow_headers=["Content-Type", "Authorization"],
- )
- # SEC-06: Rate limiting middleware
- _rate_store: Dict[str, list] = defaultdict(list)
- @app.middleware("http")
- async def rate_limit(request: Request, call_next):
- """Per-API-key rate limiting (sliding window)."""
- api_key = request.headers.get("authorization", "")[:20] # Use prefix as key
- if not api_key or request.url.path in ("/health", "/", "/docs", "/redoc"):
- return await call_next(request)
- now = _time.time()
- window = 60 # 1 minute window
- max_requests = settings.default_tenant_rate_limit
- # Clean old entries
- _rate_store[api_key] = [t for t in _rate_store[api_key] if now - t < window]
- if len(_rate_store[api_key]) >= max_requests:
- from fastapi.responses import JSONResponse
- return JSONResponse(
- status_code=429,
- content={"error": {"code": "RATE_LIMITED", "message": f"Rate limit exceeded ({max_requests}/min)"}},
- headers={"Retry-After": "60"},
- )
- _rate_store[api_key].append(now)
- return await call_next(request)
- # SEC-10: HTTPS enforcement in production
- if not settings.debug:
- @app.middleware("http")
- async def enforce_https(request: Request, call_next):
- if request.headers.get("x-forwarded-proto") == "http":
- from fastapi.responses import JSONResponse
- return JSONResponse(
- status_code=421,
- content={"error": {"code": "HTTPS_REQUIRED", "message": "HTTPS is required in production"}},
- )
- return await call_next(request)
- @app.middleware("http")
- async def add_timing(request: Request, call_next):
- t0 = time.time()
- response = await call_next(request)
- ms = int((time.time() - t0) * 1000)
- response.headers["X-Response-Time-Ms"] = str(ms)
- logger.info(f"{request.method} {request.url.path} -> {response.status_code} ({ms}ms)")
- return response
- # SEC-23: Security response headers
- @app.middleware("http")
- async def security_headers(request: Request, call_next):
- """SEC-23: Add security response headers."""
- response = await call_next(request)
- response.headers["X-Content-Type-Options"] = "nosniff"
- response.headers["X-Frame-Options"] = "DENY"
- response.headers["X-XSS-Protection"] = "1; mode=block"
- response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
- if not settings.debug:
- response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
- response.headers["Content-Security-Policy"] = "default-src 'self'"
- return response
- # SEC-24: Request ID tracking
- import uuid as _uuid
- @app.middleware("http")
- async def request_id(request: Request, call_next):
- """SEC-24: Unique request ID for log correlation."""
- req_id = request.headers.get("X-Request-ID", str(_uuid.uuid4()))
- response = await call_next(request)
- response.headers["X-Request-ID"] = req_id
- return response
- app.include_router(agents.router, prefix="/api/v1")
- app.include_router(jobs.router, prefix="/api/v1")
- app.include_router(auth.router, prefix="/api/v1")
- app.include_router(admin.router, prefix="/api/v1")
- app.include_router(billing.router, prefix="/api/v1")
- app.include_router(traces.router, prefix="/api/v1")
- app.include_router(metrics.router, prefix="/api/v1")
- app.include_router(discovery.router)
- app.include_router(status.router, prefix="/api/v1")
- app.include_router(feishu.router, prefix="/api/v1")
- @app.get("/health")
- async def health():
- return {"status": "ok", "version": "0.1.0"}
- @app.get("/")
- async def root():
- return {"service": "AgentPaaS", "version": "0.1.0", "docs": "/docs",
- "description": "Agent Platform as a Service — Every agent is a Lambda term."}
|