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>
This commit is contained in:
Stéphane Tailland
2026-08-29 17:51:18 +02:00
parent 8f1f0245da
commit 2bb6faa121
6 changed files with 61 additions and 154 deletions
+32 -8
View File
@@ -83,16 +83,40 @@ webhook, and token verification lands where it already lives. No new component.
Alternative: a small FastA2A adapter, written once for both — see
`classic-models-agent-catalogue`, which uses FastA2A already.
## The autonomous mandate
## An autonomous run is a machine identity
No user session exists when these run, so authority must be pre-established: the
user obtains `offline_access` when creating the mandate, the refresh token is
stored encrypted, and the exchange happens at trigger time. `sub` stays the user
who mandated, `azp` says which agent acted.
**Superseded, 2026-08-29.** An earlier revision had these agents act under an
`offline_access` mandate: a user authorises the agent, their refresh token is
stored encrypted, and it is redeemed at trigger time so `sub` stays the person
who mandated.
The MCP servers see no difference between an autonomous and a conversational
call — same `sub`, same `azp`, same audiences. That is deliberate: downstream
policy must not need to know the trigger mode.
That was built and then withdrawn, because it is not true. **An agent running on
its own account acts for itself.** Nobody asked for the 06:00 run; naming a
person as its principal is a fiction, and choosing which person is arbitrary —
the first implementation picked whichever test user was to hand.
It also cost more than it was worth. An offline token has no `exp`; it dies only
after 30 days unused, is not rotated on use (`revokeRefreshToken: false`), and
is revocable only by killing that user's offline session. A permanent bearer
credential, standing in for a consent nobody gave.
So: `client_credentials`, and the service account carries the realm role the job
needs. For the periodic reorder run that is **`read_only`** — so `create-product`
is not merely unused, it is **invisible** to that identity. Least privilege
enforced by the realm and the gateway rather than by the workflow behaving
itself. Verified: `tools/list` under the machine identity returns `get-product`
and `list-products`, nothing more.
What does *not* change: `azp` still names the agent, the audiences are still
per-server, and the MCP server still derives the consumer group from the token's
roles. Downstream policy still needs no knowledge of the trigger mode — it reads
roles, and a service account has roles like anyone else.
**Where a mandate would still be right:** when a person's own authority is
genuinely in play — an action they are accountable for, or data scoped to them.
Recouvrement's dunning is the candidate, and it is a human-validated step
already. The `classic-models-mandate` client is left in the realm, unused, for
that case.
## Backlog