Files
classic-models-agent-platform/CLAUDE.md
T
Stéphane Tailland ddc0f24cb9 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

181 lines
7.2 KiB
Markdown

# 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.
**Nothing runs from this repo.** It holds the `${VAR}`-parameterised manifests a
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.
| Agent | Repo | Framework | Mode |
|---|---|---|---|
| Assistant | `classic-models-agent-assistant` | LangGraph | conversational |
| Ventes | `classic-models-agent-ventes` | CrewAI | conversational |
| Catalogue | `classic-models-agent-catalogue` | PydanticAI + FastA2A | conversational |
| Support | `classic-models-agent-support` | Google ADK | conversational |
| Approvisionnement | `classic-models-agent-flows` | n8n | autonomous |
| Recouvrement | `classic-models-agent-flows` | n8n | autonomous |
Five runtimes is deliberate. The point is proving A2A interoperability between
stacks that do not know each other; a single framework would prove nothing.
## Topology
A2A between agents, MCP between an agent and data.
```
Assistant ──→ every agent (orchestration; holds no scopes itself)
Ventes ──→ Approvisionnement, Recouvrement
Support ──→ Ventes
Catalogue ──→ Approvisionnement
```
**An A2A call returns an answer, not data.** Ventes asks Approvisionnement "can
I promise delivery on 15 March?" and receives yes/no — it never gains read
access to the catalog domain. Agent Cards therefore expose narrow,
decision-shaped skills (`check_availability`, `check_creditworthiness`), never a
generic `query`. An agent that exposes "ask me anything about my domain" has
become a data proxy and defeats the scoping.
The Assistant holds **no scopes at all**. It is the component that sees raw user
text, so it is the prompt-injection target, and it must be unable to exfiltrate
anything.
## Identity
Each agent is its own Keycloak client. That is what makes the actor
identifiable: the exchanged token carries `azp` = the agent's client id
alongside `sub` = the end user.
```json
{ "sub": "alice", "azp": "classic-models-agent-ventes",
"aud": ["mcp-catalog"], "scope": "openid profile email mcp-catalog-audience" }
```
Two measured facts that constrain the implementation:
**`actor_token` is silently ignored.** Keycloak 26.6's Standard Token Exchange
accepts the parameter, returns a valid token, and emits **no `act` claim**. No
error. Do not build delegation on RFC 8693 `act` — use `azp`.
**The audience is requested via a scope, not the `audience` parameter.** Passing
`audience=mcp-catalog` returns `Requested audience not available`. Pass
`scope=openid mcp-catalog-audience`.
### 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.
```
POST https://keycloak.sttlab.local/realms/classic-models/protocol/openid-connect/token
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
client_id=<the agent's client id>
client_secret=<from the agent's Secret>
subject_token=<the end user's access token>
subject_token_type=urn:ietf:params:oauth:token-type:access_token
scope=openid mcp-<domain>-audience
```
Neither LangGraph nor CrewAI injects a per-call MCP token natively
(langchain-mcp-adapters issues #172/#194/#235; CrewAI's `MCPServerAdapter` fixes
headers at construction). This is a non-issue: build the MCP client **once per
conversation turn** with freshly exchanged tokens. A turn lasts seconds, an
access token five minutes.
## Endpoints
MCP servers, one per domain, served by Kong:
| Domain | URL | Audience scope |
|---|---|---|
| catalog | `https://catalog-dev.sttlab.local/catalog-mcp` | `mcp-catalog-audience` |
| customers | `https://customers-dev.sttlab.local/customers-mcp` | `mcp-customers-audience` |
| payments | `https://payments-dev.sttlab.local/payments-mcp` | `mcp-payments-audience` |
| sales | `https://sales-dev.sttlab.local/sales-mcp` | `mcp-sales-audience` |
| workforce | `https://workforce-dev.sttlab.local/workforce-mcp` | `mcp-workforce-audience` |
LLM inference goes through the Kong AI gateway, never direct to a 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.
These hostnames are lab-only with a self-signed certificate. `/etc/hosts`:
```
192.168.139.103 catalog-dev.sttlab.local
192.168.139.105 llm-dev.sttlab.local
192.168.139.106 customers-dev.sttlab.local
192.168.139.107 payments-dev.sttlab.local
192.168.139.108 sales-dev.sttlab.local
192.168.139.109 workforce-dev.sttlab.local
192.168.139.101 keycloak.sttlab.local
```
The CA is `sttlab-local-ca` (cert-manager ClusterIssuer). Trust that one CA
rather than disabling verification.
## Adding an agent
An agent is a declaration first. Put it here, let the generator produce the rest
— the Keycloak client is generated from this, never hand-written in the agent's
own repo, so the realm stays the single source of truth.
```yaml
name: ventes
client_id: classic-models-agent-ventes
framework: crewai
surfaces: [conversational]
mcp_servers: [catalog, sales]
scopes: [catalog:read, sales:read, sales:write]
a2a_calls: [approvisionnement, recouvrement]
limits:
max_tool_calls_per_run: 20
max_records_read_per_run: 500
```
`scopes` becomes the client's `optionalClientScopes`; `mcp_servers` becomes the
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.
## Known blockers
**Konnect is not persisting configuration.** The operator loops on the same
objects without converging and the dataplanes stay behind the control plane, so
no new Kong config reaches the gateways — including any new agent audience.
Resolve this before expecting an end-to-end run.
**No `employeeNumber` in tokens.** Sales territory scoping (a rep sees their
territory, a manager sees all) resolves through
`employee → officeCode → offices.territory`, but the token carries no employee
identity, so the API cannot filter server-side. The realm's users
(`admin-user`, `pm-user`, …) are role placeholders, not real employees.
**The `kong-token-exchange-broker` secret does not match the realm** — its
`client_credentials` returns `unauthorized_client`.
## Conventions
- Deliverables in English: code, comments, READMEs, Agent Cards. Conversation
with the maintainer is in French.
- GitOps throughout. Never `kubectl apply` an object carrying
`kustomize.toolkit.fluxcd.io/name` — Flux reverts it within its interval, and
tests run against config that no longer exists.
- Secrets never committed. Follow the `secret.example.yaml` pattern used by the
`infra-*` repos.