15 Commits

Author SHA1 Message Date
Stéphane Tailland 7ec4ffad37 Rename the supply agent everywhere it is named
Its Keycloak client, audience scope, client-secret key, workflow names and file
names, and the A2A card. Kafka moved first, under its own account and consumer
group.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:02:20 +02:00
Stéphane Tailland ed99334201 Deploy the adapter fix and document the four outcomes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 14:39:25 +02:00
Stéphane Tailland 78456c7902 Name the Kubernetes objects in English
catalogue-agent and approvisionnement-agent named a Deployment, a Service, an
Ingress and a Certificate in French, in an estate where nothing else is.

The peer URLs move with them, set through the existing per-environment override
so the rename takes effect without rebuilding an image; the defaults in the code
carry the same values for the next build.

The Keycloak client ids, the image names and the repository names are still
French. They are a separate, heavier move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 09:12:47 +02:00
Stéphane Tailland 2156cf4673 Keep -agent in the hostname, shorten the rest
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 09:10:50 +02:00
Stéphane Tailland 087b1a6d14 Give the agent a short hostname
assistant-agent.sttlab.local and its two siblings were long, and two of them
were French where nothing else in the estate is.

The Kubernetes object names are unchanged — they name a Deployment and a
Service, and nobody types them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 09:06:38 +02:00
Stéphane Tailland a64f5026e3 Give the agent a door a human can reach
Nothing outside the cluster could talk to it: every conversation went through
kubectl run curl, which makes an agent that works and nobody can use.

The same authentication subrequest that guards the n8n endpoints guards this
one. The agent forwards and exchanges the caller's token, so a forged one
fails at the realm — but failing downstream is not refusing, and an
unauthenticated caller could still make it do inference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 09:00:25 +02:00
Stéphane Tailland b04c5cbb11 Do not declare the agent's Kafka account here
Kafka is administered centrally. An ACL is not a team's decision to make about
itself, so the account moved to k8s-platform/kafka-topology.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 08:51:22 +02:00
Stéphane Tailland f810efbc04 Own the agent's Kafka account here
An account belongs to the consumer, not the broker: this repo says what the
agent may read, so adding or removing the agent adds or removes its access.
It was in infra-kafka, which deploys a cluster and should hold no opinion about
who uses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 08:42:15 +02:00
Stéphane Tailland 33d811fa68 Quote the two ConfigMap values inside the value
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 08:10:03 +02:00
Stéphane Tailland 0847e11713 Gate the n8n trigger endpoints on Keycloak
Every webhook on this instance was answering unauthenticated requests, the
stock watchlist included — it returned references and stock levels and let
anyone clear entries. The A2A facade in front of these workflows enforces a
bearer, but the workflows stayed reachable directly, facade bypassed.

Keycloak's userinfo is the authenticator: 200 for a valid bearer in the realm,
401 otherwise, which is the auth-request contract exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 07:56:13 +02:00
Stéphane Tailland 06d0ab5e2d Wake Approvisionnement on a stock event instead of a clock
The reactive half of the agent. It consumes product.stock_changed from the
catalog topic and reports only a crossing of the threshold — every further
order line on an already-low reference would otherwise raise the same alarm,
and an agent that repeats itself is one a human learns to ignore. A stock-out
is treated as its own fact even when the reference was already low.

The detection lands on a watchlist readable over a webhook, because a proposal
that reached only an execution log would be lost between two nightly runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 07:46:24 +02:00
Stéphane Tailland 2bb6faa121 refactor: an autonomous run is a machine identity, not a mandate
Withdrawn: the offline_access mandate built an hour ago. An agent running on its
own account acts for itself. Nobody asked for the 06:00 run, so naming a person
as its principal was a fiction — and choosing which person was arbitrary, as the
first implementation showed by picking whichever test user was to hand.

It also cost more than it was worth. An offline token has no exp, dies only
after 30 days unused, is not rotated on use, and is revocable only by killing
that user's offline session: a permanent bearer credential standing in for a
consent nobody gave.

Now client_credentials, with the service account carrying read_only — the least
privilege the job needs. That is not a convention the workflow observes: the MCP
server maps the role to a consumer group at discovery, so tools/list under this
identity returns get-product and list-products and nothing else. create-product
is invisible rather than merely unused.

Nothing downstream changes. azp still names the agent, audiences are still
per-server, and policy still reads roles — a service account has roles like
anyone else.

The mandate client stays in the realm, unused, for the case where a person's own
authority genuinely is in play: Recouvrement's dunning, which is human-validated
already.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:51:18 +02:00
Stéphane Tailland 8f1f0245da feat: the offline_access mandate
The standing authority the autonomous runs act under: an offline refresh token
issued by a new classic-models-mandate client for the user who authorised the
agent, stored in Vault, redeemed at trigger time.

The mandate client holds no domain audience. It cannot reach data; it can only
say who authorised whom. Direct access grants on it is a lab shortcut, noted as
such — mandating an agent is a decision a person takes, and a password grant
records no consent where the authorization code flow would.

Verified end to end: refresh gives sub=pm-user azp=classic-models-mandate with
the agent in the audience, the agent exchanges it to azp=itself and
aud=mcp-catalog, and the MCP call answers 200. sub stays the person who
mandated, azp says which agent acted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:42:33 +02:00
Stéphane Tailland 666735b0eb feat: deploy the Approvisionnement facade
The adapter runs in the agents namespace with the other agents, not with n8n:
what it is, is an agent, and the workflow behind it is an implementation detail
its callers never see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:25:35 +02:00
Stéphane Tailland f9511fd15d feat: this estate's n8n deployment
The values that infra-n8n deliberately does not carry: our hostname, issuer,
Vault path and images, plus the two things that are ours alone — the lab CA the
workflows need to verify internal TLS, and the agents' Keycloak client secrets,
read from the realm's own Vault entry so there is one source per secret.

Self-managing after three applies, like the rest of the estate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:10:10 +02:00