diff --git a/core/src/agentforge_core/api/agents.py b/core/src/agentforge_core/api/agents.py new file mode 100644 index 0000000..e57bbde --- /dev/null +++ b/core/src/agentforge_core/api/agents.py @@ -0,0 +1,5 @@ +"""Router stub de /agents — se implementa en una task posterior.""" + +from fastapi import APIRouter + +router = APIRouter() diff --git a/core/src/agentforge_core/api/executions.py b/core/src/agentforge_core/api/executions.py new file mode 100644 index 0000000..87a2df3 --- /dev/null +++ b/core/src/agentforge_core/api/executions.py @@ -0,0 +1,5 @@ +"""Router stub de /executions — se implementa en una task posterior.""" + +from fastapi import APIRouter + +router = APIRouter() diff --git a/core/src/agentforge_core/api/middlewares.py b/core/src/agentforge_core/api/middlewares.py new file mode 100644 index 0000000..c88cd4b --- /dev/null +++ b/core/src/agentforge_core/api/middlewares.py @@ -0,0 +1,27 @@ +"""Middleware de propagación de `trace_id` en cada request HTTP.""" + +from __future__ import annotations + +from uuid import uuid4 + +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.requests import Request +from starlette.responses import Response + +from agentforge_core.observability.logging import bind_trace_id, clear_trace_id + + +class TraceIdMiddleware(BaseHTTPMiddleware): + """Lee o genera `X-Trace-Id`, lo bind-ea al contexto de structlog y lo devuelve en la respuesta.""" + + async def dispatch( + self, request: Request, call_next: RequestResponseEndpoint + ) -> Response: + trace_id = request.headers.get("X-Trace-Id") or str(uuid4()) + bind_trace_id(trace_id) + try: + response = await call_next(request) + response.headers["X-Trace-Id"] = trace_id + return response + finally: + clear_trace_id() diff --git a/core/src/agentforge_core/api/policies.py b/core/src/agentforge_core/api/policies.py new file mode 100644 index 0000000..a307683 --- /dev/null +++ b/core/src/agentforge_core/api/policies.py @@ -0,0 +1,5 @@ +"""Router stub de /policies — se implementa en una task posterior.""" + +from fastapi import APIRouter + +router = APIRouter() diff --git a/core/src/agentforge_core/api/violations.py b/core/src/agentforge_core/api/violations.py new file mode 100644 index 0000000..e38b5be --- /dev/null +++ b/core/src/agentforge_core/api/violations.py @@ -0,0 +1,5 @@ +"""Router stub de /violations — se implementa en una task posterior.""" + +from fastapi import APIRouter + +router = APIRouter() diff --git a/core/src/agentforge_core/main.py b/core/src/agentforge_core/main.py new file mode 100644 index 0000000..018d575 --- /dev/null +++ b/core/src/agentforge_core/main.py @@ -0,0 +1,36 @@ +"""Composición raíz de la aplicación FastAPI (agentforge-core).""" + +from __future__ import annotations + +from fastapi import FastAPI + +from agentforge_core.api.middlewares import TraceIdMiddleware +from agentforge_core.config import Settings +from agentforge_core.observability.logging import configure_logging + + +def create_app() -> FastAPI: + """Construye la app: logging, middleware de trace_id, /health y routers.""" + settings = Settings() + configure_logging(level=settings.log_level) + app = FastAPI( + title="AgentForge Core", + version="0.1.0", + description="Plataforma de gobernanza de agentes IA — API REST.", + ) + app.add_middleware(TraceIdMiddleware) + + @app.get("/health", tags=["meta"]) + async def health() -> dict[str, str]: + return {"status": "ok"} + + from agentforge_core.api import agents, executions, policies, violations + + app.include_router(agents.router, prefix="/agents", tags=["agents"]) + app.include_router(executions.router, prefix="/executions", tags=["executions"]) + app.include_router(policies.router, prefix="/policies", tags=["policies"]) + app.include_router(violations.router, prefix="/violations", tags=["violations"]) + return app + + +app = create_app() diff --git a/tests/unit/test_health.py b/tests/unit/test_health.py new file mode 100644 index 0000000..4124b41 --- /dev/null +++ b/tests/unit/test_health.py @@ -0,0 +1,24 @@ +"""Tests del endpoint /health y del TraceIdMiddleware.""" + +from fastapi.testclient import TestClient + +from agentforge_core.main import create_app + + +def test_health_responde_ok() -> None: + client = TestClient(create_app()) + r = client.get("/health") + assert r.status_code == 200 + assert r.json() == {"status": "ok"} + + +def test_trace_id_header_se_devuelve_generado() -> None: + client = TestClient(create_app()) + r = client.get("/health") + assert r.headers.get("x-trace-id") + + +def test_trace_id_header_se_propaga_si_viene_en_la_request() -> None: + client = TestClient(create_app()) + r = client.get("/health", headers={"X-Trace-Id": "fixed-123"}) + assert r.headers["x-trace-id"] == "fixed-123"