2bb6faa121
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>
179 lines
8.0 KiB
Markdown
179 lines
8.0 KiB
Markdown
# Flows — the autonomous agents
|
|
|
|
**Runtime:** n8n · **Mode:** autonomous · Two agents in one repo.
|
|
|
|
| Agent | Client id | Trigger |
|
|
|---|---|---|
|
|
| Approvisionnement | `classic-models-agent-approvisionnement` | event-driven, plus a periodic ordering run |
|
|
| Recouvrement | `classic-models-agent-recouvrement` | weekly, Monday |
|
|
|
|
They share a repo because they are not code: n8n workflow exports, same tool,
|
|
same owner. Splitting them would buy nothing.
|
|
|
|
## Why low-code for these two
|
|
|
|
Their work is rules, thresholds and due dates — a sequence of conditions and
|
|
calls, which a visual editor expresses better than code. The LLM is optional on
|
|
their critical path, which makes them cheaper and more predictable than the
|
|
conversational agents.
|
|
|
|
They remain full A2A agents: they publish an Agent Card, accept tasks, answer.
|
|
|
|
## Approvisionnement
|
|
|
|
| | |
|
|
|---|---|
|
|
| MCP | catalog, sales |
|
|
| Scopes | `catalog:read`, `catalog:write`, `sales:read` |
|
|
|
|
Detect coming stock-outs, propose reorders — **two jobs with two different
|
|
triggers**, which an earlier revision of this file collapsed into one nightly
|
|
run.
|
|
|
|
*Detection is reactive.* It depends on something happening: an order placed, a
|
|
stock level crossing a threshold. Finding that out up to a day later is latency
|
|
chosen for nothing, and a nightly sweep re-reads the whole catalog to rediscover
|
|
what the domain already knew at the moment it changed.
|
|
|
|
*Proposing a purchase order is periodic.* Nobody reorders on every unit sold;
|
|
orders are grouped by supplier lead time and buying cycle. A clock is the right
|
|
trigger there, and that run is periodic on purpose rather than a batch standing
|
|
in for events.
|
|
|
|
Worth naming, because it changes what has to be built: **the autonomous mandate
|
|
is a cost of the trigger, not of the business need.** `offline_access`, a
|
|
refresh token stored encrypted and an exchange at trigger time are required only
|
|
because nobody is present at 3am. An agent woken by a domain event runs moments
|
|
after a human acted.
|
|
|
|
**Exposes** `check_availability` — called by Ventes before promising a delivery
|
|
date, and by Catalogue before discontinuing a reference. Returns a decision and a
|
|
date, never the stock table.
|
|
|
|
## Recouvrement
|
|
|
|
| | |
|
|
|---|---|
|
|
| MCP | customers, payments |
|
|
| Scopes | `customers:read`, `payments:read`, `payments:write` |
|
|
|
|
Weekly: unpaid invoices past 30 days, dunning, escalation.
|
|
|
|
**Exposes** `check_creditworthiness` — called by Ventes before accepting a large
|
|
order.
|
|
|
|
**Dunning is prepared automatically and sent only after human validation.**
|
|
Sending a letter to a customer is an outward-facing action; a rule bug would
|
|
otherwise write to the entire customer base in one run.
|
|
|
|
## The A2A server gap
|
|
|
|
The available n8n community nodes ([n8n-nodes-a2a](https://github.com/agentic-layer/n8n-nodes-a2a),
|
|
[n8n-nodes-agent2agent](https://github.com/pjawz/n8n-nodes-agent2agent)) make n8n
|
|
an A2A **client**: `Discover Agent`, `Send Task`.
|
|
|
|
But both agents here must be **servers** — Ventes calls them. That needs an
|
|
Agent Card on `/.well-known/agent.json` and a JSON-RPC endpoint with the task
|
|
lifecycle. n8n webhooks can do it by hand, at which point it stops being no-code.
|
|
|
|
Preferred approach: **Kong in front of n8n**, exactly as it fronts every other
|
|
resource. Kong serves the Agent Card statically and routes JSON-RPC to the
|
|
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.
|
|
|
|
## An autonomous run is a machine identity
|
|
|
|
**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.
|
|
|
|
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
|
|
|
|
**Events over Kafka, emitted by `classic-models-api`.** The target for the
|
|
reactive half. The estate emits no domain event today — the API has no signals
|
|
and no outbound webhook, and there is no bus in the cluster — which is what made
|
|
a nightly sweep look like the only option.
|
|
|
|
The emission belongs **in the API**, not in a database CDC and not in a gateway
|
|
plugin. Only the API knows which change is a business event rather than a row
|
|
update, and only it holds the identity that made it — `sub` and `azp` from the
|
|
caller's token, which the event has to carry if a downstream agent is to act
|
|
under the same authority as the person who triggered it.
|
|
|
|
Two facts about that codebase shape the work:
|
|
|
|
- Every write goes through one `BaseModelViewSet` in
|
|
`api/v1/classicmodels/views.py`. One hook there covers products, orders,
|
|
payments and the rest — Django signals scattered across models would be the
|
|
harder version of the same thing.
|
|
- **The models are `managed = False`.** The tables are the pre-existing
|
|
classicmodels schema and Django owns no migrations for them. A transactional
|
|
outbox means introducing the estate's first managed table beside unmanaged
|
|
ones — which works, and is worth deciding deliberately rather than
|
|
discovering.
|
|
|
|
An outbox is the part that matters: emitting from a viewset after `save()` drops
|
|
the event if the process dies between the two, and an agent that never hears
|
|
about a stock movement is worse than one that hears late.
|
|
|
|
Until this exists, detection stays unbuilt rather than being approximated by a
|
|
periodic scan: a batch that pretends to be reactive is worse than an absent
|
|
feature, because it looks finished.
|
|
|
|
The periodic ordering run does not wait on any of it.
|
|
|
|
## Two controls that become mandatory here
|
|
|
|
They were deferrable with a human in the loop. They are not, once an agent
|
|
loops on its own.
|
|
|
|
- **Read volume caps per `(azp, sub)`**, enforced on Kong. An agent reads a
|
|
thousand records in seconds where a human clicks ten; a scope authorises the
|
|
read but says nothing about volume.
|
|
- **Authorisation on tool-call parameters**, not just the tool name. `write`
|
|
authorises writing, not "this €40k transfer".
|
|
|
|
An autonomous agent hitting a rule that requires approval must **fail cleanly
|
|
and escalate**, never decide. And any approval screen must be rendered from the
|
|
structured tool-call parameters — never from a sentence the model produced, or a
|
|
prompt injection writes what the human validates.
|
|
|
|
## Before writing workflows
|
|
|
|
Read `classic-models-agent-platform/CLAUDE.md` for the identity model, the
|
|
endpoints and the known blockers.
|
|
|
|
Deliverables in English. Export workflows as JSON and commit them; do not rely
|
|
on the n8n instance as the source of truth.
|