# 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_` 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.2.1 ``` `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.