Skip to content

5.3. A2A Gateway

How is A2A routed?

The route marks traffic with the native A2A policy and forwards it to the host server or Kubernetes service:

- port: 3001
  listeners:
    - name: a2a
      routes:
        - policies:
            a2a: {}
            localRateLimit:
              - maxTokens: 60
                tokensPerFill: 60
                fillInterval: 60s
          backends:
            - host: localhost:8080

The k3d/GKE configs replace only the backend with agentops-agent.agentops.svc.cluster.local:8080.

Why use A2A policy instead of generic HTTP proxying?

Protocol awareness lets the gateway interpret discovery/task traffic for policy and telemetry rather than treating it as opaque bytes. The application still owns task execution, streaming events, persistent task/session state, and the model-call budget.

Which address should clients use?

Command-line host clients use http://127.0.0.1:3001. Kubernetes learners port-forward the agentgateway service and use the same local port. The direct agent :8080 is an internal diagnostic boundary, not the documented client route.

curl -fsS http://127.0.0.1:3001/.well-known/agent-card.json \
  | jq '{name,version,capabilities,skills}'

Expected name: Ops Copilot; expected skills: incident triage and guarded remediation.

How do I chat with the agent from a browser?

The repository ships a self-built, single-file A2A web client in clients/web/ (vanilla JavaScript, no build step, no external requests, MIT licensed). It discovers the agent card, streams message/stream events over SSE with a message/send fallback, and renders each task state distinctly. Its incremental parser accepts the CRLF, LF, and CR separators allowed by SSE; the locked A2A server emits CRLF, and the server/client regression test preserves that wire contract.

Browsers enforce CORS, and neither the raw A2A server nor the a2a/localRateLimit policies emit CORS headers (the raw server answers preflight OPTIONS with 405). The checked-in agentgateway profiles include a route-level cors policy for exactly this case, so the browser path stays on the governed :3001 route:

cors:
  allowOrigins:
    - "http://localhost:8001"
  allowMethods:
    - GET
    - POST
    - OPTIONS
  allowHeaders:
    - content-type
  1. Start the A2A server (mise run a2a from agents/python) and the gateway (mise run gateway:host from the repository root).
  2. Serve the client from the repository root: mise run client:web.
  3. Open http://localhost:8001 (the exact checked-in origin), keep the base URL http://localhost:3001, and press Connect.

The gateway answers the preflight itself and stamps access-control-allow-origin on the card, RPC, and SSE responses. No CORS policy means the browser blocks every call — curl keeps working because CORS is a browser-side control. Keep the exact origin instead of broadening it to *, especially if you later add credentials.

What does the web client show for a guarded action?

Asking for a mock remediation (for example "Restart the inventory service.") pauses the task in the input-required state. The stream's final event carries a long-running adk_request_confirmation function call whose arguments include the original call and its confirmation hint. The client keeps the preceding evidence/tool results visible, repeats the exact action arguments, warns that current state is revalidated at execution, and requires a rationale — the agent refuses approvals without one (Chapter 4.5).

Approving sends a FunctionResponse data part on the same taskId/contextId with {"confirmed": true, "payload": {"rationale": "..."}}; denying sends "confirmed": false. The audit trail is still enriched server-side in the same transaction as the action. On the unauthenticated default path, its approver is a synthetic A2A context user, not a verified person. This browser round-trip requires a model backend (local Qwen3 through Ollama or a configured provider), unlike the deterministic fake-model integration test.

How are long-running tasks bounded?

The gateway limits request arrival rate. The A2A application independently sets max_llm_calls from AGENT_A2A_MAX_LLM_CALLS (default 12), persists sessions/tasks in SQLite, and closes resources during shutdown. These controls address different failure modes and should not be collapsed into one timeout.

What is still missing for public A2A?

The default A2A path creates no public endpoint, client authentication, TLS, per-tenant authorization, request-size policy, or distributed task store. Port-forwarding/loopback keep the lab private. Chapter 5.5 adds an opt-in local authenticated/TLS profile, but it does not turn this into a public edge. Adding public exposure before production identity and abuse controls would be a regression.

What is the A2A checkpoint?

Run cd agents/python && uv run pytest tests/test_server.py to lock the SSE and confirmation/resume wire contracts with a fake model. Then fetch the agent card through :3001, stop the A2A backend, and confirm the gateway returns a failure rather than a stale success. Restart it and verify the card again. A live model is not needed for either checkpoint.