Two-stage Dockerfile: the runtime image carries neither uv nor a build
toolchain, runs as uid 10001, drops every capability and needs no writable
filesystem (verified with --read-only and no tmpfs).
k8s/ is what runs, dev/ is the Flux plumbing that applies it, self-managing
after three manual applies — the same shape every kong-* repo settled on.
Three choices worth naming:
- One namespace for the estate. What isolates an agent is its Keycloak client
and the audiences it may request, not a namespace boundary.
- The GitRepository uses anonymous HTTPS rather than SSH with a Vault-held
deploy key. The repo is already anonymously readable on gitea, so a key would
guard nothing while adding two objects to create by hand before anything
deploys. If the repos ever go private this becomes the kong-* SSH pattern.
- The lab CA certificate is committed. It is a certificate, never a key, and
checking it in means the deployment has no hidden prerequisite.
replicas: 1 is a constraint, not a default — conversation history lives in the
process, so a second replica would route a user's "yes" to a pod that never saw
the question.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gateway already authorised writes for a product_manager — ai-mcp-proxy
filters tool discovery by consumer group, so create-product is simply absent
from a read-only user's turn. Only the system prompt was holding the agent
back, and it now describes how to write instead: restate the record, ask for
confirmation, never invent a missing field, report the identifier, and stop on
a refusal rather than working around it.
That flow cannot terminate without history. Each turn built a fresh Agent with
no message_history, so the user's "yes" arrived at an agent that had no idea
what it was agreeing to. History is now kept per A2A contextId in a bounded
in-memory map — enough for confirm-then-act, deliberately not durable, and
documented as needing sticky routing beyond one replica.
Verified end to end: pm-user creates S99_TEST1 across two turns and the row
appears in MySQL; ro-user asking for the same thing is told no write tool
exists, because for that token none does. Both test rows removed afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
First of the six agents, and the one that gives the A2A facade for the
others to interoperate with. A turn exchanges the user's token once per
audience, builds one MCP toolset per server with those tokens, runs the
model through the Kong AI gateway, and drops the toolsets.
Per-turn MCP clients are the point, not an implementation detail: a token
that outlives its turn can be replayed after the session ended. Several
narrow tokens rather than one broad one, so a prompt injection that talks
the agent into the wrong domain is refused at Kong rather than succeeding
quietly.
a2a-sdk rather than FastA2A, revising the May decision. That decision
rested on Agent.to_a2a() being removed in PydanticAI v2 — which has now
happened, so the argument no longer separates them. With four other
runtimes due to interoperate, the protocol author's implementation is the
safer base. httpx2 rather than httpx, which PydanticAI deprecates for
OpenAI-compatible providers and drops in v3.
Four things about a2a-sdk 1.1 that no documentation gave and that were
established against the running library, all recorded where they bite:
a2a.types are protobuf, not pydantic; DefaultRequestHandler speaks 0.3 and
V2 is needed for 1.0; a client must send A2A-Version: 1.0 or the server
assumes 0.3 and answers VERSION_NOT_SUPPORTED on a perfectly valid
request; and an executor must enqueue a Task before any status update.
Verified: the app builds with no deprecation warnings, serves its Agent
Card, and fails a turn cleanly with "no bearer token" rather than
answering from the model's own knowledge — which would hide a broken
identity chain behind a plausible reply. Seven tests cover the exchange
shape (scope not audience, per-audience caching, refusal) and the card.
The Keycloak client does not exist yet, so no end-to-end run has happened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>