feat: the orchestrating agent

Entry point for the estate: understands a request, consults the specialists
that can settle it, composes one answer. LangGraph 1.2.11, a2a-sdk 1.1.2, both
current as of August 2026.

It holds no data, and the realm is what enforces that rather than the prompt.
classic-models-agent-assistant is granted two optional scopes — one per
specialist, plus the inference endpoint — and every domain audience it could
request comes back Invalid scopes. An injection that succeeds here finds no
path to a domain because there is none. "No scopes at all" was too absolute:
the agent has to reason, and inference is not a domain.

The two-hop token chain was the heaviest unproven thing in the design and it
holds: sub stays the end user through all four hops, so their own roles apply
at the far end and the MCP server filters tool discovery for the right human.
azp names the last agent, not the first — Keycloak accepts actor_token and
emits no act claim, so an orchestrator's identity survives only in the traces.

Verified end to end: a routed question answered from the specialist's data; a
question outside every remit refused rather than answered from memory; and a
write proposed through the orchestrator, held by the specialist in
input-required, relayed here unchanged, approved by the user through the same
conversation, and written. The orchestrator never owns that decision.

The graph is plan → delegate → plan, tools described by what a specialist
decides rather than what it holds, delegations in one step run concurrently and
capped at eight counted in calls. Rebuilt per turn, because its tools carry the
user's token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Stéphane Tailland
2026-08-29 12:26:20 +02:00
parent 8af9e92fe0
commit ebf864f7ba
35 changed files with 1380 additions and 4 deletions
+108
View File
@@ -0,0 +1,108 @@
# Assistant — the orchestrating agent
The single entry point for users. Understands a request, consults the
specialists that can settle it, and composes one answer.
**Framework:** LangGraph · **Mode:** conversational · **Client id:**
`classic-models-agent-assistant`
## It holds nothing, and that is enforced by the realm
This is the component that sees raw user text, so it is the prompt-injection
target. It has no MCP server, no domain scope, and no data of its own. That is
not a convention in the prompt — it is what the realm grants:
```
OBTAINED classic-models-agent-catalogue-audience aud=[classic-models-agent-catalogue]
refused mcp-catalog-audience → Invalid scopes
refused mcp-sales-audience → Invalid scopes
refused catalog-api-audience → Invalid scopes
```
An injection that succeeds against this agent finds no path to a domain,
because there is none to find. Its two optional scopes are one specialist and
the inference endpoint — inference is not a domain, and the agent has to reason.
Adding a specialist is an entry in `specialists.py` plus an audience scope
granted in the realm. Add the entry without the grant and every call to it fails
at the token exchange with `Invalid scopes`, loudly, before any data moves.
## The chain
Four hops, `sub` unchanged throughout — which is what makes the end user's own
roles apply at the far end:
```
1. user sub=pm-user azp=librechat aud=[…-agent-assistant]
2. assistant→cat sub=pm-user azp=classic-models-agent-assistant aud=[…-agent-catalogue]
3. cat→mcp sub=pm-user azp=classic-models-agent-catalogue aud=[mcp-catalog, token-exchange-broker]
4. MCP call → 200
```
`azp` names the last agent in the chain, not the first. Keycloak accepts
`actor_token` and emits no `act` claim, so an orchestrator's identity does not
survive a second exchange — the delegation is visible in the traces, not in the
token.
## What it does with an answer
A specialist returns an answer, never data. The orchestrator composes; it does
not aggregate records it was not given.
Two behaviours worth naming, both verified end to end:
- **Out of remit → it says so.** Asked for a company-wide revenue figure, it
reports that no specialist can settle it rather than answering from what the
model knows. An invented answer is indistinguishable from one the estate gave.
- **A held write is relayed verbatim.** Catalogue parks a proposed write in
`input-required`; this agent passes that message through unchanged and stops.
The user's `approve` comes back through the same conversation and reaches the
specialist, which owns the decision. The orchestrator never approves on
anyone's behalf.
## The graph
`plan → delegate → plan → …` until the model stops calling tools. One tool per
specialist, described by what it *decides*. Delegations in the same step run
concurrently — that is the point of fanning out at all — and are capped by
`ASSISTANT_MAX_DELEGATIONS` (8), counted in calls rather than model turns,
because one specialist consulted twice for the same question is the loop worth
catching.
The graph is rebuilt per turn: its tools carry the user's token, and a graph
held across turns would be a token held across turns.
## Running it
```
uv venv && uv pip install -e '.[dev]'
ASSISTANT_CLIENT_SECRET=… ASSISTANT_CA_BUNDLE=… \
ASSISTANT_URL_CATALOGUE=http://localhost:8099/ \
python -m assistant_agent
```
`ASSISTANT_URL_<NAME>` overrides a specialist's address, for running outside the
cluster against a port-forward. In the cluster nothing sets it.
macOS resolves every `.local` name through mDNS before falling back to
`/etc/hosts`, at a flat ~5s per host. `ASSISTANT_HTTP_TIMEOUT` defaults to 60s
for that reason; in the cluster the names resolve instantly.
## Deployment
```
gitea.sttlab.eu/stt/classic-models-agent-assistant:v0.1.0
```
`k8s/` is what runs, `dev/` is the Flux plumbing. Two manual applies close the
loop once — the `agents` namespace and the `sttlab-ca` ConfigMap already exist,
created by the catalogue agent's repo, and are deliberately not declared here so
two Kustomizations do not fight over pruning them.
```
kubectl apply -f dev/gitrepository.yaml
kubectl apply -f dev/bootstrap-kustomization.yaml
```
`replicas: 1` is a constraint: conversation history lives in the process, and a
specialist's approval round trip passes back through here.