# Registro de auditoría de webhooks: rastrea los flujos de los agentes de principio a fin

Una acción de un agente que inicia un flujo de trabajo asíncrono tiene dos historiales: la solicitud que envió y el evento que regresó. Los equipos suelen registrar el primero lo bastante bien como para responder «¿el agente llamó a la API?» y el segundo lo bastante bien como para responder «¿nuestro receptor recibió un webhook?». Durante un incidente descubren que nadie puede demostrar que esos registros describen el mismo trabajo.

Un registro de auditoría de webhooks debe conectar la intención, la autorización, la entrega saliente, el reconocimiento remoto, la recepción entrante, la verificación y el estado empresarial que decidiste aceptar. Tratar una respuesta 200 como el final del registro es la razón por la que un pago, despliegue, ticket o cambio de acceso se vuelve imposible de explicar tres días después.

## Un registro de auditoría de webhooks conserva dos hechos distintos

Un registro de auditoría de webhooks debe mantener la diferencia entre una solicitud de acción y una notificación de evento porque responden preguntas diferentes. El lado saliente indica qué pidió un agente a un servicio remoto. El lado entrante indica qué afirmó después algún remitente que había ocurrido.

Esos hechos pueden referirse al mismo flujo de trabajo, pero tienen modos de fallo distintos. Un agente puede enviar un comando, perder la conexión y enviarlo de nuevo. El servicio remoto puede aceptar el comando, ejecutarlo minutos después y enviar dos callbacks idénticos. Tu receptor puede verificar el primer callback, pero fallar antes de guardar el estado resultante. Una línea ordenada en el registro de la aplicación no puede explicar esa cadena.

Uso seis tipos de registros cuando reviso estos sistemas:

- Un registro de acción identifica la ejecución del agente, la autorización humana, la operación solicitada y el objetivo previsto.
- Un registro de intento saliente identifica cada transmisión HTTP, incluido un resumen de la solicitud y la respuesta recibida.
- Un registro de referencia remota conserva cualquier identificador que devuelva el proveedor, como un ID de trabajo u operación.
- Un registro de recepción conserva cada entrega HTTP entrante antes de que el procesamiento empresarial cambie nada.
- Un registro de verificación explica exactamente por qué el receptor aceptó, rechazó o puso en cuarentena esa entrega.
- Un registro de transición de estado indica qué cambió en el flujo de trabajo después de procesar un evento verificado.

No mezcles un intento con una acción. Una acción puede producir varios intentos. Tampoco mezcles una recepción con un evento. Un evento del proveedor puede llegar varias veces a tu endpoint. La distinción parece minuciosa hasta que un ingeniero tiene que explicar si un segundo despliegue provino del reintento del agente, del reintento del cliente HTTP o del reenvío de un mismo evento por parte del proveedor.

RFC 9110 define POST de forma deliberadamente amplia: el recurso objetivo procesa la representación según sus propios criterios. Por eso una respuesta 202 suele significar «aceptado para trabajar más tarde», y una respuesta 200 solo indica que el endpoint terminó de gestionar la solicitud. No certifica el resultado empresarial remoto. Si el proveedor ofrece un endpoint separado para consultar el estado de la operación o un callback, esa evidencia posterior determina el resultado.

Un buen registro permite que un revisor lea la historia en orden sin deducir hechos a partir de las marcas de tiempo:

```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
```

Esa es una cadena de afirmaciones, no un único campo de estado. Cada afirmación necesita su propio origen y momento.

## La correlación necesita más de un identificador

Un único ID de correlación no resuelve el rastreo de webhooks porque cada parte crea identificadores para ámbitos diferentes. Usa un pequeño conjunto de identificadores con un propietario claro y registra sus relaciones.

Empieza con un ID de acción interno. Créalo antes de cualquier llamada de red y asígnalo a la sesión del agente, la operación solicitada, la decisión de autorización y la entrada de auditoría inmutable. Este ID responde a la pregunta «¿qué instrucción del agente provocó este trabajo?». No debe cambiar cuando el cliente reintenta.

Crea un ID de intento saliente cada vez que tu cliente HTTP transmita algo. Este ID responde a la pregunta «¿qué intento en la red produjo esta respuesta o error?». Incluye una referencia de idempotencia cuando la API remota la admita. Una referencia de idempotencia indica que los envíos repetidos deben corresponder a una única operación remota lógica. No te dice si un intento HTTP concreto llegó al servidor.

Cuando el servicio remoto devuelva un ID de operación, guárdalo de inmediato junto al intento que lo recibió. Si tu solicitud admite una referencia del cliente o un campo de metadatos, coloca ahí tu ID de acción después de confirmar que el proveedor lo devolverá en los callbacks o en las respuestas de estado. Nunca pongas un secreto, el nombre de un empleado o el prompt completo en un campo de referencia. Estos campos suelen aparecer en las consolas del proveedor, los tickets de soporte y los payloads de eventos.

Los callbacks entrantes añaden dos IDs más: el ID del evento del proveedor y tu ID de recepción. El ID del evento del proveedor permite eliminar duplicados de ese remitente. Tu ID de recepción identifica la entrega HTTP exacta que recibió tu infraestructura, incluidos sus encabezados, la dirección de origen si la conservas, el resumen del cuerpo original y el resultado de la verificación.

La tabla de relaciones debería tener este aspecto:

| Identificador | Creado por | ¿Se mantiene en los reintentos? | Responde a |
|---|---|---:|---|
| ID de acción | Tu servicio de acciones | Sí | ¿Qué solicitud del agente inició el trabajo? |
| ID de intento | Tu cliente HTTP | No | ¿Qué transmisión produjo este resultado? |
| Referencia de idempotencia | Tu servicio de acciones | Sí | ¿Qué envíos representan el mismo comando remoto? |
| ID de operación remota | Proveedor | Normalmente | ¿Qué trabajo u objeto remoto cambió? |
| ID de evento del proveedor | Proveedor | Sí, para un evento | ¿Qué callback debe deduplicarse? |
| ID de recepción | Tu receptor | No | ¿Qué entrega recibimos? |

CloudEvents resulta útil aquí, incluso cuando un proveedor no envía CloudEvents. Su especificación separa `id`, `source`, `type`, `subject` y `time`. Esa separación evita un error recurrente: tratar el ID de un evento como el ID de un flujo de trabajo. Un ID de evento identifica un evento de un origen. Un ID de flujo de trabajo identifica el trabajo que estás rastreando. Pueden apuntar al mismo objeto remoto, pero no significan lo mismo.

Si un proveedor solo te entrega un payload de callback con un ID de objeto, relaciónalo con cautela. Marca la relación como exacta únicamente cuando el ID del objeto provenga de la respuesta saliente registrada o de una consulta de estado autenticada. Coincidir por dirección de correo, texto del título, importe o marca de tiempo es una suposición disfrazada de correlación. No la incluyas en las conclusiones de auditoría.

## Una respuesta 2xx y un callback resuelven preguntas distintas

Una respuesta 2xx resuelve el intercambio HTTP. Un callback verificado puede resolver un cambio de estado remoto. Tu flujo de trabajo necesita ambos y debe describir con honestidad la distancia entre ellos.

Imagina un agente que pide a un servicio de compilación alojado que publique un artefacto. El servicio devuelve 202 y un ID de operación. Tu servicio registra la solicitud como aceptada y espera. Diez minutos después, un callback indica que la publicación falló porque un repositorio posterior rechazó un manifiesto obligatorio. Si el registro de auditoría cambió a «éxito» con la respuesta 202, ahora contradice las propias pruebas del proveedor.

Usa estados que indiquen qué evidencia tienes. Por ejemplo:

1. `requested` significa que la acción del agente pasó la autorización y creó un elemento de trabajo.
2. `submitted` significa que al menos un intento saliente recibió una respuesta de aceptación o que un resultado ambiguo recuperable está pendiente de comprobación.
3. `confirmed` significa que un callback verificado o una respuesta de estado autenticada estableció el resultado previsto.
4. `failed` significa que una prueba autorizada estableció el fallo.
5. `unknown` significa que todavía no puedes establecer si el lado remoto actuó.

El estado `unknown` es necesario. A los equipos no les gusta porque hace menos atractivos los paneles. Yo prefiero eso a la duplicación silenciosa. Un tiempo de espera después de enviar un POST produce una entrega ambigua: el sistema remoto puede haberlo recibido y procesado, o quizá nunca lo vio. Reintentar sin un mecanismo de idempotencia puede crear dos operaciones remotas. Llamar «fallido» al primer intento fomenta exactamente ese error.

Un callback tardío tampoco gana automáticamente. Supón que un agente solicita una cancelación después del comando inicial y tu flujo de trabajo interno registra una cancelación válida. Un callback de finalización que llega después puede informar de lo que ocurrió remotamente antes de que la cancelación tuviera efecto. Consérvalo, verifícalo, relaciónalo y registra el conflicto. No permitas que un controlador genérico sobrescriba un estado terminal de cancelación solo porque «completado» ocupa un lugar superior en el enum de alguien.

Escribe una regla de transición para cada tipo de callback. Una aprobación de pago, una compilación finalizada, un evento de aprovisionamiento de usuario y una confirmación de eliminación no merecen las mismas transiciones. La regla debe indicar qué estados anteriores permiten la transición, qué pruebas necesita el controlador y si un operador debe resolver un conflicto.

## El receptor debe conservar la evidencia antes de analizarla

Tu receptor debe capturar la entrega original, verificarla y eliminar sus duplicados antes de ejecutar un efecto secundario. Analizar primero el JSON y guardar solo algunos campos destruye pruebas cuando después se descubre que el analizador, el esquema o el código de la aplicación tenían un error.

Al recibir el evento, registra lo siguiente en un almacén de eventos protegido:

- El ID de recepción y la marca de tiempo del servidor.
- El método de la solicitud, la ruta, los encabezados seleccionados y un resumen criptográfico del cuerpo original exacto.
- La identidad del remitente que esperabas y el esquema de verificación aplicado.
- El ID del evento del proveedor, si el payload lo incluye, además del tipo de evento analizado.
- La decisión: aceptado, duplicado, rechazado o puesto en cuarentena, junto con un código de motivo.

Conserva los payloads originales solo durante el periodo que justifiquen tus necesidades de investigación y cumplimiento. Un resumen suele bastar para demostrar que dos payloads coinciden. Si conservas un cuerpo, cífralo, restringe el acceso y evita copiarlo en los registros normales de la aplicación. Los webhooks suelen contener datos personales, metadatos de repositorios, direcciones y notas internas. Un almacén de auditoría que filtra el payload es un riesgo, no una prueba.

La verificación de la firma debe funcionar sobre el cuerpo exactamente como lo firmó el remitente. Una capa de middleware que analiza el JSON, le da otro formato y después verifica los bytes reformateados rechazará entregas legítimas o, peor aún, permitirá un tratamiento incoherente. Lee con atención la documentación de verificación del proveedor. Algunos esquemas firman `timestamp + "." + raw_body`; otros solo el cuerpo original; otros usan firmas asimétricas y claves públicas rotatorias.

Para un esquema HMAC genérico que firma únicamente el cuerpo original, este comando muestra el formato del resumen que deberías esperar de los bytes sin modificar:

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

Esto sirve para diagnosticar, no sustituye la cadena canónica exacta del proveedor. Si el proveedor incluye una marca de tiempo o un prefijo de versión, copiar el comando genérico dará un resultado incorrecto. Este error aparece a menudo porque los ingenieros verifican una aproximación cómoda en lugar del algoritmo documentado por el remitente.

La validez de la firma no detiene las repeticiones. Si el remitente proporciona una marca de tiempo firmada, rechaza las entregas fuera de una ventana estrecha después de tener en cuenta el desfase horario medido. Después registra los IDs de evento del proveedor en un almacén duradero de deduplicación antes de invocar el trabajo posterior. Si no puedes confiar en un ID de evento, deduplica mediante un resumen limitado al remitente y un periodo de retención apropiado, sabiendo que dos eventos idénticos legítimos podrían necesitar un tratamiento especial.

Devuelve una respuesta HTTP solo después de hacer duradera la decisión sobre la recepción. Si devuelves éxito primero y el proceso falla antes de escribir la deduplicación, el remitente puede reintentar y tu controlador puede procesar dos veces el mismo evento. El error se oculta en las pruebas de poco volumen y aparece precisamente durante las interrupciones en las que el tráfico de webhooks aumenta.

## Los reintentos muestran dónde tus registros son demasiado vagos

Los reintentos son un comportamiento normal, no un caso límite, y cada capa puede reintentar de forma independiente. Los agentes reintentan después de un tiempo de espera. Las bibliotecas HTTP reintentan cuando falla una conexión. Los proveedores de API reintentan los callbacks. Los consumidores de colas reintentan un controlador fallido. Un registro que reduce todo esto a «número de reintentos: 3» no ayuda a nadie.

Repasemos un fallo que he visto con distintas variantes. Un agente solicita crear un registro de acceso remoto. El cliente envía un POST y agota el tiempo de espera después de que los bytes salgan del equipo. El servicio remoto crea el registro y pone un callback en cola. El marco del agente reintenta porque ve un tiempo de espera. La segunda solicitud crea otro registro porque la capa de acciones generó una nueva referencia de idempotencia en cada intento. Llegan ambos callbacks. El receptor solo usa una dirección de correo para relacionarlos, decide que son duplicados y suprime el segundo. La página de auditoría muestra una solicitud completada. El servicio remoto ahora tiene dos registros de acceso.

Cada componente se comportó de una manera razonable. El sistema falló porque no conservó un comando lógico a través de los límites de los reintentos.

Corrige la secuencia de esta forma:

1. Genera el ID de acción y la referencia de idempotencia una sola vez, antes del primer intento saliente.
2. Registra cada intento por separado, incluidos los tiempos de espera y los errores de transporte.
3. Ante una situación ambigua, consulta al proveedor por la referencia de idempotencia o la referencia del cliente antes de emitir otro comando.
4. Acepta cada callback autenticado como una recepción y deduplica únicamente el ID del evento del proveedor, no el objeto remoto.
5. Compara el número esperado de objetos remotos con la acción registrada antes de declarar completo el flujo de trabajo.

La primera comprobación de idempotencia pertenece al remitente y la segunda al receptor. Resuelven problemas diferentes. La idempotencia del remitente evita comandos remotos duplicados. La deduplicación del receptor evita procesar varias veces un mismo evento remoto. Los equipos suelen instalar una y asumir que también tienen la otra.

No uses la hora de llegada como orden de la verdad empresarial. Los proveedores pueden entregar eventos tarde o fuera de orden, y tu propia cola puede retrasar el procesamiento. Guarda al menos tres momentos: cuándo tu servicio de acciones creó la acción, cuándo tu cliente HTTP envió el intento o recibió su respuesta y cuándo tu receptor aceptó el callback. Conserva por separado la hora del evento declarada por el remitente. El reloj del remitente es una prueba de ese remitente, no tu reloj.

## La autorización debe sobrevivir al límite asíncrono

La aprobación humana de una acción del agente debe estar vinculada a la acción misma, no al callback que llegue después. Un callback contiene información sobre un trabajo remoto. Nunca debe adquirir silenciosamente autoridad para activar una nueva operación privilegiada solo porque comparte un campo de correlación con una solicitud aprobada.

Esto importa cuando los callbacks pueden contener URL, nombres de objetos, metadatos controlados por usuarios o instrucciones que sigue un controlador interno. Un diseño defectuoso habitual recibe un evento de «trabajo completado» y permite que un trabajador de automatización genérico obtenga una URL de resultados o ejecute un comando posterior con credenciales amplias. La aprobación original del agente cubría el envío de un trabajo, no un conjunto abierto de acciones incluidas en un evento.

Registra la acción autorizada de forma concreta: sesión del actor, endpoint solicitado o plantilla de comando SSH, alcance del objetivo, identidad de la credencial, resultado de la aprobación y hora de aprobación. Para cada llamada saliente, apunta a ese registro de autorización. Para cada callback, apunta a la acción solo después de verificarlo y correlacionarlo. La dirección importa. Una solicitud entrante no debe buscar en tu base de datos cualquier aprobación previa conveniente y tomarla prestada.

Sallyport mantiene las credenciales del agente fuera del proceso del agente y registra tanto las ejecuciones de los agentes como las llamadas individuales, lo que facilita conservar esta parte saliente de la evidencia. El receptor del callback sigue necesitando sus propios registros de recepción y flujo de trabajo, porque un diario de acciones HTTP no puede saber si un sistema remoto envió después un evento válido.

Usa credenciales separadas para ambas direcciones. La credencial que autoriza tu llamada saliente a la API normalmente no debe verificar firmas entrantes, y el secreto de verificación entrante no debe autorizar a un controlador de callbacks a llamar a API externas arbitrarias. Separar la custodia limita el daño cuando una ruta del receptor, una dependencia o un destino de registros falla.

## La evidencia de manipulación debe cubrir las relaciones, no solo las llamadas

Un registro de llamadas salientes que solo permite añadir datos resulta útil, pero no demuestra las decisiones de correlación tomadas después. Un operador o un error de la aplicación puede asociar el callback equivocado con la acción equivocada sin cambiar ninguno de los registros HTTP originales.

Convierte la correlación en un evento de auditoría de primer nivel. El evento debe incluir el ID de acción, el ID de recepción, la base de la relación, el actor o proceso que tomó la decisión y un resumen de los campos utilizados. Usa bases explícitas como `remote_operation_id_exact`, `client_reference_exact`, `authenticated_status_lookup` o `manual_review`. No escribas «coincide» y dejes que el investigador tenga que adivinar.

Un registro encadenado mediante hashes puede demostrar que los registros no cambiaron después de crearse, siempre que protejas la ruta de escritura y conserves puntos de control. No puede demostrar que la aplicación tomó una decisión correcta en ese momento. Es sano declarar esa limitación con claridad. La evidencia de manipulación te da un relato estable de lo que registró tu sistema, pero no convierte una correlación débil en un hecho.

El registro de auditoría cifrado y encadenado mediante hashes de Sallyport se puede comprobar sin conexión con `sp audit verify`, incluso sin una clave del almacén. Usa este tipo de verificación para los registros de acciones y conserva después una referencia inmutable comparable desde tu almacén de flujos de trabajo hacia los identificadores de acción y llamada relevantes.

En los flujos de trabajo de alto impacto, añade un proceso de reconciliación que compare tres grupos: las acciones enviadas, las operaciones remotas conocidas por el proveedor y los callbacks aceptados por tu receptor. El proceso debe crear un registro de excepción para cada elemento sin pareja, en lugar de cerrarlo automáticamente. La falta de un callback puede indicar una interrupción del proveedor, un endpoint incorrecto, un fallo en la rotación de firmas o un error del flujo de trabajo. Necesitas pruebas antes que optimismo.

## La observabilidad debe permitir repetir la decisión

Un investigador debe poder empezar desde cualquier identificador y reconstruir el flujo de trabajo sin acceso privilegiado a los prompts de los agentes ni a los secretos de las API. Diseña las rutas de búsqueda antes de publicar la integración.

Si se parte de un ID de acción, el registro debe mostrar la sesión del agente, la autorización, la etiqueta de la credencial, la forma de la solicitud después de ocultar datos, todos los intentos, las referencias remotas, las recepciones relacionadas y el estado terminal del flujo de trabajo. Si se parte de un ID de evento del proveedor, debe mostrar cada entrega de ese evento, los resultados de verificación, el resultado de la deduplicación, la operación relacionada y los cambios de estado. Si se parte de un objeto empresarial interno, debe mostrar la evidencia exacta que lo asoció con una acción del agente.

Usa campos estructurados, no una única cadena narrativa. Un contrato de evento útil puede copiarse en una revisión de esquema o en una canalización de registros:

```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"}
}
```

El resumen del cuerpo, el resultado de la verificación y la base de la correlación hacen mucho más que un `status: success` impreciso. Permiten comprobar las afirmaciones. Si un proveedor cuestiona un callback, compara el resumen conservado. Si un ingeniero cuestiona una relación, revisa su base. Si un duplicado provocó efectos secundarios, comprueba si el receptor escribió el registro de deduplicación antes de enviar el trabajo.

Evita registrar encabezados de autorización, tokens bearer, claves privadas, secretos de firma o URL completas que contengan credenciales. Oculta los valores de consulta cuando contengan datos sensibles, pero conserva suficiente identidad de la solicitud para distinguir dos objetivos. He visto equipos ocultar una URL hasta volverla inútil y luego no poder determinar si un agente contactó con producción o con un endpoint de prueba. Guarda un host normalizado, una plantilla de ruta, el método y un identificador de objetivo cuidadosamente limitado.

## Construye el rastreo antes de que los agentes hagan llamadas asíncronas

Debes definir los identificadores, las reglas de recepción y las transiciones de estado antes de dar a un agente una acción que inicie trabajo asíncrono. Añadirlos después de una disputa es costoso porque la evidencia que falta nunca existió.

Realiza un simulacro de fallo deliberado. Envía una acción de prueba inofensiva, fuerza al cliente a agotar el tiempo de espera después de la transmisión si tu entorno lo permite, reenvía el mismo callback, envía un callback con una firma no válida y entrega un callback válido después de que el flujo de trabajo entre en un estado terminal. Comprueba que el registro de auditoría explique cada resultado sin que una persona tenga que completar los huecos de memoria.

Si tu sistema no puede responder «¿qué acción autorizada provocó este callback, con qué evidencia exacta y qué hicimos con él?», todavía no tiene una historia de auditoría para los webhooks. Tiene dos conjuntos de registros que casualmente comparten un reloj.
