"""HTTP adapter that turns any :class:`A2AAgent` into a service. This is intentionally minimal: it covers the surface needed to plug into the wider A2A ecosystem and the platform's control plane. Endpoints: GET /healthz -> liveness GET /.well-known/agent-card -> Agent Card JSON POST /invoke/{skill} -> invoke skill (JSON in, JSON out) Auth: a single bearer token is read from the ``A2A_API_KEY`` env var. If set, all routes except ``/healthz`` and the card require ``Authorization: Bearer ``. The bearer token is materialized into the agent's declared ``auth_model`` (best-effort: APIKeyAuth, otherwise NoAuth). """ from __future__ import annotations import os from typing import Any from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel from ..agent import A2AAgent, SkillInputError, SkillNotFound from ..auth import APIKeyAuth, NoAuth from ..context import LocalRunContext, MissingScopes class _InvokeIn(BaseModel): arguments: dict[str, Any] = {} def build_app(agent: A2AAgent) -> FastAPI: """Build a FastAPI app for the given agent instance.""" app = FastAPI(title=type(agent).name, version=type(agent).version) api_key = os.environ.get("A2A_API_KEY") def _build_auth(provided_key: str | None) -> Any: auth_cls = type(agent).auth_model if auth_cls is APIKeyAuth: return APIKeyAuth(api_key_id=provided_key or "anonymous") if auth_cls is NoAuth: return NoAuth() # Unknown auth model: try default-construct, else fail loudly. try: return auth_cls() except Exception as exc: # pragma: no cover - depends on user model raise HTTPException( status_code=500, detail=f"cannot materialize auth_model {auth_cls.__name__}: {exc}", ) from exc def _check_key(authorization: str | None) -> str | None: if api_key is None: return None if not authorization or not authorization.lower().startswith("bearer "): raise HTTPException(401, "missing bearer token") token = authorization.split(None, 1)[1].strip() if token != api_key: raise HTTPException(401, "invalid bearer token") return token @app.get("/healthz") async def healthz() -> dict[str, Any]: ok = await agent.health() return {"ok": ok, "agent": type(agent).name, "version": type(agent).version} @app.get("/.well-known/agent-card") async def agent_card() -> dict[str, Any]: return agent.card().model_dump(mode="json") @app.post("/invoke/{skill_name}") async def invoke( skill_name: str, body: _InvokeIn, authorization: str | None = Header(default=None), ) -> dict[str, Any]: token = _check_key(authorization) ctx: LocalRunContext[Any] = LocalRunContext( auth=_build_auth(token), task_id=f"http-{skill_name}" ) try: result = await agent.invoke_json(skill_name, ctx, body.arguments) except SkillNotFound: raise HTTPException(404, f"unknown skill: {skill_name}") except SkillInputError as exc: raise HTTPException(400, str(exc)) except MissingScopes as exc: raise HTTPException(403, str(exc)) return { "result": result, "events": [ {"kind": e.kind, "payload": e.payload} for e in ctx.events ], } return app def serve(agent: A2AAgent, *, host: str = "0.0.0.0", port: int = 8000) -> None: """Run the agent's HTTP server with uvicorn (blocking).""" import uvicorn uvicorn.run(build_app(agent), host=host, port=port, log_level="info")