From 7ec4ffad370009b5c11c2132801fff98f69bd5fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Mon, 31 Aug 2026 15:02:20 +0200 Subject: [PATCH] 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 --- deploy/agent-secrets.yaml | 4 +- deploy/agents/ingress.yaml | 2 +- deploy/agents/supply.yaml | 4 +- workflows/approvisionnement-stock-watch.json | 115 ------------------ ...ty.json => supply-check-availability.json} | 8 +- ...rder.json => supply-periodic-reorder.json} | 8 +- workflows/supply-stock-watch.json | 115 ++++++++++++++++++ 7 files changed, 128 insertions(+), 128 deletions(-) delete mode 100644 workflows/approvisionnement-stock-watch.json rename workflows/{approvisionnement-check-availability.json => supply-check-availability.json} (97%) rename workflows/{approvisionnement-periodic-reorder.json => supply-periodic-reorder.json} (95%) create mode 100644 workflows/supply-stock-watch.json diff --git a/deploy/agent-secrets.yaml b/deploy/agent-secrets.yaml index 7d1b846..41aed7b 100644 --- a/deploy/agent-secrets.yaml +++ b/deploy/agent-secrets.yaml @@ -18,7 +18,7 @@ spec: target: name: n8n-agent-secrets data: - - secretKey: APPROVISIONNEMENT_CLIENT_SECRET + - secretKey: SUPPLY_CLIENT_SECRET remoteRef: key: keycloak/classic-models-realm - property: KC_SECRET_CLASSIC_MODELS_AGENT_APPROVISIONNEMENT + property: KC_SECRET_CLASSIC_MODELS_AGENT_SUPPLY diff --git a/deploy/agents/ingress.yaml b/deploy/agents/ingress.yaml index dfba8fa..53ed12d 100644 --- a/deploy/agents/ingress.yaml +++ b/deploy/agents/ingress.yaml @@ -4,7 +4,7 @@ # every conversation with it went through `kubectl run curl`. An orchestrator # nobody can talk to is not finished. # -# Peer traffic does **not** come through here — approvisionnement calls, and is called by, +# Peer traffic does **not** come through here — supply calls, and is called by, # other agents on their in-cluster Service names. This is the human door. # # ## It is not an open door diff --git a/deploy/agents/supply.yaml b/deploy/agents/supply.yaml index ba88cbb..f1d5c1f 100644 --- a/deploy/agents/supply.yaml +++ b/deploy/agents/supply.yaml @@ -1,4 +1,4 @@ -# The A2A facade for Approvisionnement. It lives in the `agents` namespace with +# The A2A facade for Supply. It lives in the `agents` namespace with # the other agents, not with n8n: what it is, is an agent — the workflow behind # it is an implementation detail its callers never see. apiVersion: apps/v1 @@ -31,7 +31,7 @@ spec: containerPort: 8080 env: - name: ADAPTER_AGENT_NAME - value: approvisionnement + value: supply - name: ADAPTER_AGENT_DESCRIPTION value: >- Supply decisions for the Classic Models catalog. Answers whether a diff --git a/workflows/approvisionnement-stock-watch.json b/workflows/approvisionnement-stock-watch.json deleted file mode 100644 index a61647e..0000000 --- a/workflows/approvisionnement-stock-watch.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "name": "Approvisionnement \u2014 stock watch", - "nodes": [ - { - "parameters": { - "topic": "classic-models.catalog", - "groupId": "approvisionnement-stock-watch", - "resolveOffset": "onCompletion", - "allowedStatuses": [ - "success" - ], - "options": { - "jsonParseMessage": true, - "onlyMessage": true, - "fromBeginning": false - } - }, - "type": "n8n-nodes-base.kafkaTrigger", - "typeVersion": 2, - "position": [ - 0, - 0 - ], - "id": "kafka-trigger-stock-watch", - "name": "Catalog events", - "credentials": { - "kafka": { - "id": "8qNZXLzvdx5rWQ0V", - "name": "kafka-classic-models" - } - } - }, - { - "parameters": { - "jsCode": "// A stock movement, judged the moment it happens.\n//\n// This is the reactive half of Approvisionnement. The 06:00 run proposes\n// purchase orders on a buying cycle, which is genuinely periodic; noticing that\n// a reference is about to run out is not, and a nightly sweep would report at\n// 06:00 what was true at 14:32 the previous afternoon.\n//\n// What it reacts to is a fact the API published, not a row it went looking for.\nconst THRESHOLD = 50;\nconst COVER = 3; // a proposal covers three times the threshold\nconst STOCK_CHANGED = 'com.sttlab.classicmodels.product.stock_changed.v1';\n\nconst event = $json ?? {};\n\n// The topic carries every catalog event, so most messages are not ours. Saying\n// so explicitly keeps the offset moving: a workflow that threw on an\n// uninteresting event would replay it forever.\nif (event.type !== STOCK_CHANGED) {\n return [{ json: { considered: false, type: event.type ?? 'unreadable' } }];\n}\n\nconst data = event.data ?? {};\nconst before = Number(data.before?.quantityinstock);\nconst after = Number(data.after?.quantityinstock);\nif (!Number.isFinite(before) || !Number.isFinite(after)) {\n return [{ json: {\n considered: false,\n escalate: true,\n reason: `${event.subject}: the movement carried no readable quantities.`,\n }}];\n}\n\n// Only a *crossing* is news. Without this, every further order line on an\n// already-low reference raises the same alarm again, and an agent that repeats\n// itself is one a human learns to ignore.\nconst crossed = before >= THRESHOLD && after < THRESHOLD;\n// A stock-out is a different fact from a low stock, even when the reference was\n// already below the threshold \u2014 nothing can be promised at all.\nconst emptied = after === 0 && before > 0;\n\n// `order:` means demand; anything else is someone correcting the books. Both\n// justify a purchase order, but only demand is a signal about the future.\nconst fromDemand = String(data.reason ?? '').startsWith('order:');\n\nconst store = $getWorkflowStaticData('global');\nstore.watchlist = store.watchlist ?? {};\n\nif (!crossed && !emptied) {\n // Not news, but if this reference is already being watched its figures have\n // just moved. Refresh them rather than leaving a human reading the number it\n // held at the moment of detection, which is the number least likely to still\n // be true.\n const watched = store.watchlist[event.subject];\n if (watched) {\n watched.on_hand = after;\n watched.propose_order = THRESHOLD * COVER - after;\n watched.last_movement_at = event.time ?? new Date().toISOString();\n }\n return [{ json: {\n considered: true,\n detected: false,\n watching: Boolean(watched),\n productcode: event.subject,\n on_hand: after,\n }}];\n}\n\nconst state = { detected: true, escalate: true,\n productcode: event.subject,\n on_hand: after,\n was: before,\n threshold: THRESHOLD,\n stock_out: after === 0,\n driver: fromDemand ? 'demand' : 'correction',\n movement: data.reason ?? null,\n propose_order: THRESHOLD * COVER - after,\n observed_at: event.time ?? new Date().toISOString(),\n // Kept so this detection can be joined to the request that caused it. It is\n // the only link back \u2014 the event deliberately names no actor.\n traceparent: event.traceparent ?? null,\n};\n\nstate.reason = state.stock_out\n ? `${state.productcode} is out of stock (was ${before}). Nothing can be promised.`\n : `${state.productcode} fell from ${before} to ${after}, below the threshold of ${THRESHOLD}.`;\n\n// The watchlist is the destination, and it is a real one: the proposal still\n// has nowhere to be written \u2014 the domain has no purchase order and no supplier\n// entity \u2014 so a detection that only logged would be lost between two nightly\n// runs. Here it survives, keyed by reference so a reference cannot appear twice.\nstore.watchlist[state.productcode] = state;\n\nreturn [{ json: state }];\n" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 220, - 0 - ], - "id": "detect-crossing", - "name": "Judge the movement" - }, - { - "parameters": { - "httpMethod": [ - "GET", - "POST" - ], - "path": "stock-watchlist", - "responseMode": "lastNode", - "options": {}, - "multipleMethods": true - }, - "type": "n8n-nodes-base.webhook", - "typeVersion": 2, - "position": [ - 0, - 200 - ], - "id": "watchlist-read", - "name": "Watchlist", - "webhookId": "stock-watchlist-get" - }, - { - "parameters": { - "jsCode": "// What has been detected and not yet dealt with.\n//\n// A detection that goes only into an execution log is one nobody reads. This\n// makes the watchlist readable \u2014 by the periodic run, which can start from what\n// was already noticed rather than rediscovering it, and by a human.\n//\n// GET /webhook/stock-watchlist \u2192 the current list\n// POST /webhook/stock-watchlist {\"clear\": [\"S24_2000\"]} \u2192 drop entries\nconst store = $getWorkflowStaticData('global');\nstore.watchlist = store.watchlist ?? {};\n\n// GET carries no body; the node still runs, and clear is then empty.\nconst body = $json.body ?? {};\nconst clear = Array.isArray(body.clear) ? body.clear : [];\nfor (const code of clear) delete store.watchlist[code];\n\nconst entries = Object.values(store.watchlist);\nreturn [{ json: {\n watching: entries.length,\n cleared: clear,\n // Worst first: an out-of-stock reference is not the same problem as one that\n // merely crossed the threshold.\n entries: entries.sort((a, b) => a.on_hand - b.on_hand),\n}}];\n" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 220, - 200 - ], - "id": "watchlist-render", - "name": "Render the watchlist" - } - ], - "connections": { - "Catalog events": { - "main": [ - [ - { - "node": "Judge the movement", - "type": "main", - "index": 0 - } - ] - ] - }, - "Watchlist": { - "main": [ - [ - { - "node": "Render the watchlist", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Render the watchlist", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "settings": { - "executionOrder": "v1" - } -} diff --git a/workflows/approvisionnement-check-availability.json b/workflows/supply-check-availability.json similarity index 97% rename from workflows/approvisionnement-check-availability.json rename to workflows/supply-check-availability.json index 7f3e9cc..4b996d3 100644 --- a/workflows/approvisionnement-check-availability.json +++ b/workflows/supply-check-availability.json @@ -1,5 +1,5 @@ { - "name": "Approvisionnement \u2014 check_availability", + "name": "Supply \u2014 check_availability", "nodes": [ { "parameters": { @@ -32,11 +32,11 @@ }, { "name": "client_id", - "value": "classic-models-agent-approvisionnement" + "value": "classic-models-agent-supply" }, { "name": "client_secret", - "value": "={{ $env.APPROVISIONNEMENT_CLIENT_SECRET }}" + "value": "={{ $env.SUPPLY_CLIENT_SECRET }}" }, { "name": "subject_token", @@ -82,7 +82,7 @@ }, "sendBody": true, "specifyBody": "json", - "jsonBody": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-06-18\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"approvisionnement\", \"version\": \"0.1.0\"}}}", + "jsonBody": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-06-18\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"supply\", \"version\": \"0.1.0\"}}}", "options": { "response": { "response": { diff --git a/workflows/approvisionnement-periodic-reorder.json b/workflows/supply-periodic-reorder.json similarity index 95% rename from workflows/approvisionnement-periodic-reorder.json rename to workflows/supply-periodic-reorder.json index 448163a..3cba58d 100644 --- a/workflows/approvisionnement-periodic-reorder.json +++ b/workflows/supply-periodic-reorder.json @@ -1,5 +1,5 @@ { - "name": "Approvisionnement \u2014 periodic reorder proposal", + "name": "Supply \u2014 periodic reorder proposal", "nodes": [ { "parameters": { @@ -35,11 +35,11 @@ }, { "name": "client_id", - "value": "classic-models-agent-approvisionnement" + "value": "classic-models-agent-supply" }, { "name": "client_secret", - "value": "={{ $env.APPROVISIONNEMENT_CLIENT_SECRET }}" + "value": "={{ $env.SUPPLY_CLIENT_SECRET }}" }, { "name": "scope", @@ -77,7 +77,7 @@ }, "sendBody": true, "specifyBody": "json", - "jsonBody": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-06-18\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"approvisionnement-periodic\", \"version\": \"0.1.0\"}}}", + "jsonBody": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-06-18\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"supply-periodic\", \"version\": \"0.1.0\"}}}", "options": { "response": { "response": { diff --git a/workflows/supply-stock-watch.json b/workflows/supply-stock-watch.json new file mode 100644 index 0000000..36f9a6f --- /dev/null +++ b/workflows/supply-stock-watch.json @@ -0,0 +1,115 @@ +{ + "name": "Supply \u2014 stock watch", + "nodes": [ + { + "parameters": { + "topic": "classic-models.catalog", + "groupId": "supply-stock-watch", + "resolveOffset": "onCompletion", + "allowedStatuses": [ + "success" + ], + "options": { + "jsonParseMessage": true, + "onlyMessage": true, + "fromBeginning": false + } + }, + "type": "n8n-nodes-base.kafkaTrigger", + "typeVersion": 2, + "position": [ + 0, + 0 + ], + "id": "kafka-trigger-stock-watch", + "name": "Catalog events", + "credentials": { + "kafka": { + "id": "8qNZXLzvdx5rWQ0V", + "name": "kafka-classic-models" + } + } + }, + { + "parameters": { + "jsCode": "// A stock movement, judged the moment it happens.\n//\n// This is the reactive half of Supply. The 06:00 run proposes\n// purchase orders on a buying cycle, which is genuinely periodic; noticing that\n// a reference is about to run out is not, and a nightly sweep would report at\n// 06:00 what was true at 14:32 the previous afternoon.\n//\n// What it reacts to is a fact the API published, not a row it went looking for.\nconst THRESHOLD = 50;\nconst COVER = 3; // a proposal covers three times the threshold\nconst STOCK_CHANGED = 'com.sttlab.classicmodels.product.stock_changed.v1';\n\nconst event = $json ?? {};\n\n// The topic carries every catalog event, so most messages are not ours. Saying\n// so explicitly keeps the offset moving: a workflow that threw on an\n// uninteresting event would replay it forever.\nif (event.type !== STOCK_CHANGED) {\n return [{ json: { considered: false, type: event.type ?? 'unreadable' } }];\n}\n\nconst data = event.data ?? {};\nconst before = Number(data.before?.quantityinstock);\nconst after = Number(data.after?.quantityinstock);\nif (!Number.isFinite(before) || !Number.isFinite(after)) {\n return [{ json: {\n considered: false,\n escalate: true,\n reason: `${event.subject}: the movement carried no readable quantities.`,\n }}];\n}\n\n// Only a *crossing* is news. Without this, every further order line on an\n// already-low reference raises the same alarm again, and an agent that repeats\n// itself is one a human learns to ignore.\nconst crossed = before >= THRESHOLD && after < THRESHOLD;\n// A stock-out is a different fact from a low stock, even when the reference was\n// already below the threshold \u2014 nothing can be promised at all.\nconst emptied = after === 0 && before > 0;\n\n// `order:` means demand; anything else is someone correcting the books. Both\n// justify a purchase order, but only demand is a signal about the future.\nconst fromDemand = String(data.reason ?? '').startsWith('order:');\n\nconst store = $getWorkflowStaticData('global');\nstore.watchlist = store.watchlist ?? {};\n\nif (!crossed && !emptied) {\n // Not news, but if this reference is already being watched its figures have\n // just moved. Refresh them rather than leaving a human reading the number it\n // held at the moment of detection, which is the number least likely to still\n // be true.\n const watched = store.watchlist[event.subject];\n if (watched) {\n watched.on_hand = after;\n watched.propose_order = THRESHOLD * COVER - after;\n watched.last_movement_at = event.time ?? new Date().toISOString();\n }\n return [{ json: {\n considered: true,\n detected: false,\n watching: Boolean(watched),\n productcode: event.subject,\n on_hand: after,\n }}];\n}\n\nconst state = { detected: true, escalate: true,\n productcode: event.subject,\n on_hand: after,\n was: before,\n threshold: THRESHOLD,\n stock_out: after === 0,\n driver: fromDemand ? 'demand' : 'correction',\n movement: data.reason ?? null,\n propose_order: THRESHOLD * COVER - after,\n observed_at: event.time ?? new Date().toISOString(),\n // Kept so this detection can be joined to the request that caused it. It is\n // the only link back \u2014 the event deliberately names no actor.\n traceparent: event.traceparent ?? null,\n};\n\nstate.reason = state.stock_out\n ? `${state.productcode} is out of stock (was ${before}). Nothing can be promised.`\n : `${state.productcode} fell from ${before} to ${after}, below the threshold of ${THRESHOLD}.`;\n\n// The watchlist is the destination, and it is a real one: the proposal still\n// has nowhere to be written \u2014 the domain has no purchase order and no supplier\n// entity \u2014 so a detection that only logged would be lost between two nightly\n// runs. Here it survives, keyed by reference so a reference cannot appear twice.\nstore.watchlist[state.productcode] = state;\n\nreturn [{ json: state }];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 220, + 0 + ], + "id": "detect-crossing", + "name": "Judge the movement" + }, + { + "parameters": { + "httpMethod": [ + "GET", + "POST" + ], + "path": "stock-watchlist", + "responseMode": "lastNode", + "options": {}, + "multipleMethods": true + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + 0, + 200 + ], + "id": "watchlist-read", + "name": "Watchlist", + "webhookId": "stock-watchlist-get" + }, + { + "parameters": { + "jsCode": "// What has been detected and not yet dealt with.\n//\n// A detection that goes only into an execution log is one nobody reads. This\n// makes the watchlist readable \u2014 by the periodic run, which can start from what\n// was already noticed rather than rediscovering it, and by a human.\n//\n// GET /webhook/stock-watchlist \u2192 the current list\n// POST /webhook/stock-watchlist {\"clear\": [\"S24_2000\"]} \u2192 drop entries\nconst store = $getWorkflowStaticData('global');\nstore.watchlist = store.watchlist ?? {};\n\n// GET carries no body; the node still runs, and clear is then empty.\nconst body = $json.body ?? {};\nconst clear = Array.isArray(body.clear) ? body.clear : [];\nfor (const code of clear) delete store.watchlist[code];\n\nconst entries = Object.values(store.watchlist);\nreturn [{ json: {\n watching: entries.length,\n cleared: clear,\n // Worst first: an out-of-stock reference is not the same problem as one that\n // merely crossed the threshold.\n entries: entries.sort((a, b) => a.on_hand - b.on_hand),\n}}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 220, + 200 + ], + "id": "watchlist-render", + "name": "Render the watchlist" + } + ], + "connections": { + "Catalog events": { + "main": [ + [ + { + "node": "Judge the movement", + "type": "main", + "index": 0 + } + ] + ] + }, + "Watchlist": { + "main": [ + [ + { + "node": "Render the watchlist", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Render the watchlist", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + } +}