fix(workflow): reach the catalog, and answer

Two things were wrong, and the second hid the first.

n8n does not re-register an active workflow's definition on PATCH: the running
webhook kept executing the version it was activated with. So the corrected
subject_token expression was never running, and the Keycloak V1 NullPointer it
kept producing was the old empty-token failure — including through the attempt
to capture the request with an echo service, which the old definition never
called. Deactivate, apply, reactivate; written down in workflows/README.md
because it costs an afternoon otherwise.

The token reference itself: $json is the previous node's output, not the
request, so the subject token has to come from $('Webhook').

The regex is now loose after the underscore, so a reference that looks valid but
does not exist reaches the catalog instead of being answered with 'you named no
product', which was false.

Working for the main path and for a question naming no product. A nonexistent
code still returns an empty body — recorded as a known gap, not as acceptable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Stéphane Tailland
2026-08-29 13:24:44 +02:00
parent f9511fd15d
commit 562987cd69
2 changed files with 48 additions and 32 deletions
+44 -29
View File
@@ -2,47 +2,62 @@
Exported as JSON and committed. The n8n instance is not the source of truth.
## Importing
## Deploying a change
n8n's REST API, with an owner session cookie. The activation endpoint is
separate from the update one and requires the current `versionId`:
**n8n does not re-register an active workflow when you PATCH it.** The running
webhook keeps executing the definition it was activated with, so an edit appears
to have no effect — and the failure it keeps producing is the *old* one, which
is a very good way to spend an afternoon debugging something already fixed.
Deactivate, apply, reactivate, in that order:
```
POST /rest/workflows # import
POST /rest/workflows/<id>/activate # {"versionId": "<current>"}
POST /rest/workflows/<id>/deactivate {"versionId": "<current>"}
PATCH /rest/workflows/<id> {"nodes":…, "connections":…, "versionId": "<current>"}
POST /rest/workflows/<id>/activate {"versionId": "<current>"}
```
`PATCH /rest/workflows/<id>` with `{"active": true}` returns 200 and leaves the
workflow inactive, silently. Use the activate endpoint.
`versionId` changes at every step, so re-read it each time. And note
`PATCH {"active": true}` returns 200 and leaves the workflow inactive, silently:
activation has its own endpoint.
## approvisionnement-check-availability
**Not working yet.** The `Exchange token` node fails, and what it fails with is
worth recording:
Working. `POST /webhook/check-availability` with the end user's token in
`X-Subject-Token`:
```
Keycloak 500, an HTML login page as the body
NullPointerException: Cannot invoke UserSessionModel.getAuthenticatedClientSessionByClient
because "userSession" is null
at V1TokenExchangeProvider.exchangeClientToOIDCClient
"Peut-on promettre S10_1949 ?"
→ {"answer":"Yes — 1952 Alpine Renault 1300 (S10_1949) can be promised:
7305 in stock, above the reorder threshold of 50.","escalate":false}
"Peut-on promettre la Vespa ?"
→ {"answer":"No product reference in the question…","escalate":true}
```
Two things stand out. Keycloak answers a malformed token exchange with a **500
and a rendered login page**, which tells a client nothing. And it took the **V1**
exchange path, not the Standard one every other agent in this estate uses
successfully.
It exchanges the token itself, using the agent's client secret from the pod
environment so the committed JSON holds no secret, then talks MCP in two calls:
`initialize` to obtain the session id from a **response header**, then
`tools/call`. No `notifications/initialized` is needed.
What has been eliminated, each by hand against the same running Keycloak:
MCP rather than the REST route, which would have been one call instead of two:
the per-tool ACL by consumer group lives on the MCP route only. Going through
REST would give an autonomous agent broader access than a conversational one,
which is exactly backwards.
- the client and its secret — Keycloak logs
`kc.clientId: classic-models-agent-approvisionnement`, so it authenticated,
which also proves `$env` reaches the node
- the subject token — the *same* token replayed with curl exchanges fine (200)
- omitting `subject_token_type` — still 200
- omitting `openid` from the scope — still 200
- using `audience=` instead of a scope — a clean 400, not this
The product code is read with a regex, not a model. This agent's critical path
is rules; a question naming no product escalates rather than being guessed at.
The regex is loose after the underscore on purpose — a reference that looks
valid but does not exist must reach the catalog and come back as "no such
product", because answering "you named no product" would be false.
So n8n is sending something the reproduction attempts are not. The next step is
to capture the request as the node actually emits it: point the node at an
in-cluster echo service and read the body. That was set up and the run did not
reach it, which is itself unexplained and where this stopped.
### Known gap
A product code that does not exist returns an **empty body** rather than that
message. The run completes, every node executes including the one that composes
the answer, and the response is still empty — so the fault is between the
composing node and the webhook response, not in the decision logic.
`alwaysOutputData` on the read node did not change it.
For an autonomous agent, silence is the worst possible answer. Not shipped as
acceptable — recorded as unfinished.