Réveil des agents — nudges Module
Pourquoi un réveil, alors que les agents tirent déjà
Le modèle des agents est délibérément en pull : un agent demande sa prochaine tâche, la réclame de façon atomique, la traite, livre. Rien ne le pousse — c'est ce qui garde le cycle de vie gouverné côté serveur et empêche deux agents de détenir la même tâche. Mais le pull seul laisse une question ouverte : quand l'agent redemande-t-il ? Le faire interroger l'API toutes les quelques secondes, c'est du gaspillage et de la latence ; le faire trop rarement, c'est un agent qui répond une heure après qu'on lui a confié une tâche.
Le nudge résout exactement ça, sans rien changer au modèle de sécurité. C'est une sonnette, pas une clé : Vaks PM signale simplement « il y a peut-être du travail pour toi », et l'agent, réveillé, fait ce qu'il aurait fait de toute façon — il tire et réclame avec son propre jeton. Le nudge ne porte aucune autorité et ne peut rien débloquer que l'agent ne pouvait déjà faire.
Le modèle en un coup d'œil
| Temps | Ce qui se passe |
|---|---|
| 1. Abonnement (une fois) | La plateforme de l'agent enregistre une URL de callback auprès de Vaks PM (POST /me/webhook-subscriptions), authentifiée par le jeton de l'agent. Vaks PM renvoie un secret HMAC, affiché une seule fois. |
| 2. Événement | Une tâche devient pertinente pour l'agent — on la lui assigne, une revue demande des corrections, un commentaire arrive sur une tâche qu'il tient. |
| 3. Nudge | Vaks PM POST un petit corps JSON signé vers l'URL de callback. Il contient de quoi identifier la tâche et l'agent — jamais de contenu sensible, jamais de jeton. |
| 4. Pull | La plateforme réveille l'agent. Celui-ci vérifie la signature, puis tire et réclame atomiquement avec son propre PAT (vaks_claim_task / POST /me/next-task/claim). À partir de là, c'est la boucle de travail normale. |
Quand un nudge part
Trois événements déclenchent un réveil. Ils couvrent l'entrée dans la boucle et son bouclage après une revue :
| Événement | Déclencheur | Agent réveillé |
|---|---|---|
task.assigned | Un humain assigne une tâche à l'agent (ou la capacité se libère et une tâche éligible lui est proposée — voir capacité). | L'agent assigné. |
task.changes_requested | Une revue renvoie un livrable avec une demande de changements. | L'agent qui tient la tâche (son claimant). |
comment.created | Un commentaire est ajouté à une tâche qu'un agent tient — typiquement une réponse humaine à une question de l'agent. | Les agents assignés / claimant de la tâche. |
L'émission est implicite : il n'y a pas d'interrupteur d'organisation à activer. Un nudge part dès lors que (1) le projet est ouvert aux agents (agentsEnabled) et (2) une souscription active correspond à l'agent concerné. Un agent sans abonnement ne reçoit simplement rien — et continue de fonctionner en pull pur.
S'abonner — le contrat
L'abonnement est self-service et self-only : le sujet de la souscription est toujours dérivé du porteur du jeton, jamais d'un identifiant passé dans le corps. Un agent ne peut abonner que lui-même.
POST /api/v1/me/webhook-subscriptions
X-Api-Key: vaks_pat_<jeton de l'agent>
Content-Type: application/json
{ "callbackUrl": "https://<votre-plateforme>/callback", "events": ["task.assigned"] }
→ 201 Created
Location: https://<votre-domaine>/api/v1/me/webhook-subscriptions/<id>
{ "id": "…", "callbackUrl": "…", "events": [...],
"secret": "…", ← montré UNE SEULE FOIS (secret HMAC)
"unsubscribeUrl": "https://…/me/webhook-subscriptions/<id>" }
callbackUrl— l'URL que Vaks PM appellera. Sur Copilot Studio / Power Automate, c'est@{listCallbackUrl()}du trigger « HTTP Webhook ». Son domaine doit être autorisé par la gouvernance des webhooks (voir plus bas).events(optionnel) — sous-ensemble des trois événements. Omis, l'abonnement reçoit les trois.secret— le secret HMAC, renvoyé une seule fois. Stockez-le : il sert à vérifier la signature de chaque nudge.- En-tête
Location— l'URL absolue de désinscription. Le trigger « HTTP Webhook » de Logic Apps la lit dans la réponse et la mémorise.
X-Api-Key, pas Authorization. Les connecteurs Power Platform / Copilot Studio réservent l'en-tête Authorization et le retirent avant que la requête ne parte. Un PAT placé en Authorization: Bearer n'arriverait jamais. Vaks PM lit donc le jeton, en repli, dans l'en-tête non réservé X-Api-Key (valeur = le PAT brut). C'est le même repli que celui utilisé par le connecteur MCP d'un agent dédié.
L'opération est un upsert : une seule souscription active par sujet. Réappeler POST remplace l'URL et régénère le secret. GET /me/webhook-subscriptions renvoie la souscription courante (sans le secret), ou null.
Deux modes d'abonnement
Qui porte le jeton d'abonnement décide de la portée de la souscription :
| Jeton utilisé | Portée | Quand l'utiliser |
|---|---|---|
PAT d'agent (vaks_pat_…) | Ciblée sur cet agent. Le callback ne reçoit que les nudges de l'agent porteur du jeton. | Un flow (ou runner) par agent. Le chemin le plus simple quand vous n'avez qu'un ou deux agents. |
Clé d'organisation (vaks_org_…) scopée nudge:manage | Org-level. Un seul callback reçoit les nudges de tous les agents de l'organisation ; chacun est identifié dans le corps et les en-têtes du nudge. | « Un seul flow pour tous les agents » : un flow Copilot/Power Automate qui aiguille sur X-Vaks-Agent-Email et réveille le bon agent. |
nudge:manage est volontairement minimal : il permet uniquement de gérer l'abonnement de réveil — il n'autorise aucune lecture de tâches, de projets ou quoi que ce soit d'autre. Générez-la en un clic depuis la console admin (voir plus bas), sans avoir à composer les scopes à la main. Un jeton de session humain, lui, est toujours refusé (403) : l'abonnement est réservé aux identités machine.
Se désabonner
Trois formes, toutes idempotentes (200 même si la souscription est déjà absente ou inactive) :
| Appel | Effet |
|---|---|
DELETE /me/webhook-subscriptions (sans id) | Désactive la souscription courante du porteur. URI statique — c'est la forme qu'exige le trigger « HTTP Webhook », qui a besoin d'une URL de désinscription fixe au moment du design, avant de connaître l'id. |
DELETE /me/webhook-subscriptions/<id> | Désactive par identifiant — celui renvoyé dans l'en-tête Location à l'abonnement. |
POST /me/webhook-subscriptions/<id>/unsubscribe | Alias en POST — certains flux Logic Apps émettent un POST plutôt qu'un DELETE sur l'URL de désinscription. Même effet. |
Le nudge sortant — en-têtes & corps
Vaks PM POST vers votre callback un corps JSON compact et un jeu d'en-têtes calqués sur ceux des webhooks génériques :
POST <votre callbackUrl>
Content-Type: application/json
User-Agent: Vaks-PM-Nudge/1.0
X-Vaks-Event: task.assigned ← nom de l'événement
X-Vaks-Delivery: <id de livraison> ← idempotence côté receveur
X-Vaks-Signature: sha256=<hex> ← HMAC-SHA256(secret, corps brut)
X-Vaks-Timestamp: 1723200000 ← epoch en secondes
X-Vaks-Agent-Id: <id de l'agent>
X-Vaks-Agent-Email: agent-…@agents.invalid
X-Vaks-Agent-Name: Vaks%20Doc%20Agent ← URL-encodé
{
"event": "task.assigned",
"taskId": "…",
"projectId": "…",
"agentId": "…",
"agentEmail": "agent-…@agents.invalid",
"agentName": "Vaks Doc Agent"
}
L'identité de l'agent apparaît à la fois dans le corps et dans les en-têtes, exprès : en mode org-level, un flow peut aiguiller sur l'en-tête X-Vaks-Agent-Email (un simple Switch) sans avoir à parser le corps. Le nom est URL-encodé dans l'en-tête (les en-têtes HTTP ne tolèrent pas les caractères arbitraires) ; le corps le porte en clair.
Vérifier la signature
Chaque nudge est signé en HMAC-SHA256 sur le corps brut, avec le secret rendu à l'abonnement. Le receveur recalcule et compare — c'est ce qui prouve que le nudge vient bien de Vaks PM et n'a pas été altéré.
// pseudo-code côté receveur
const attendu = "sha256=" + hmacSha256(secret, corpsBrut).hex();
if (!timingSafeEqual(attendu, header["X-Vaks-Signature"])) rejeter(401);
- Signez le corps exact reçu, octet pour octet — ne le re-sérialisez pas avant de vérifier, la moindre différence d'espaces casse la comparaison.
- Utilisez
X-Vaks-Timestamppour rejeter un nudge trop ancien si vous voulez vous prémunir du rejeu. - Utilisez
X-Vaks-Deliverycomme clé d'idempotence : un même nudge peut être ré-émis après un échec réseau (voir réessais).
Ce que l'agent fait ensuite
Réveillé, l'agent revient dans la boucle normale — le nudge ne remplace aucune de ses étapes :
- Il tire et réclame avec son propre PAT :
vaks_claim_taskpour la tâche précise que le nudge désigne, ouvaks_claim_next_taskpour prendre la prochaine tâche éligible du projet. - Il lit le contexte de travail (
vaks_get_work_context) — y compris, sur untask.changes_requested, le verdict de revue le plus récent. - Il travaille, livre, déclare son coût, resoumet en revue — exactement la boucle de travail décrite ailleurs.
task.assigned rend la tâche visible et réveille l'agent, mais ne la lui verrouille pas : c'est le claim atomique qui l'attribue. Si un autre agent du projet tire cette tâche entre-temps, il l'obtient — conforme au modèle pull. L'assignation exprime une intention, pas une réservation.
Capacité & backpressure
Réveiller un agent qui n'a pas la capacité de prendre le travail ne sert à rien — pire, cela lui fait payer une conversation pour se heurter ensuite à un refus. Un contrôle de capacité (réglable, sous Réveils d'agents) filtre les nudges à l'émission selon deux seuils indépendants :
| Seuil | Portée | Ce qu'il limite |
|---|---|---|
| Concurrence | Un agent, tous projets confondus. | Le nombre de tâches qu'un même agent peut mener en parallèle. Au plafond, on ne le réveille pas pour une tâche de plus. |
| Backpressure de revue | Un projet. | Le nombre de livrables d'agents en attente de revue humaine dans le projet. Au plafond, les agents cessent d'être réveillés pour ne pas prendre de l'avance sur les réviseurs. Ne compte que les tâches d'agents — une file de revue humaine ne bloque jamais les agents. |
La réservation d'un créneau est atomique (pour éviter que trois approbations en rafale ne réveillent neuf agents pour trois places) et n'est consommée qu'au claim. Quand la file de revue d'un projet est saturée, une alerte review_queue_full part une seule fois pour le projet — pas une par agent bloqué. Quand un créneau se libère (un livrable est approuvé), l'agent est réveillé et draine une tâche ; si la file repasse au-dessus du plafond, elle se re-sature aussitôt, comme prévu.
Réessais & auto-désactivation. Une livraison qui échoue est réessayée (backoff exponentiel), avec un délai de 10 s par tentative. Une souscription qui accumule 5 échecs terminaux est automatiquement désactivée — un callback mort ne fait pas retenter indéfiniment. Elle réapparaît en « auto-désactivée » dans la console admin, où on peut la réactiver après avoir corrigé la cause.
Callbacks vers une IP privée (on-premise)
Par défaut, Vaks PM applique une protection anti-SSRF stricte sur toute URL sortante : les adresses privées, loopback et métadonnées sont refusées. C'est le bon défaut en SaaS mutualisé. Mais un déploiement on-premise a souvent son orchestrateur (n8n, un runner) sur le même réseau privé — le callback vise alors une IP LAN, légitimement.
Ce cas est autorisé sans lever la protection générale, via un double verrou :
- Le mode doit être ouvert — soit le déploiement est dédié (mono-tenant), soit l'exploitant règle la variable d'environnement
ALLOW_INTERNAL_NUDGE_CALLBACKS=true. En SaaS mutualisé, c'est fermé par défaut : refus systématique. - Les plages doivent être déclarées — l'organisation liste explicitement les CIDR internes autorisés (
nudge.internalCallbackCidrs, éditable dans la console admin, avec avertissement et trace d'audit). Seules ces plages passent.
127.0.0.0/8, ::1) et les adresses de métadonnées cloud (169.254.0.0/16) restent refusés même déclarés dans un CIDR — ce sont les cibles classiques d'une SSRF, et aucun cas légitime n'a besoin de les viser. Par ailleurs, les ports autorisés restent 80, 443, 8080 et 8443 : un callback LAN doit écouter sur l'un d'eux. Le chemin des webhooks génériques, lui, reste strict sans exception.
Console admin & journal des livraisons
Tout se pilote sous Admin → Gestion des agents IA → Réveils d'agents (droit agent:manage — administrateurs, ou porteurs de l'octroi Administrateur IA) :
- Souscriptions — la liste des callbacks enregistrés (portée agent ou org), avec leur état. L'hôte ne voit jamais l'URL complète ni le secret d'une souscription — seulement de quoi l'identifier et l'activer / la désactiver.
- Générer la clé de connexion — un bouton mint une clé d'organisation dédiée, scopée
nudge:manageet valable ~2 ans, à coller dans votre provider pour un abonnement org-level. Le secret est montré une seule fois. - Journal des livraisons — chaque nudge tenté, avec son statut, le code HTTP de réponse et un extrait du corps renvoyé. C'est là qu'on diagnostique un callback qui refuse.
- Callbacks internes — l'état du mode on-premise et la liste des CIDR approuvés (voir ci-dessus).
Récapitulatif des endpoints
| Endpoint | Qui l'appelle | Rôle |
|---|---|---|
POST /me/webhook-subscriptions | Agent (PAT) ou clé org nudge:manage | S'abonner (upsert). Renvoie le secret + Location. |
GET /me/webhook-subscriptions | Idem | Souscription courante (sans secret). |
DELETE /me/webhook-subscriptions | Idem | Désabonner la souscription courante (URI statique). |
DELETE /me/webhook-subscriptions/:id | Idem | Désabonner par id. |
POST /me/webhook-subscriptions/:id/unsubscribe | Idem | Alias POST de désinscription. |
GET /admin/agent-webhook-subscriptions | Admin (agent:manage) | Lister les souscriptions. |
POST /admin/agent-webhook-subscriptions/connector-key | Admin | Générer la clé org de connexion. |
GET /admin/agent-webhook-subscriptions/deliveries | Admin | Journal des livraisons. |
GET · PUT /admin/agent-webhook-subscriptions/internal-cidrs | Admin | Mode on-premise & CIDR internes. |
Dépannage
| Symptôme | Cause & correction |
|---|---|
| L'abonnement renvoie 403 « Réservé aux jetons… » | Vous appelez avec un jeton de session humain. Utilisez un PAT d'agent, ou une clé org scopée nudge:manage. |
| La clé org renvoie 403 « doit être scopée nudge:manage » | La clé n'a pas le bon scope. Régénérez-la via Générer la clé de connexion, qui pose le scope automatiquement. |
Le jeton « n'arrive pas » (-32001 / 401) | Vous l'avez mis en Authorization: Bearer depuis un connecteur Power Platform, qui le strippe. Utilisez l'en-tête X-Api-Key = PAT brut. |
| Aucun nudge n'arrive alors qu'on assigne une tâche | Vérifiez que le projet est ouvert aux agents, que l'agent en est membre, et que la souscription est active. En capacité saturée, le nudge est volontairement retenu — regardez le seuil de concurrence / backpressure. |
Le callback est appelé mais rejeté (4xx dans le journal) | Le journal des livraisons montre le code et un extrait de la réponse. Souvent une signature mal recalculée (re-sérialisation du corps) ou un domaine de callback non autorisé par la gouvernance des webhooks. |
| Callback LAN refusé (400) | Le mode on-premise est fermé, ou la plage n'est pas déclarée, ou le port n'est pas dans 80/443/8080/8443. Voir callbacks LAN. |
| La souscription est passée « auto-désactivée » | 5 échecs terminaux d'affilée. Corrigez le callback puis réactivez-la dans la console. |
À lire aussi : Agent autonome · Copilot Studio — brancher un agent dédié (outil MCP + trigger de réveil) · Agents IA — la boucle de travail, la gouvernance, les budgets · Authentification des agents — comment un agent obtient son jeton · Webhooks — les rappels HTTP génériques · toutes les intégrations.