Commit Graph

6 Commits

Author SHA1 Message Date
Stéphane Tailland 9c3bdba3f9 feat: deploy the agent — image, manifests, and the Flux plumbing
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>
2026-08-28 17:50:02 +02:00
Stéphane Tailland 6ffab873e2 feat: enable catalog writes, with the conversation history they require
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>
2026-08-28 17:39:51 +02:00
Stéphane Tailland 0bc06512f5 fix: unblock the MCP session, and authenticate to the AI gateway with OIDC
Two things, both discovered by instrumenting the failure rather than trusting
the message it printed.

The MCP session never failed on protocol. macOS resolves any `.local` name
through mDNS first and takes a flat ~5.0s doing it, which sits exactly on
httpx's 5s default connect timeout; fastmcp reported the resulting
ConnectTimeout only as "Failed to initialize server session". Earlier tests
used `curl --resolve`, which bypasses DNS entirely and made it look like DNS
had been eliminated. Settings.http_timeout (60s) now covers the shared client,
the client fastmcp builds, and MCPToolset's init_timeout. list-products,
get-product and create-product now enumerate over the real gateway.

Inference used a static "unused" api_key against a gateway whose
openid-connect plugin requires audience ["llm-proxy"]. The model call now
carries a token exchanged for llm-proxy-audience, so it is attributable to the
same (agent, end user) pair as every tool call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 11:01:18 +02:00
Stéphane Tailland 4fd6e21a40 Request both the MCP and the broker audience
A token audienced only for mcp-catalog is refused by the gateway with a
bare 401. The reason is not visible to the caller: Kong's own
openid-connect plugin exchanges the token again before reaching the
backend, and Keycloak refuses that second exchange unless the token also
names kong-token-exchange-broker. The gateway log says "invalid status
code received from the token endpoint (403)"; the client sees only 401.

LibreChat already requests both audiences for the same reason. Proven end
to end with a pm-user token: aud=[mcp-catalog, kong-token-exchange-broker]
returns 200 from catalog-mcp, and the same request with aud=[mcp-catalog]
alone returns 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:14:33 +02:00
Stéphane Tailland ba8892b0ad Build the Catalogue agent: A2A in, MCP out, inference through Kong
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>
2026-08-28 08:45:11 +02:00
Stéphane Tailland 67d7bffd1c Add implementation instructions
Design agreed in discussion, nothing built yet. Records the agent's role, its
scopes and MCP servers, its A2A surface, and the framework choice with its
reason — so the next session starts from the decisions rather than re-deriving
them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:14:41 +02:00