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