docs: stop naming the middleware

An agent talks to URLs that honour a contract: an MCP endpoint, an OIDC token
endpoint, an OpenAI-shaped LLM endpoint. Whatever serves those URLs is
replaceable, and naming it in an agent's code or docs implies a coupling that
does not exist.

Every product name is gone from prose, comments and identifiers, replaced by
what the agent actually depends on. Where a behaviour of the current middleware
leaks into the contract — the second audience scope, the tool ACL — the text
now says what the caller must do and why, not who imposes it.

One string survives and cannot be fixed here: the broker client an agent must
name in its audience scopes is itself named after the middleware. Closing that
means renaming it in Keycloak and following the rename through the gateway
configuration and LibreChat. Recorded as a blocker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Stéphane Tailland
2026-08-29 07:35:13 +02:00
parent 92f0a272a5
commit 3e4e8e363f
+47 -28
View File
@@ -1,8 +1,8 @@
# classic-models-agent-platform
Shared ground for the classic-models agent estate: templates, conventions, and
the identity plumbing every agent repeats. Plays the role `kong-k8s-platform`
plays for the gateways.
the identity plumbing every agent repeats. What `k8s-platform` is to the
gateway estate, this is to the agent estate.
**Nothing runs from this repo.** It holds the `${VAR}`-parameterised manifests a
domain's Flux `Kustomization` builds, plus the reference documentation below.
@@ -10,8 +10,15 @@ domain's Flux `Kustomization` builds, plus the reference documentation below.
## The estate this plugs into
The agents replace LibreChat as the MCP client and orchestrator. Everything
below them already exists and does not change: Kong gateways per domain, MCP
servers served by Kong's `ai-mcp-proxy`, Keycloak, and the domain APIs.
below them already exists and does not change: an MCP server per domain, an
OpenAI-shaped LLM endpoint, Keycloak, and the domain APIs behind them.
**Name none of the middleware.** An agent talks to URLs that honour a contract.
Whatever serves those URLs today is replaceable — swap it and nothing in an
agent changes, as long as the contract holds. So write "the MCP server", "the
LLM endpoint", "the gateway"; never a product name, in code, comments, commit
messages or docs. Where a behaviour of the current middleware leaks into the
contract, say what the caller must do and why, not who imposes it.
| Agent | Repo | Framework | Mode |
|---|---|---|---|
@@ -71,9 +78,10 @@ error. Do not build delegation on RFC 8693 `act` — use `azp`.
### The exchange
Do what LibreChat already does — a POST per target audience, roughly twenty
lines. Do not build a sidecar or move the exchange into Kong; both were
considered and rejected as over-engineering, the second also because it would
leave the agent holding one broad token instead of several narrow ones.
lines. Do not build a sidecar, and do not push the exchange down into the
gateway; both were considered and rejected as over-engineering, the second also
because it would leave the agent holding one broad token instead of several
narrow ones.
```
POST https://keycloak.sttlab.local/realms/classic-models/protocol/openid-connect/token
@@ -93,7 +101,7 @@ access token five minutes.
## Endpoints
MCP servers, one per domain, served by Kong:
MCP servers, one per domain:
| Domain | URL | Audience scopes to request |
|---|---|---|
@@ -103,22 +111,34 @@ MCP servers, one per domain, served by Kong:
| sales | `https://sales-dev.sttlab.local/sales-mcp` | `mcp-sales-audience kong-token-exchange-broker-audience` |
| workforce | `https://workforce-dev.sttlab.local/workforce-mcp` | `mcp-workforce-audience kong-token-exchange-broker-audience` |
**Two scopes per server, not one.** The gateway's own `openid-connect` plugin
exchanges the token again before reaching the backend, so the token must also
name `kong-token-exchange-broker`. Without it the call fails with a bare `401`
and the real cause appears only in the gateway log — *invalid status code
received from the token endpoint (403)*. Measured on catalog: with both scopes
the MCP server answers `200`, with `mcp-catalog-audience` alone it answers
`401`. LibreChat has always requested both.
**Two scopes per server, not one.** The MCP endpoint exchanges the token again
on its own account before reaching the domain API, so the token must also name
the broker client that performs that second exchange. Without it the call fails
with a bare `401` and no usable detail — the real cause (*invalid status code
received from the token endpoint (403)*) appears only in the server's own log.
Measured on catalog: with both scopes the MCP server answers `200`, with
`mcp-catalog-audience` alone it answers `401`. LibreChat has always requested
both.
LLM inference goes through the Kong AI gateway, never direct to a provider:
This is a property of the contract as it stands, not of any agent's design. The
broker client is still named after the middleware that introduced it, which is
a leak worth closing — see the blockers.
LLM inference goes through the estate's LLM endpoint, never direct to a model
provider:
```
https://llm-dev.sttlab.local/llm/v1/chat/completions
```
It applies rate limiting, prompt guards and semantic guards, and routes to
OpenRouter. Sending inference anywhere else bypasses all of them.
It speaks the OpenAI protocol, applies rate limiting and prompt guards, and
routes onward to a provider. Sending inference anywhere else bypasses all of
that, and loses the audit trail with it.
Authenticate to it with an exchanged token carrying the `llm-proxy` audience,
not a static API key. A key proves which application is calling; the token
carries `azp` and `sub`, so inference is attributable to the same (agent, end
user) pair as every tool call.
These hostnames are lab-only with a self-signed certificate. `/etc/hosts`:
@@ -158,8 +178,8 @@ limits:
audience scopes it may request; `a2a_calls` becomes the A2A authorisations.
The realm actually deployed is `infra-keycloak/k8s/keycloak/configmap-realm.yaml`
— note that `kong-v2/classic-models-realm.yaml` is an older partial copy and is
**not** authoritative.
— note that the copy under the gateway estate is older, partial, and **not**
authoritative.
**The realm file is re-imported with `--override true` on every pod start.** An
`import-realm` initContainer replays it before Keycloak serves traffic, so the
@@ -194,14 +214,13 @@ territory, a manager sees all) resolves through
identity, so the API cannot filter server-side. The realm's users
(`admin-user`, `pm-user`, …) are role placeholders, not real employees.
**PydanticAI's MCP client cannot open a session against Kong's
`ai-mcp-proxy`.** It fails with `Failed to initialize server session` and
swallows the cause. Everything else was eliminated: identity, TLS, DNS and the
token are all fine, and the three handshake steps — `initialize` (200 with a
session id), the SSE `GET` (200), `notifications/initialized` (202) — all
succeed with curl using the very token the agent minted. This blocks the
Catalogue agent end to end, and with it the Kong AI gateway path, which has
therefore never been exercised.
**The broker client is named after the middleware.** Every agent must request
`kong-token-exchange-broker-audience` alongside the server's own audience, so
the one thing an agent cannot avoid naming is the very thing it should not know
about. Closing it means renaming the client and its scope in Keycloak, and
following that rename through the gateway configuration, LibreChat and each
agent's config. Until then, treat the string as an opaque identifier owned by
the realm.
## Conventions