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>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user