ba8892b0ad
First of the six agents, and the one that gives the A2A facade for the others to interoperate with. A turn exchanges the user's token once per audience, builds one MCP toolset per server with those tokens, runs the model through the Kong AI gateway, and drops the toolsets. Per-turn MCP clients are the point, not an implementation detail: a token that outlives its turn can be replayed after the session ended. Several narrow tokens rather than one broad one, so a prompt injection that talks the agent into the wrong domain is refused at Kong rather than succeeding quietly. a2a-sdk rather than FastA2A, revising the May decision. That decision rested on Agent.to_a2a() being removed in PydanticAI v2 — which has now happened, so the argument no longer separates them. With four other runtimes due to interoperate, the protocol author's implementation is the safer base. httpx2 rather than httpx, which PydanticAI deprecates for OpenAI-compatible providers and drops in v3. Four things about a2a-sdk 1.1 that no documentation gave and that were established against the running library, all recorded where they bite: a2a.types are protobuf, not pydantic; DefaultRequestHandler speaks 0.3 and V2 is needed for 1.0; a client must send A2A-Version: 1.0 or the server assumes 0.3 and answers VERSION_NOT_SUPPORTED on a perfectly valid request; and an executor must enqueue a Task before any status update. Verified: the app builds with no deprecation warnings, serves its Agent Card, and fails a turn cleanly with "no bearer token" rather than answering from the model's own knowledge — which would hide a broken identity chain behind a plausible reply. Seven tests cover the exchange shape (scope not audience, per-audience caching, refusal) and the card. The Keycloak client does not exist yet, so no end-to-end run has happened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
4.7 KiB
Markdown
114 lines
4.7 KiB
Markdown
# Catalogue
|
|
|
|
Product management agent for the Classic Models catalog domain. Serves a product
|
|
manager: what exists, how it is priced, whether it sells, what to discontinue.
|
|
|
|
Reached over **A2A**; reads data over **MCP** through the Kong gateways; thinks
|
|
through the **Kong AI gateway**. It is one MCP client among others, exactly as
|
|
LibreChat is — nothing under it changes to accommodate it.
|
|
|
|
## Running it
|
|
|
|
```bash
|
|
uv venv && uv pip install -e ".[dev]"
|
|
export CATALOGUE_CLIENT_SECRET=... # Keycloak client secret, no default
|
|
export CATALOGUE_CA_BUNDLE=/path/to/sttlab-local-ca.crt
|
|
python -m catalogue_agent
|
|
```
|
|
|
|
Everything else has a working default in `config.py` and is overridable with
|
|
`CATALOGUE_`-prefixed variables.
|
|
|
|
```bash
|
|
curl localhost:8080/.well-known/agent-card.json
|
|
|
|
curl -X POST localhost:8080/ \
|
|
-H 'Content-Type: application/json' \
|
|
-H 'A2A-Version: 1.0' \
|
|
-H "Authorization: Bearer $USER_TOKEN" \
|
|
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage",
|
|
"params":{"message":{"messageId":"m1","role":"ROLE_USER",
|
|
"parts":[{"text":"How many product lines are there?"}]}}}'
|
|
```
|
|
|
|
**`A2A-Version: 1.0` is required.** Without it the SDK assumes 0.3 and answers
|
|
`VERSION_NOT_SUPPORTED`, which reads as a protocol error even though the request
|
|
is fine.
|
|
|
|
## How a turn works
|
|
|
|
```
|
|
A2A SendMessage ──► exchange the user's token, once per audience
|
|
──► build one MCP toolset per server, with that turn's tokens
|
|
──► PydanticAI runs the model through the Kong AI gateway
|
|
──► answer, toolsets dropped
|
|
```
|
|
|
|
The MCP client is built **per turn**. A token that outlives the turn it was
|
|
minted for is a token that can be replayed after the user's session ended, and
|
|
holding one long-lived connection would mean one long-lived token.
|
|
|
|
Several narrow tokens rather than one broad one: a token minted for
|
|
`mcp-catalog` is refused by the sales gateway, so a prompt injection that talks
|
|
the agent into reaching the wrong domain fails at Kong rather than succeeding
|
|
quietly.
|
|
|
|
## What it can and cannot do
|
|
|
|
| | |
|
|
|---|---|
|
|
| Reads | catalog (products, product lines), sales (read-only, to judge whether a product sells) |
|
|
| Writes | **none yet** — the agent says so when asked |
|
|
| Calls | nothing yet. `Approvisionnement.check_availability` comes with discontinuation decisions |
|
|
| Exposes | `describe_catalog` |
|
|
|
|
Skills are decision-shaped on purpose. An agent exposing a generic `query` has
|
|
become a data proxy and defeats the per-domain scoping the estate is built on,
|
|
so skills get added when another agent has a specific question — not in advance.
|
|
|
|
## Two Keycloak facts this depends on
|
|
|
|
Both measured against Keycloak 26.6, both costly to rediscover:
|
|
|
|
**`actor_token` is silently ignored.** Standard Token Exchange accepts it,
|
|
returns a valid token and emits no `act` claim — no error. Delegation cannot be
|
|
built on RFC 8693 `act`; the actor is identified by `azp`, which carries this
|
|
agent's client id while `sub` stays the human.
|
|
|
|
**The audience is requested as a scope.** `audience=mcp-catalog` returns
|
|
"Requested audience not available". `scope=openid mcp-catalog-audience` works.
|
|
|
|
## Library choices
|
|
|
|
Pinned to what was current on 2026-08-28, ranges below the next major so patch
|
|
fixes arrive but an upgrade stays deliberate.
|
|
|
|
**`a2a-sdk` rather than FastA2A.** The earlier design said FastA2A, on the
|
|
grounds that `Agent.to_a2a()` was going away in PydanticAI v2 — which has since
|
|
happened, so that argument no longer distinguishes them. This estate exists to
|
|
prove A2A interoperability across five runtimes, and the reference
|
|
implementation is the safer thing for four other stacks to interoperate with.
|
|
|
|
**`httpx2` rather than `httpx`.** PydanticAI deprecates httpx clients for
|
|
OpenAI-compatible providers and removes them in v3.
|
|
|
|
Notes on `a2a-sdk` 1.1 that are not guessable, all established against the
|
|
running library:
|
|
|
|
- `a2a.types` are protobuf messages, not pydantic models.
|
|
- Use `DefaultRequestHandlerV2`; `DefaultRequestHandler` speaks 0.3.
|
|
- JSON-RPC methods are `SendMessage`, `GetTask`… The 0.3 spelling
|
|
(`message/send`) still works — `enable_v0_3_compat` is on.
|
|
- An executor must enqueue a `Task` before any status update when
|
|
`context.current_task` is None.
|
|
|
|
## Not done yet
|
|
|
|
- **The Keycloak client does not exist.** `classic-models-agent-catalogue` has
|
|
to be declared in `infra-keycloak/k8s/keycloak/configmap-realm.yaml`, with
|
|
`mcp-catalog-audience` and `mcp-sales-audience` as optional scopes. Until then
|
|
the exchange fails and the agent says so.
|
|
- Writes, and the A2A call to Approvisionnement.
|
|
- Deployment manifests — they belong in `classic-models-agent-platform` once a
|
|
second agent shows what is actually shared.
|