# Piste d'audit webhook : tracer les workflows d'agents de bout en bout

Une action d'agent qui démarre un workflow asynchrone possède deux historiques : la requête envoyée et l'événement reçu en retour. Les équipes journalisent souvent assez bien le premier pour répondre à la question « l'agent a-t-il appelé l'API ? », et assez bien le second pour répondre à « notre récepteur a-t-il reçu un webhook ? ». Puis, lors d'un incident, elles découvrent que personne ne peut prouver que ces enregistrements décrivent le même travail.

Une piste d'audit webhook doit relier l'intention, l'autorisation, les tentatives sortantes, l'accusé de réception distant, la réception entrante, la vérification et l'état métier que vous avez choisi d'accepter. Considérer une réponse 200 comme la fin de l'enregistrement est la meilleure façon de rendre un paiement, un déploiement, un ticket ou une modification d'accès impossible à expliquer trois jours plus tard.

## Une piste d'audit webhook enregistre deux faits différents

Une piste d'audit webhook doit préserver la différence entre une requête d'action et une notification d'événement, car elles répondent à des questions différentes. Le côté sortant indique ce qu'un agent a demandé à un service distant. Le côté entrant indique ce qu'un expéditeur a ensuite affirmé s'être produit.

Ces faits peuvent concerner un même workflow, mais leurs modes d'échec sont distincts. Un agent peut envoyer une commande, perdre sa connexion, puis la renvoyer. Le service distant peut accepter la commande, l'exécuter quelques minutes plus tard et envoyer deux callbacks identiques. Votre récepteur peut vérifier le premier callback, puis échouer avant d'enregistrer l'état qui en découle. Une ligne bien ordonnée dans un journal d'application ne suffit pas à expliquer cette chaîne.

Lorsque j'examine ces systèmes, j'utilise six types d'enregistrements :

- Un enregistrement d'action identifie l'exécution de l'agent, l'autorisation humaine, l'opération demandée et la cible prévue.
- Un enregistrement de tentative sortante identifie chaque transmission HTTP, y compris le condensat de la requête et la réponse reçue.
- Un enregistrement de référence distante conserve tout identifiant renvoyé par le fournisseur, comme un identifiant de tâche ou d'opération.
- Un enregistrement de réception conserve chaque livraison HTTP entrante avant toute modification de l'état métier.
- Un enregistrement de vérification indique précisément pourquoi le récepteur a accepté, refusé ou placé cette livraison en quarantaine.
- Un enregistrement de transition d'état indique ce que le workflow a changé après le traitement d'un événement vérifié.

Ne fusionnez pas une tentative avec une action. Une action peut produire plusieurs tentatives. Ne fusionnez pas non plus une réception avec un événement. Un événement du fournisseur peut atteindre votre endpoint plusieurs fois. Cette distinction semble tatillonne jusqu'au jour où un ingénieur doit expliquer si un second déploiement vient d'une nouvelle tentative de l'agent, du client HTTP ou d'une nouvelle livraison du même événement par le fournisseur.

La norme RFC 9110 définit POST en termes volontairement larges : la ressource cible traite la représentation selon ses propres règles. C'est pourquoi une réponse 202 signifie généralement « accepté pour un traitement ultérieur », et même une réponse 200 signifie seulement que l'endpoint a terminé le traitement de la requête. Elle ne certifie pas le résultat métier distant. Si le fournisseur expose un endpoint distinct pour l'état de l'opération ou un callback, ces éléments ultérieurs font foi pour le résultat.

Un bon enregistrement permet au lecteur de suivre l'histoire dans l'ordre sans déduire les faits à partir des horodatages :

```text
agent session sess_7c1e authorized action act_01
act_01 created outbound attempt out_01 with idempotency ref idem_44
remote service accepted out_01 and returned operation op_903
receiver accepted delivery rcp_01 for provider event evt_775
rcp_01 verified its signature and linked evt_775 to op_903
workflow wf_18 moved from pending to completed
```

Il s'agit d'une chaîne d'affirmations, pas d'un champ d'état unique. Chaque affirmation doit avoir sa propre source et son propre moment.

## La corrélation exige plusieurs identifiants

Un seul identifiant de corrélation ne suffit pas pour tracer un webhook, car les différents acteurs créent des identifiants pour des périmètres différents. Utilisez un petit ensemble d'identifiants dont la responsabilité est explicite, puis enregistrez leurs relations.

Commencez par un identifiant d'action interne. Créez-le avant tout appel réseau et associez-le à la session de l'agent, à l'opération demandée, à la décision d'autorisation et à l'entrée d'audit immuable. Cet identifiant répond à la question « quelle instruction de l'agent a provoqué ce travail ? ». Il ne doit pas changer lorsque le client réessaie.

Créez un identifiant de tentative sortante chaque fois que votre client HTTP transmet une requête. Il répond à la question « quelle tentative sur le réseau a produit cette réponse ou cette erreur ? ». Ajoutez une référence d'idempotence lorsque l'API distante la prend en charge. Cette référence indique que plusieurs soumissions doivent correspondre à une seule opération distante logique. Elle ne dit pas si une tentative HTTP précise a atteint le serveur.

Lorsque le service distant renvoie un identifiant d'opération, enregistrez-le immédiatement avec la tentative qui l'a reçu. Si votre requête accepte une référence client ou un champ de métadonnées, placez-y votre identifiant d'action après avoir vérifié que le fournisseur le renverra dans les callbacks ou les réponses d'état. Ne placez jamais un secret, le nom d'un employé ou un prompt complet dans un champ de référence. Ces champs apparaissent souvent dans les consoles des fournisseurs, les tickets d'assistance et les charges utiles d'événements.

Les callbacks entrants ajoutent deux autres identifiants : l'identifiant de l'événement du fournisseur et votre identifiant de réception. L'identifiant de l'événement du fournisseur sert à dédupliquer les événements de cet expéditeur. Votre identifiant de réception désigne la livraison HTTP précise reçue par votre infrastructure, avec ses en-têtes, son adresse source si vous la conservez, le condensat du corps brut et le résultat de la vérification.

Le tableau des relations devrait ressembler à ceci :

| Identifiant | Créé par | Stable entre les tentatives ? | Répond à la question |
|---|---|---:|---|
| Identifiant d'action | Votre service d'actions | Oui | Quelle requête de l'agent a lancé le travail ? |
| Identifiant de tentative | Votre client HTTP | Non | Quelle transmission a produit ce résultat ? |
| Référence d'idempotence | Votre service d'actions | Oui | Quelles soumissions désignent la même commande distante ? |
| Identifiant d'opération distante | Fournisseur | Généralement | Quelle tâche ou quel objet distant a changé ? |
| Identifiant d'événement du fournisseur | Fournisseur | Oui pour un événement donné | Quel callback faut-il dédupliquer ? |
| Identifiant de réception | Votre récepteur | Non | Quelle livraison avons-nous reçue ? |

CloudEvents est utile ici, même lorsqu'un fournisseur n'envoie pas de CloudEvents. Sa spécification sépare `id`, `source`, `type`, `subject` et `time`. Cette séparation évite une erreur récurrente : traiter un identifiant d'événement comme un identifiant de workflow. Un identifiant d'événement désigne un événement provenant d'une source donnée. Un identifiant de workflow désigne le travail que vous tracez. Ils peuvent pointer vers le même objet distant, mais ils ne signifient pas la même chose.

Si un fournisseur vous donne seulement une charge utile de callback avec un identifiant d'objet, reliez-la avec prudence. Marquez le lien comme exact uniquement lorsque l'identifiant d'objet provient de la réponse sortante enregistrée ou d'une recherche d'état authentifiée. Une correspondance fondée sur une adresse e-mail, un titre, un montant ou un horodatage est une supposition déguisée en corrélation. Ne l'utilisez pas dans les conclusions d'audit.

## Une réponse 2xx et un callback répondent à des questions différentes

Une réponse 2xx clôt l'échange HTTP. Un callback vérifié peut confirmer un changement d'état distant. Votre workflow a besoin des deux et doit décrire honnêtement l'écart entre eux.

Imaginez un agent qui demande à un service de build hébergé de publier un artefact. Le service renvoie 202 et un identifiant d'opération. Votre service enregistre la requête comme acceptée, puis attend. Dix minutes plus tard, un callback indique que la publication a échoué parce qu'un dépôt en aval a refusé un manifeste obligatoire. Si l'enregistrement d'audit est passé à « succès » au moment du 202, il contredit maintenant les propres preuves du fournisseur.

Utilisez des états qui nomment les preuves dont vous disposez. Par exemple :

1. `requested` signifie que l'action de l'agent a été autorisée et qu'un élément de travail a été créé.
2. `submitted` signifie qu'au moins une tentative sortante a reçu une réponse d'acceptation, ou qu'un résultat ambigu pouvant être récupéré attend une vérification.
3. `confirmed` signifie qu'un callback vérifié ou une réponse d'état authentifiée a établi le résultat attendu.
4. `failed` signifie qu'une preuve faisant autorité a établi l'échec.
5. `unknown` signifie que vous ne pouvez pas encore établir si le système distant a agi.

L'état `unknown` est nécessaire. Les équipes ne l'aiment pas, car il rend les tableaux de bord moins attrayants. Je préfère cela à une duplication silencieuse. Une expiration du délai après l'envoi d'un POST produit une livraison ambiguë : le système distant peut l'avoir reçue et traitée, ou ne jamais l'avoir vue. Réessayer sans mécanisme d'idempotence peut créer deux opérations distantes. Appeler la première tentative « échec » encourage précisément cette erreur.

Un callback tardif ne l'emporte pas automatiquement. Supposons qu'un agent demande une annulation après la commande initiale et que votre workflow interne enregistre une annulation valide. Un callback de fin reçu plus tard peut décrire ce qui s'est produit à distance avant que l'annulation ne prenne effet. Conservez-le, vérifiez-le, reliez-le et enregistrez le conflit. Ne laissez pas un gestionnaire générique remplacer un état terminal d'annulation simplement parce que « terminé » est mieux classé dans l'énumération de quelqu'un.

Écrivez une règle de transition pour chaque type de callback. Une approbation de paiement, la fin d'un build, la création d'un utilisateur et la confirmation d'une suppression ne doivent pas déclencher les mêmes transitions. La règle doit indiquer quels états antérieurs autorisent la transition, quelles preuves le gestionnaire exige et si un opérateur doit résoudre un conflit.

## Le récepteur doit préserver les preuves avant de les analyser

Votre récepteur doit capturer la livraison brute, la vérifier et la dédupliquer avant d'effectuer un effet de bord. Analyser le JSON en premier et ne stocker que certains champs détruit les preuves si l'analyseur, le schéma ou le code applicatif s'avère ensuite incorrect.

Au moment de la réception, enregistrez les éléments suivants dans un stockage d'événements protégé :

- L'identifiant de réception et l'horodatage de réception par le serveur.
- La méthode de la requête, la route, les en-têtes sélectionnés et le condensat cryptographique du corps brut exact.
- L'identité de l'expéditeur attendue et le mécanisme de vérification appliqué.
- L'identifiant de l'événement du fournisseur, s'il est fourni, ainsi que le type d'événement analysé.
- La décision : acceptée, doublon, refusée ou placée en quarantaine, avec un code explicatif.

Ne conservez les charges utiles brutes que pendant la période justifiée par vos besoins d'enquête et de conformité. Un condensat suffit généralement à prouver que deux charges utiles sont identiques. Si vous conservez un corps, chiffrez-le, limitez son accès et évitez de le copier dans les journaux d'application ordinaires. Les webhooks contiennent souvent des données personnelles, des métadonnées de dépôts, des adresses et des notes internes. Un stockage d'audit qui divulgue la charge utile devient un risque, pas une preuve.

La vérification de signature doit s'effectuer sur le corps exactement comme l'expéditeur l'a signé. Une couche intermédiaire qui analyse le JSON, le reformate, puis vérifie les octets reformattés rejettera des livraisons légitimes ou, pire, permettra un traitement incohérent. Lisez attentivement la documentation de vérification du fournisseur. Certains mécanismes signent `timestamp + "." + raw_body`, d'autres signent uniquement le corps brut, et d'autres utilisent des signatures asymétriques avec des clés publiques renouvelées.

Pour un mécanisme HMAC générique qui signe uniquement le corps brut, cette commande montre la forme du condensat attendue à partir des octets non modifiés :

```sh
printf '%s' "$RAW_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"
# SHA2-256(stdin)= 4d3c...hex digest...
```

Il s'agit d'un outil de diagnostic, pas d'un remplacement de la chaîne canonique exacte du fournisseur. Si celui-ci inclut un horodatage ou un préfixe de version, cette commande générique donnera un résultat incorrect. Cette erreur apparaît souvent parce que les ingénieurs vérifient une approximation pratique au lieu de l'algorithme documenté par l'expéditeur.

La validité de la signature n'empêche pas les rejeux. Si l'expéditeur fournit un horodatage signé, refusez les livraisons qui sortent d'une fenêtre étroite, après avoir prévu la dérive d'horloge mesurée. Enregistrez ensuite les identifiants d'événement du fournisseur dans un stockage de déduplication durable avant d'appeler le travail en aval. Si vous ne pouvez pas compter sur un identifiant d'événement, dédupliquez à partir d'un condensat associé à l'expéditeur et d'une durée de conservation adaptée, en sachant que deux événements identiques mais légitimes pourront alors nécessiter un traitement particulier.

Ne renvoyez une réponse HTTP qu'après avoir rendu durable la décision de réception. Si vous renvoyez d'abord un succès et que le processus s'arrête avant l'écriture de déduplication, l'expéditeur peut réessayer et votre gestionnaire peut traiter deux fois le même événement. Ce bug reste invisible lors de tests à faible volume et apparaît précisément dans les conditions de panne où le trafic webhook augmente.

## Les tentatives répétées révèlent les zones trop vagues de vos enregistrements

Les nouvelles tentatives sont normales, pas exceptionnelles, et chaque couche peut réessayer indépendamment. Les agents réessaient après une expiration de délai. Les bibliothèques HTTP réessaient après une erreur de connexion. Les fournisseurs d'API réessaient les callbacks. Les consommateurs de files réessaient un gestionnaire en échec. Un enregistrement qui résume tout cela par « nombre de tentatives : 3 » n'aide personne.

Reprenons un échec que j'ai rencontré sous plusieurs formes. Un agent demande la création d'un enregistrement d'accès distant. Le client envoie un POST et expire après que les octets ont quitté la machine. Le service distant crée l'enregistrement et met un callback en file d'attente. Le framework de l'agent réessaie, car il voit une expiration. La seconde requête crée un autre enregistrement, car la couche d'action a généré une nouvelle référence d'idempotence à chaque tentative. Les deux callbacks arrivent. Le récepteur utilise seulement une adresse e-mail pour les associer, décide qu'il s'agit de doublons et supprime le second. La page d'audit affiche une seule requête terminée. Le service distant possède désormais deux enregistrements d'accès.

Chaque composant a agi d'une manière plausible. Le système a échoué parce qu'il n'a pas conservé une commande logique entre les différentes limites de nouvelle tentative.

Corrigez plutôt la séquence :

1. Générez l'identifiant d'action et la référence d'idempotence une seule fois, avant la première tentative sortante.
2. Enregistrez chaque tentative séparément, y compris les expirations et les erreurs de transport.
3. En cas d'ambiguïté, interrogez le fournisseur avec la référence d'idempotence ou la référence client avant d'émettre une nouvelle commande.
4. Acceptez chaque callback authentifié comme une réception, puis dédupliquez uniquement l'identifiant de l'événement du fournisseur, pas l'objet distant lui-même.
5. Comparez le nombre attendu d'objets distants avec l'action enregistrée avant de déclarer le workflow terminé.

La première vérification d'idempotence appartient à l'émetteur et la seconde au récepteur. Elles résolvent des problèmes différents. L'idempotence côté émetteur empêche les commandes distantes en double. La déduplication côté récepteur empêche le traitement répété d'un même événement distant. Les équipes mettent souvent en place l'une et pensent avoir obtenu l'autre.

N'utilisez pas l'heure d'arrivée comme ordre de vérité métier. Les fournisseurs peuvent livrer les événements en retard ou dans le désordre, et votre propre file peut retarder le traitement. Stockez au moins trois moments : la création de l'action par votre service, l'envoi de la tentative par votre client HTTP ou la réception de sa réponse, et l'acceptation du callback par votre récepteur. Conservez séparément l'heure de l'événement déclarée par l'expéditeur. L'horloge de l'expéditeur est une preuve fournie par cet expéditeur, pas la vôtre.

## L'autorisation doit survivre à la frontière asynchrone

L'approbation humaine d'une action d'agent doit être rattachée à l'action elle-même, pas au callback qui arrive ensuite. Un callback fournit des informations sur le travail distant. Il ne doit jamais acquérir discrètement le pouvoir de déclencher une nouvelle opération privilégiée simplement parce qu'il partage un champ de corrélation avec une requête approuvée.

Cela compte lorsque les callbacks peuvent contenir des URL, des noms d'objets, des métadonnées contrôlées par l'utilisateur ou des instructions suivies par un gestionnaire interne. Une mauvaise conception reçoit un événement « tâche terminée », puis laisse un outil d'automatisation générique récupérer une URL de résultat ou exécuter une commande de suivi avec des identifiants trop larges. L'approbation initiale de l'agent portait sur la soumission d'une tâche, pas sur un ensemble ouvert d'actions intégrées à un événement.

Enregistrez l'action autorisée avec des éléments concrets : session de l'acteur, endpoint demandé ou modèle de commande SSH, périmètre de la cible, identité de l'identifiant, résultat de l'approbation et heure de l'approbation. Pour chaque appel sortant, faites référence à cet enregistrement d'autorisation. Pour chaque callback, faites référence à l'action seulement après vérification et corrélation. Ce sens est important. Une requête entrante ne doit pas chercher dans votre base une approbation antérieure qui lui convient et l'emprunter.

Sallyport garde les identifiants des agents hors du processus de l'agent et enregistre à la fois les exécutions d'agents et les appels individuels. Cela facilite la conservation des preuves côté sortant. Le récepteur de callbacks a tout de même besoin de ses propres enregistrements de réception et de workflow, car un journal d'actions HTTP ne peut pas savoir si un système distant a envoyé plus tard un événement valide.

Utilisez des identifiants distincts pour les deux directions. L'identifiant qui autorise votre appel API sortant ne doit normalement pas servir à vérifier les signatures entrantes, et le secret de vérification entrant ne doit pas autoriser un gestionnaire de callbacks à appeler des API externes arbitraires. Séparer ces responsabilités limite les dommages lorsqu'une route de récepteur, une dépendance ou une destination de journaux présente un problème.

## Les preuves d'intégrité doivent couvrir les liens, pas seulement les appels

Un enregistrement append-only des appels sortants est utile, mais il ne prouve pas les décisions de corrélation prises ensuite. Un opérateur ou un bug applicatif peut associer le mauvais callback à la mauvaise action sans modifier l'un ou l'autre des enregistrements HTTP d'origine.

Faites de la corrélation un événement d'audit à part entière. L'événement doit inclure l'identifiant d'action, l'identifiant de réception, le fondement du lien, l'acteur ou le processus qui a pris la décision et un condensat des champs utilisés. Utilisez des fondements explicites comme `remote_operation_id_exact`, `client_reference_exact`, `authenticated_status_lookup` ou `manual_review`. N'écrivez pas simplement « correspondant » en laissant l'enquêteur deviner.

Un journal chaîné par condensats peut montrer que les enregistrements n'ont pas changé après leur création, à condition de protéger le chemin d'ajout et de conserver des points de contrôle. Il ne peut pas prouver que l'application a pris la bonne décision au moment voulu. Il est sain d'énoncer clairement cette limite. Les preuves d'intégrité vous donnent un compte rendu stable de ce que votre système a enregistré, mais elles ne transforment pas une corrélation faible en fait établi.

Le journal d'audit chiffré et chaîné par condensats de Sallyport peut être vérifié hors ligne avec `sp audit verify`, même sans clé de coffre. Utilisez ce type de vérification pour les enregistrements d'action, puis conservez une référence immuable comparable dans votre stockage de workflows, reliée aux identifiants d'action et d'appel concernés.

Pour les workflows à conséquences importantes, ajoutez une tâche de rapprochement qui compare trois populations : les actions soumises, les opérations distantes connues du fournisseur et les callbacks acceptés par votre récepteur. Cette tâche doit créer une exception pour chaque élément sans paire au lieu de le clôturer automatiquement. Un callback manquant peut indiquer une panne du fournisseur, un endpoint incorrect, un échec lors de la rotation d'une signature ou un bug de workflow. Il faut des preuves avant de chercher à être rassuré.

## L'observabilité doit permettre à l'enquêteur de rejouer la décision

Un enquêteur doit pouvoir partir de n'importe quel identifiant et reconstruire le workflow sans accès privilégié aux prompts des agents ou aux secrets d'API. Concevez les chemins de recherche avant de mettre l'intégration en production.

À partir d'un identifiant d'action, l'enregistrement doit afficher la session de l'agent, l'autorisation, le libellé de l'identifiant, la forme de la requête après masquage, toutes les tentatives, les références distantes, les réceptions associées et l'état terminal du workflow. À partir d'un identifiant d'événement du fournisseur, il doit afficher chaque livraison de cet événement, les résultats de vérification, le résultat de la déduplication, l'opération associée et les changements d'état. À partir d'un objet métier interne, il doit afficher les preuves exactes qui l'ont associé à une action d'agent.

Utilisez des champs structurés, pas une seule chaîne narrative. Un contrat d'événement utile peut être copié dans une revue de schéma ou un pipeline de journaux :

```json
{
  "record_type": "callback_receipt",
  "receipt_id": "rcp_01J...",
  "received_at": "2025-03-08T22:14:31Z",
  "sender": "build-service",
  "provider_event_id": "evt_775",
  "event_type": "publication.finished",
  "raw_body_sha256": "4d3c...",
  "signature": {"scheme": "hmac-sha256", "result": "valid"},
  "correlation": {
    "action_id": "act_01J...",
    "remote_operation_id": "op_903",
    "basis": "remote_operation_id_exact"
  },
  "processing": {"deduplication": "new", "result": "completed"}
}
```

Le condensat du corps, le résultat de vérification et le fondement de la corrélation sont plus utiles qu'un vague `status: success`. Ils permettent de vérifier les affirmations. Si un fournisseur conteste un callback, comparez le condensat conservé. Si un ingénieur conteste une correspondance, examinez son fondement. Si un doublon a entraîné des effets de bord, vérifiez si le récepteur a écrit son enregistrement de déduplication avant de distribuer le travail.

Évitez de journaliser les en-têtes d'autorisation, les jetons bearer, les clés privées, les secrets de signature ou les URL complètes contenant des identifiants. Masquez les valeurs de requête lorsqu'elles contiennent des données sensibles, mais conservez assez d'identité pour distinguer deux cibles. J'ai vu des équipes masquer une URL au point de la rendre inutile, puis ne plus pouvoir déterminer si un agent avait contacté la production ou un endpoint de test. Stockez un hôte normalisé, un modèle de route, la méthode et un identifiant de cible soigneusement limité.

## Construisez la trace avant que les agents ne lancent des appels asynchrones

Définissez les identifiants, les règles de réception et les transitions d'état avant de donner à un agent une action qui démarre un travail asynchrone. Les ajouter après un litige coûte cher, car les preuves manquantes n'ont jamais existé.

Organisez un exercice d'échec volontaire. Soumettez une action de test sans danger, forcez le client à expirer après la transmission si votre environnement le permet, renvoyez le même callback, envoyez un callback avec une signature invalide et livrez un callback valide après l'entrée du workflow dans un état terminal. Vérifiez que l'enregistrement d'audit explique chaque résultat sans qu'une personne ait à combler les vides de mémoire.

Si votre système ne peut pas répondre à la question « quelle action autorisée a provoqué ce callback, avec quelles preuves exactes, et qu'en avons-nous fait ? », il n'a pas encore de véritable piste d'audit pour les webhooks. Il possède deux ensembles de journaux qui partagent par hasard la même horloge.
