app.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. """
  2. agentpaas.api.app — FastAPI application entry point.
  3. """
  4. from __future__ import annotations
  5. import os
  6. import time
  7. from typing import Dict
  8. from collections import defaultdict
  9. import time as _time
  10. from fastapi import FastAPI, Request
  11. from fastapi.middleware.cors import CORSMiddleware
  12. from agentpaas.config import settings
  13. from agentpaas.api.v1 import agents, auth, admin, billing, traces, jobs, discovery, metrics, status, feishu
  14. from agentpaas.observability.logging import logger
  15. app = FastAPI(
  16. title="AgentPaaS",
  17. description="Agent Platform as a Service — powered by lambdagent",
  18. version="0.1.0",
  19. docs_url="/docs",
  20. redoc_url="/redoc",
  21. )
  22. # SEC-02: CORS whitelist (NOT allow_origins=["*"])
  23. _cors_origins = os.getenv("AGENTPAAS_CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000").split(",")
  24. app.add_middleware(
  25. CORSMiddleware,
  26. allow_origins=_cors_origins,
  27. allow_credentials=True,
  28. allow_methods=["GET", "POST", "PUT", "DELETE"],
  29. allow_headers=["Content-Type", "Authorization"],
  30. )
  31. # SEC-06: Rate limiting middleware
  32. _rate_store: Dict[str, list] = defaultdict(list)
  33. @app.middleware("http")
  34. async def rate_limit(request: Request, call_next):
  35. """Per-API-key rate limiting (sliding window)."""
  36. api_key = request.headers.get("authorization", "")[:20] # Use prefix as key
  37. if not api_key or request.url.path in ("/health", "/", "/docs", "/redoc"):
  38. return await call_next(request)
  39. now = _time.time()
  40. window = 60 # 1 minute window
  41. max_requests = settings.default_tenant_rate_limit
  42. # Clean old entries
  43. _rate_store[api_key] = [t for t in _rate_store[api_key] if now - t < window]
  44. if len(_rate_store[api_key]) >= max_requests:
  45. from fastapi.responses import JSONResponse
  46. return JSONResponse(
  47. status_code=429,
  48. content={"error": {"code": "RATE_LIMITED", "message": f"Rate limit exceeded ({max_requests}/min)"}},
  49. headers={"Retry-After": "60"},
  50. )
  51. _rate_store[api_key].append(now)
  52. return await call_next(request)
  53. # SEC-10: HTTPS enforcement in production
  54. if not settings.debug:
  55. @app.middleware("http")
  56. async def enforce_https(request: Request, call_next):
  57. if request.headers.get("x-forwarded-proto") == "http":
  58. from fastapi.responses import JSONResponse
  59. return JSONResponse(
  60. status_code=421,
  61. content={"error": {"code": "HTTPS_REQUIRED", "message": "HTTPS is required in production"}},
  62. )
  63. return await call_next(request)
  64. @app.middleware("http")
  65. async def add_timing(request: Request, call_next):
  66. t0 = time.time()
  67. response = await call_next(request)
  68. ms = int((time.time() - t0) * 1000)
  69. response.headers["X-Response-Time-Ms"] = str(ms)
  70. logger.info(f"{request.method} {request.url.path} -> {response.status_code} ({ms}ms)")
  71. return response
  72. # SEC-23: Security response headers
  73. @app.middleware("http")
  74. async def security_headers(request: Request, call_next):
  75. """SEC-23: Add security response headers."""
  76. response = await call_next(request)
  77. response.headers["X-Content-Type-Options"] = "nosniff"
  78. response.headers["X-Frame-Options"] = "DENY"
  79. response.headers["X-XSS-Protection"] = "1; mode=block"
  80. response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
  81. if not settings.debug:
  82. response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
  83. response.headers["Content-Security-Policy"] = "default-src 'self'"
  84. return response
  85. # SEC-24: Request ID tracking
  86. import uuid as _uuid
  87. @app.middleware("http")
  88. async def request_id(request: Request, call_next):
  89. """SEC-24: Unique request ID for log correlation."""
  90. req_id = request.headers.get("X-Request-ID", str(_uuid.uuid4()))
  91. response = await call_next(request)
  92. response.headers["X-Request-ID"] = req_id
  93. return response
  94. app.include_router(agents.router, prefix="/api/v1")
  95. app.include_router(jobs.router, prefix="/api/v1")
  96. app.include_router(auth.router, prefix="/api/v1")
  97. app.include_router(admin.router, prefix="/api/v1")
  98. app.include_router(billing.router, prefix="/api/v1")
  99. app.include_router(traces.router, prefix="/api/v1")
  100. app.include_router(metrics.router, prefix="/api/v1")
  101. app.include_router(discovery.router)
  102. app.include_router(status.router, prefix="/api/v1")
  103. app.include_router(feishu.router, prefix="/api/v1")
  104. @app.get("/health")
  105. async def health():
  106. return {"status": "ok", "version": "0.1.0"}
  107. @app.get("/")
  108. async def root():
  109. return {"service": "AgentPaaS", "version": "0.1.0", "docs": "/docs",
  110. "description": "Agent Platform as a Service — Every agent is a Lambda term."}