diff --git a/CLAUDE.md b/CLAUDE.md index de42de7..64cf5d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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