8 min de lectura

Los errores del bróker de secretos exigen pruebas

Diseñe los errores del bróker de secretos con pruebas de ejecución para separar reintentos previos seguros de resultados remotos desconocidos.

Los errores del bróker de secretos exigen pruebas

Un bróker de secretos debe informar de lo que demuestran sus registros, no de lo que sugiere una excepción. Si no puede demostrar que la operación remota quedó sin intentar, no debe describir la llamada como un fallo previo. En cuanto los bytes de una petición o una solicitud exec de SSH puedan haber llegado al otro extremo, el resultado puede ser desconocido aunque el error local diga "timeout" o "connection reset".

Esa diferencia decide si un agente genera un segundo pago, rota dos veces una credencial, vuelve a desplegar la misma versión o repite con seguridad un trabajo que nunca salió del equipo. Un contrato de errores útil debe contener dos hechos independientes: dónde se detuvo el bróker y qué sabe sobre la ejecución remota. Un solo campo llamado "transient" no puede expresar ambos.

El diseño que sigue se aplica a llamadas HTTP y comandos SSH. También presupone que el bróker mantiene un registro de acciones duradero. El estado que solo vive en memoria puede mejorar un mensaje de error, pero no puede justificar un reintento después de que el bróker o el llamador se reinicien.

Una causa no es un resultado

Cada fallo necesita una fase, un resultado y una disposición de reintento. La fase indica al operador dónde investigar. El resultado indica al llamador si el sistema remoto puede haber cambiado de estado. La disposición indica a la automatización qué puede hacer en ese momento, según las pruebas registradas y la semántica de la operación.

Use cinco fases públicas:

  • validation: la acción se rechazó antes de que el bróker seleccionara o usara un secreto.
  • credential_injection: el bróker no pudo obtener, autorizar o adjuntar la credencial.
  • connection_setup: la resolución de nombres, el enrutamiento, TCP, TLS, el transporte SSH, la verificación del host o la autenticación remota fallaron antes del envío.
  • remote_execution: el bróker envió la operación y espera el resultado remoto o ya lo recibió.
  • result_delivery: el bróker registró el resultado remoto, pero no pudo entregarlo intacto al llamador.

Estas fases sirven para diagnosticar, no forman una política de reintentos. Un error de connection_setup puede demostrar que no se envió ninguna petición de aplicación, mientras que una conexión reutilizada que se rompe puede dejar al bróker sin saber si el otro extremo recibió la escritura. Un error de result_delivery puede acompañar a un éxito remoto conocido. Tratar ambos como un error de red genérico destruye el dato que necesita el llamador.

Use cuatro estados de resultado:

  • not_attempted: las pruebas duraderas muestran que la operación remota no cruzó el límite de envío.
  • rejected: el sistema remoto devolvió un rechazo completo y autoritativo sin afirmar que hubo éxito.
  • committed: el bróker tiene un resultado completo y autoritativo de la operación.
  • unknown: el envío puede haber ocurrido, pero el bróker carece de un resultado completo que resuelva la operación.

committed no significa éxito. Un comando que termina con el estado 23 o una petición HTTP que recibe una respuesta 500 completa tienen un resultado conocido. La aplicación remota se ejecutó hasta el punto de poder contestar. Llamar a eso "fallo de ejecución" invita a crear un duplicado.

La disposición de reintento debe ser igual de explícita: never, after_correction, backoff, same_idempotency_key, reconcile o fetch_result. El bróker la calcula a partir de las pruebas, la semántica del método, las garantías remotas y el estado de su diario de resultados. Los llamadores nunca deben deducirla de un texto de error.

No fusione rejected y committed en un único valor known dentro del contrato público. Un rechazo puede permitir una petición corregida, mientras que un resultado confirmado obliga al llamador a consumirlo o conciliarlo. Ambos ofrecen certeza sobre el intento, pero conducen a flujos de trabajo distintos.

La aceptación asíncrona necesita una prueba adicional, no otro valor de resultado. Una respuesta HTTP 202 completa es un resultado confirmado para el envío, no una prueba de que el trabajo en cola terminó. Registre el identificador del trabajo y el recurso de estado entregados por el servicio, y siga el trabajo como una operación aparte. Repetir el envío porque el trabajo sigue pendiente puede ponerlo dos veces en la cola.

Esta separación aclara una distinción que muchos SDK confunden. La causa del error responde "¿qué se rompió aquí?". El resultado remoto responde "¿qué puede haber ocurrido ya?". Un motor de reintentos que solo lee la causa no es seguro.

La validación y la inyección fallan antes del envío

Los errores de validación son verdaderos fallos previos solo mientras el bróker no haya abierto un canal capaz de transportar la acción. Rechace destinos mal formados, métodos no admitidos, campos ausentes, cargas demasiado grandes, referencias de credenciales desconocidas y cambios de cabeceras prohibidos antes de resolver un host o acceder a un secreto. Registre phase=validation, outcome=not_attempted y, por lo general, retry=after_correction.

Un reintento automático no arregla un fallo de validación determinista. Repetir la misma carga no válida consume capacidad y puede ocultar un bucle del agente. Devuelva un código estable, como INVALID_TARGET, UNSUPPORTED_ACTION o PAYLOAD_LIMIT, junto con la ruta del campo que el llamador puede corregir. No devuelva la referencia rechazada si contiene información sensible en su nombre.

La inyección de credenciales sigue siendo una operación previa cuando el bróker falla antes de liberar cualquier byte de una petición remota. Una bóveda bloqueada, una aprobación denegada, una credencial ausente, un modo de inyección no admitido y un fallo local al descifrar una clave pertenecen a esta fase. El resultado sigue siendo not_attempted, pero el consejo de reintento cambia. Una bóveda bloqueada puede permitir reintentar tras una acción del usuario; una aprobación denegada debe ser normalmente never para esa invocación; un secreto ausente requiere una corrección.

Mantenga el material de la credencial fuera del error y del registro de la acción. Registre el identificador de la credencial o una etiqueta de versión no secreta, el modo de inyección y la decisión que detuvo la llamada. Guardar una cabecera Authorization ya preparada para demostrar que hubo inyección anula la función del bróker.

Hay un punto de corte sutil. Si el bróker construye una petición HTTP completa con la credencial en un búfer privado y falla antes de escribirla, la acción remota sigue sin intentarse. Si entrega ese búfer a una API de transporte y la API devuelve una escritura parcial o ambigua, la inyección terminó y el envío puede haber comenzado. Clasifique el fallo por el último límite probado, no por la función cuya pila capturó la excepción.

La fase previa también necesita una instantánea de la configuración. Si la validación usa una definición de ruta y el envío lee después una definición modificada, las pruebas ya no describen la acción ejecutada. Vincule el destino normalizado, la versión de la credencial, el modo de inyección permitido y la huella de la petición a la invocación antes de autorizar. Si cambia algún valor vinculado, cree una invocación nueva en vez de modificar el registro anterior.

Esto importa durante una aprobación humana. Una tarjeta de aprobación puede permanecer abierta mientras un agente o una recarga de configuración cambia el cuerpo, el host o el secreto seleccionado. El bróker debe aprobar la huella que va a enviar y verificarla de nuevo justo antes de escribir. Una diferencia es validation/not_attempted, no un motivo para enviar la nueva petición con una aprobación antigua.

La configuración de conexión necesita un límite preciso

Una conexión nueva que falla antes de que exista un canal de aplicación demuestra normalmente not_attempted. Los fallos de DNS y ruta, una conexión TCP rechazada, el rechazo de un certificado TLS o de una clave de host SSH, y un fallo de autenticación de usuario SSH ocurren antes de que pueda ejecutarse una petición HTTP o un comando SSH. Registre la etapa exacta completada para que el llamador distinga un nombre de host erróneo de unas credenciales rechazadas sin ver ningún secreto.

La frase "falló la conexión" es demasiado amplia para un cliente HTTP con conexiones agrupadas. Cuando el bróker toma una conexión existente, la configuración ya terminó. Una escritura puede fallar porque el otro extremo cerró un socket inactivo. El sistema operativo puede informar de una tubería rota después de que algunos bytes llegaran al otro extremo, o después de que este recibiera toda la petición pero antes de que el cliente observara el cierre. Eso pertenece a remote_execution con outcome=unknown, salvo que el transporte aporte una prueba más fuerte.

No use "la llamada de escritura local confirmó cero bytes" como prueba de que el sistema remoto no recibió nada. Una API con búfer puede aceptar bytes localmente antes de un fallo posterior, y una escritura fallida dice poco sobre lo que el otro extremo ya leyó. El límite útil de envío está en la primera entrega a un transporte capaz de llevar datos de aplicación. Al cruzarlo, el resultado predeterminado cambia a unknown.

La configuración de conexión también termina en puntos distintos para HTTP y SSH. En HTTPS, termine DNS, TCP, TLS, la verificación del certificado y cualquier túnel de proxy antes del envío. En SSH, termine la negociación del transporte, la verificación del host, la autenticación del usuario, la creación del canal de sesión y toda preparación necesaria del entorno. Nada de eso demuestra si un comando posterior empezó, pero un fallo dentro de esas etapas puede demostrar que nunca se solicitó.

Una redirección crea un segundo límite de envío. Una respuesta 307 o 308 completa resuelve el primer intercambio HTTP, pero seguirla crea una petición nueva a otro destino. Vuelva a validar el destino, el alcance de la credencial y el método antes de esa petición. Nunca reenvíe una credencial de autorización entre orígenes solo porque una biblioteca sigue redirecciones automáticamente.

Un proxy HTTP añade otro observador, pero no elimina la incertidumbre. Un túnel correcto solo demuestra que el proxy abrió una ruta. Un proxy de reenvío puede devolver un error completo sobre su propio intento, y RFC 9209 puede describir dónde falló el reenvío, pero el resultado en el origen aún puede ser desconocido. Guarde qué salto produjo la prueba y no presente la certeza del intermediario sobre su respuesta como certeza sobre los efectos en el origen.

Un bróker puede reintentar internamente la configuración de la conexión cuando cada intento tiene su propio registro y ninguno cruzó el límite de envío. Debe limitar los intentos y mostrarlos en un solo registro de acción:

{"attempts":[{"n":1,"stage":"tcp_connect","outcome":"not_attempted","code":"ECONNREFUSED"},{"n":2,"stage":"tls_handshake","outcome":"not_attempted","code":"CERT_EXPIRED"}]}

El error final no debe borrar las pruebas anteriores. Debe indicar que la operación quedó sin intentar en ambos casos y que hace falta una corrección.

El envío cambia la carga de la prueba

El momento del envío debe ser una transición de estado explícita y duradera. Antes de entregar datos al transporte por primera vez, añada dispatch_started al registro de la acción y fuerce su persistencia conforme a la promesa de durabilidad del sistema. Si el proceso muere después de escribir pero antes de registrar esa transición, un reinicio puede llamar a la operación "no intentada" de forma incorrecta.

El registro previo estricto cuesta latencia, por lo que resulta tentador registrar después de enviar. La recomendación es popular porque la ruta normal gana velocidad y el código parece más sencillo. Es incorrecta para acciones no idempotentes. El fallo poco habitual cae justo en el hueco donde el bróker debe elegir entre perder el trabajo y duplicarlo.

El estado escrito por adelantado no demuestra que el sistema remoto recibiera la petición. Desplaza la incertidumbre de forma deliberada hacia el lado seguro. Después de dispatch_started, el resultado empieza como unknown. Una respuesta autoritativa posterior puede convertirlo en rejected o committed. El bróker nunca vuelve a not_attempted.

En HTTP, el envío empieza antes de que el primer byte de la petición entre en la conexión. Registre si el bróker envió las cabeceras, envió el cuerpo completo, recibió las cabeceras de respuesta y recibió un cuerpo de respuesta completo. Estos marcadores ayudan a diagnosticar, pero request_body_sent=true no demuestra que la aplicación procesara la petición. Del mismo modo, request_body_sent=false no demuestra que la aplicación no hiciera nada; un servidor puede rechazar o actuar según las cabeceras antes de leer el cuerpo entero.

En SSH, el envío empieza antes de que la solicitud de canal exec entre en el transporte autenticado. Use want reply=true. RFC 4254 dice que el servidor contesta con éxito o fallo del canal, pero el éxito solo significa que aceptó la solicitud para iniciar el comando. No significa que el comando terminara ni que sea seguro repetir sus efectos.

Una cancelación posterior al envío no es un fallo previo. Si el llamador agota su tiempo y cierra el canal, el proceso remoto puede seguir ejecutándose. Indique CALLER_CANCELLED como causa local y conserve outcome=unknown hasta que un resultado remoto registrado lo resuelva. La cancelación describe el interés del llamador, no el estado remoto.

Las peticiones por lotes necesitan un resultado por elemento. Si el bróker envía cinco cambios en una petición HTTP y recibe una respuesta completa que solo resuelve cuatro, no puede asignar de forma segura un valor de reintento al lote entero. Registre el resultado del transporte padre y cinco resultados hijos. Reintente solo el elemento cuyas pruebas y contrato remoto lo permitan, o concilie todo el lote si la API remota aplica los cambios de forma atómica.

La misma regla se aplica a un script de shell enviado por SSH. Un estado de salida cubre el proceso del script, no necesariamente cada efecto que intentó producir. Si los llamadores necesitan decisiones de reintento por acción, dé a cada operación su propio identificador remoto y registro de resultado en vez de deducir el progreso de stdout.

Un resultado remoto completo resuelve la ejecución

Ponga HTTP detrás de una puerta
Los agentes envían llamadas bearer, basic o de cabecera personalizada mediante Sallyport sin guardar credenciales.

Una respuesta autoritativa con un enmarcado completo convierte la incertidumbre en un resultado conocido. Para HTTP, registre el estado final, las cabeceras no secretas seleccionadas, el cuerpo completo o su resumen, y la finalización del enmarcado. Para SSH, registre la aceptación del comando, el estado de finalización de stdout y stderr, el estado de salida o la señal cuando existan, y el cierre del canal.

RFC 9112 exige que un cliente registre una respuesta HTTP como incompleta cuando la conexión se cierra antes de tiempo o falla la decodificación por bloques. Un bróker de secretos debe aplicar una regla más estricta: nunca mostrar un cuerpo parcial como resultado remoto completo, aunque los primeros bytes contengan JSON plausible. Devuelva el contenido parcial solo en un campo de diagnóstico marcado con claridad, o descártelo si puede contener datos sensibles.

El estado HTTP por sí solo no define si es seguro repetir la acción de la aplicación. Un 401 completo demuestra que el servidor rechazó esas credenciales para esa petición, por lo que el bróker puede marcar el resultado como rejected; repetir con las mismas credenciales no sirve. Un 429 o 503 completo puede permitir backoff cuando el método es seguro de repetir y la respuesta da un tiempo adecuado. Un 500 completo es conocido, pero la aplicación puede haber cambiado de estado antes de generarlo. No convierta todos los 5xx en permiso para repetir un POST.

RFC 9110 define la idempotencia según el efecto previsto de varias peticiones idénticas y permite el reintento automático de métodos idempotentes después de un fallo de comunicación. La condición útil es que se sepa que el método es idempotente. Los nombres de método son una prueba, no magia. Un endpoint GET mal diseñado que activa un despliegue sigue siendo inseguro pese al token del método; un PUT bien implementado puede repetirse aunque cambie el estado.

Las respuestas HTTP informativas no resuelven la acción. 100 Continue permite al cliente enviar un cuerpo de petición, pero no dice nada sobre el resultado final de la aplicación. Las demás respuestas 1xx también dejan la invocación en curso. Solo una respuesta final completa, o un recibo de aplicación más fuerte cuyo contrato entienda el bróker, puede sacar el resultado de unknown.

La integridad y la autenticidad de la respuesta deben ir juntas. Una respuesta con enmarcado perfecto procedente de una identidad TLS incorrecta, un host SSH que no es de confianza o un proxy inesperado no es una prueba autoritativa sobre el destino previsto. La verificación de identidad suele terminar durante la conexión, pero las sesiones reanudadas y los grupos de conexiones aún deben vincular la identidad verificada del otro extremo al registro de la acción.

RFC 9209 define http_response_incomplete para un intermediario que recibió una respuesta parcial del siguiente salto. El estado 502 que recomienda ayuda a la compatibilidad HTTP, pero un 502 aislado pierde las pruebas del resultado. Mantenga el outcome=unknown estructurado del bróker junto a cualquier estado mapeado.

SSH tiene una trampa comparable. RFC 4254 recomienda que el servidor devuelva exit-status, pero no lo exige. Si el canal se cierra después de stdout sin estado de salida ni señal, el bróker sabe que el flujo terminó, pero no sabe si el comando tuvo éxito. Devuelva REMOTE_RESULT_INCOMPLETE y elija unknown, salvo que el contrato de la acción defina otro marcador de finalización autoritativo.

La entrega del resultado no debe repetir la ejecución

La entrega comienza solo después de que el bróker almacene de forma duradera un resultado remoto resuelto. Si falla la serialización para el llamador, se cierra la tubería MCP o termina el proceso llamador, la operación remota no vuelve a ser desconocida. Informe de phase=result_delivery, conserve outcome=committed o rejected, y establezca retry=fetch_result.

Esta fase necesita un identificador de invocación que permita recuperar el resultado guardado. Repetir el envío de una acción no equivale a recuperar su resultado. Separe esas operaciones para impedir que una biblioteca cliente genérica convierta por accidente una tubería de respuesta rota en otra llamada remota.

El orden de las escrituras importa:

  1. Termine y valide la respuesta remota.
  2. Añada el resultado resuelto y su resumen al registro duradero.
  3. Confirme el resultado recuperable bajo el identificador de invocación.
  4. Entregue el resultado al llamador.

Si falla el paso 4, los pasos 2 y 3 demuestran lo ocurrido. Si el bróker entrega primero y escribe el diario después, una caída puede dejar al llamador con un éxito mientras el registro de auditoría dice que el resultado es desconocido. Es un defecto de auditoría aunque no cause un reintento inmediato.

Los resultados grandes o transmitidos por flujo necesitan la misma regla. Almacene fragmentos con números de secuencia y un marcador final de integridad. Un llamador puede reanudar la entrega desde el último fragmento verificado, pero el bróker no debe llamar completo al resultado hasta obtener el terminador, la longitud declarada o el cierre específico del protocolo que lo demuestre.

La confirmación del llamador sirve para la retención, no para el resultado remoto. Marque RESULT_DELIVERED solo después de que el protocolo orientado al llamador confirme una entrega completa. Si no existe confirmación, mantenga disponible el resultado hasta que venza la política de retención y trate las lecturas repetidas como lecturas. Nunca vuelva a ejecutar la acción para reconstruir un resultado que el bróker decidió no conservar.

El almacenamiento puede fallar después de que termine la operación remota. Si el bróker tiene la respuesta completa en memoria, pero no puede confirmarla, sabe más de lo que expresa un simple unknown, aunque las pruebas no sobrevivirán a una caída. Devuelva phase=result_delivery, incluya outcome=committed solo si el contrato de durabilidad permite sostener esa afirmación con el registro actual, y exija atención inmediata del operador. La solución correcta es reservar capacidad y probar los fallos de almacenamiento, no reintentar la acción remota.

La idempotencia es un contrato remoto

Revoque una ejecución dudosa
Sallyport revoca una sesión activa y mantiene registrado su historial de llamadas.

Una clave de idempotencia permite reintentar un resultado desconocido solo cuando el servicio remoto promete vincularla a una operación lógica. Generar un UUID en el bróker y registrarlo no aporta nada por sí mismo. El endpoint remoto debe aceptar la clave, comparar la huella de la petición, conservar el primer resultado resuelto durante un tiempo suficiente y devolverlo ante una repetición.

El bróker debe almacenar cuatro hechos antes del envío: la clave de idempotencia, la huella de la petición, el alcance remoto y la caducidad o información de retención cuando el servicio la publique. En un reintento debe reutilizar la misma clave y una huella idéntica. Reutilizar una clave con un cuerpo cambiado debe fallar localmente con IDEMPOTENCY_MISMATCH.

No añada en silencio una cabecera de idempotencia a endpoints que no declaran su semántica. Algunos servicios ignoran las cabeceras desconocidas. Otros limitan las claves por cuenta o ruta. Una política de reintentos necesita conocimiento configurado y revisado del contrato remoto, no confianza en el nombre de una cabecera.

Las ventanas de retención forman parte del contrato. Si un servicio olvida las claves después de un día, un reintento posterior puede crear un efecto nuevo aunque parezca idéntico al bróker. Guarde la caducidad segura más temprana, detenga los reintentos automáticos antes de alcanzarla y concilie después. Cuando el servicio no publique una garantía de retención, trate la clave como útil solo dentro de una ventana conservadora configurada.

La concurrencia puede derrotar un diseño correcto en un solo hilo. Dos trabajadores pueden leer el mismo registro desconocido y decidir reintentar con la misma clave. Un buen contrato remoto de deduplicación debería agruparlos, pero el bróker debe adquirir además un arrendamiento sobre la invocación, registrar la generación de reintento y permitir un solo intento activo. Eso reduce la carga y mantiene el diario comprensible.

Solo hay tres rutas seguras desde unknown:

  • Repetir una operación cuya semántica se conoce como idempotente.
  • Repetir con la misma clave bajo un contrato remoto de deduplicación verificado.
  • Conciliar consultando el estado remoto con un identificador estable y decidir después si hace falta otra acción.

Todo lo demás se detiene para una revisión. Puede parecer conservador mientras un agente espera, pero los efectos duplicados cuestan más que una pausa visible.

Las peticiones HTTP condicionales pueden reforzar el contrato. If-Match con una etiqueta de entidad conocida puede hacer que una actualización falle si el recurso cambió, mientras que If-None-Match: * puede impedir la creación de un segundo recurso en el mismo destino. No resuelven todos los duplicados porque el modelo de recursos del endpoint sigue importando, pero aportan pruebas impuestas por el servidor en vez de conjeturas del cliente.

Los comandos SSH rara vez ofrecen una clave de idempotencia en el protocolo. Incorpore la repetibilidad al contrato de aplicación del comando: cree un despliegue bajo un identificador de versión único, escriba con una comparación atómica o ejecute una consulta que confirme el estado previsto. Nunca dé por seguro un comando de shell porque no devolvió salida.

El sobre de error debe incluir pruebas

Bloquee envíos desde la bóveda cerrada
La puerta de la bóveda rechaza toda acción mientras las credenciales cifradas siguen bloqueadas.

Un llamador necesita un contrato estable para máquinas y un mensaje humano breve. Mantenga las excepciones de la biblioteca de transporte en un campo de diagnóstico interno, porque sus nombres cambian entre plataformas y exponen detalles de implementación. El sobre público debe tener este aspecto:

{"invocation_id":"act_01J...","error":{"code":"REMOTE_OUTCOME_UNKNOWN","phase":"remote_execution","outcome":"unknown","retry":"same_idempotency_key","message":"Connection closed before a complete response was recorded."},"evidence":{"dispatch_started":true,"request_complete":true,"response_headers_received":false,"response_complete":false,"idempotency":{"key":"req_01J...","scope":"payments.create","fingerprint":"sha256:8b1...","remote_contract":"configured"}}}

Mantenga code, phase, outcome y retry como enumeraciones cerradas. Añada nuevos campos de pruebas sin cambiar su significado. Los llamadores pueden bifurcar según las enumeraciones y mostrar message a una persona. No deben analizar el texto del mensaje.

Las pruebas deben decir cómo lo sabe el bróker, no limitarse a repetir la conclusión. Entre los campos útiles están el número de intento, el identificador de conexión, la secuencia del diario de envío, la huella de la petición, el marcador de finalización del protocolo, el identificador remoto, el resumen de respuesta, el estado de salida y el identificador del registro de resultado. Omita secretos, cabeceras completas de autorización, claves privadas y cuerpos remotos sin filtrar.

Conserve las transiciones como eventos de solo anexado y proyecte después el estado actual:

ACTION_ACCEPTED
PREFLIGHT_VALIDATED
CREDENTIAL_AUTHORIZED
DISPATCH_STARTED
REQUEST_SENT
REMOTE_RESPONSE_STARTED
REMOTE_RESPONSE_COMPLETE
RESULT_COMMITTED
RESULT_DELIVERED

Una acción que termina después de PREFLIGHT_VALIDATED se puede demostrar como no intentada. Una acción que termina después de DISPATCH_STARTED y antes de una finalización autoritativa sigue siendo desconocida. Una acción con RESULT_COMMITTED sobrevive a una entrega fallida sin otra ejecución remota.

La proyección debe rechazar regresiones imposibles. unknown puede convertirse en rejected o committed cuando llegan pruebas tardías, pero committed no puede convertirse en not_attempted. Un segundo observador puede adjuntar un resultado de conciliación, pero no debe reescribir el intento original como si nunca hubiera habido envío.

Registre números de secuencia monótonos en lugar de depender del orden del reloj. Los relojes pueden cambiar y los eventos de componentes concurrentes pueden llegar tarde. Las marcas de tiempo ayudan a los operadores a correlacionar sistemas, pero la secuencia del diario define qué transición duradera ocurrió primero. Adjunte un origen y una secuencia local a las pruebas remotas importadas en vez de insertarlas en mitad del historial.

Las pruebas también necesitan declarar su fuente de confianza. transport_observed, remote_response, remote_query y operator_attested explican al código posterior por qué cambió un resultado. Un operador puede resolver legítimamente un intento desconocido tras comprobar el sistema remoto, pero ese dato no debe hacerse pasar por una respuesta recibida por el bróker en la conexión original.

La integridad de la auditoría y las pruebas del resultado resuelven problemas distintos. Una cadena de hashes puede demostrar que los eventos registrados no se alteraron después, pero no puede demostrar que el bróker registró todos los eventos ni que la aplicación remota respetó una petición. El registro de auditoría cifrado, encadenado por hashes y ciego a la escritura de Sallyport, junto con sus vistas separadas de sesiones y actividad, ofrece un lugar duradero para conservar estas transiciones; el resultado de la acción aún necesita el contrato de fase y resultado descrito aquí.

El código de reintento debe ser simple y comprobable

El motor de reintentos debe consumir la disposición ya derivada de las pruebas. Puede añadir límites de frecuencia y de intentos, pero no debe volver segura una disposición insegura porque una excepción parezca temporal.

decide(record, operation):
  if record.retry == "fetch_result":
    return FETCH(record.invocation_id)

  if record.outcome == "not_attempted":
    if record.retry == "backoff":
      return RETRY_NEW_ATTEMPT
    return STOP_FOR_CORRECTION

  if record.outcome == "unknown":
    if operation.idempotent:
      return RETRY_NEW_ATTEMPT
    if record.retry == "same_idempotency_key" and
       operation.fingerprint == record.fingerprint:
      return RETRY_SAME_KEY
    return RECONCILE

  if record.outcome == "rejected" and record.retry == "backoff":
    return RETRY_WHEN_ALLOWED

  return RETURN_RECORDED_RESULT

Pruebe transiciones, no clases de excepción. Inyecte un fallo antes del acceso a la credencial, durante TLS, antes de la primera escritura, después de escribir una petición completa, a mitad de las cabeceras de respuesta, a mitad de un cuerpo enmarcado, después de confirmar el resultado y durante la entrega al llamador. Mate el bróker entre cada par de transiciones duraderas y compruebe que la recuperación nunca afirme not_attempted después de DISPATCH_STARTED.

Añada comportamientos remotos adversos. Haga que un servidor aplique el efecto y cierre sin responder. Haga que devuelva 500 después de confirmar. Haga que respete una clave de idempotencia, que la ignore y que rechace una clave reutilizada con otra carga. Para SSH, cierre después de aceptar exec, omita exit-status y envíe un estado de salida antes de romper el flujo de salida. El resultado esperado debe seguir siempre las pruebas.

Las métricas deben contar la fase y el resultado por separado. Un aumento de connection_setup/not_attempted apunta a problemas de ruta, certificados o autenticación. Un aumento de remote_execution/unknown exige conciliación y puede revelar un problema de fiabilidad remota. Combinarlos como "fallos del bróker" oculta tanto la causa operativa como el riesgo de duplicación.

No permita que un SDK amable borre el contrato. Si debe lanzar excepciones, adjunte el sobre completo y deje el reintento automático como opción solo para backoff o same_idempotency_key. Un llamador debería tener que escribir código visiblemente inseguro para repetir una acción unknown/reconcile.

Las pruebas de recuperación deben incluir trabajadores en competencia y arrendamientos caducados. Pause un trabajador después de que adquiera un arrendamiento de reintento, deje que caduque e inicie otro. Cuando el primero continúe, su comprobación de generación debe detenerlo antes del envío. Sin ella, una clasificación de resultado perfecta aún puede crear duplicados concurrentes.

Trate los presupuestos de reintento como parte del registro de la acción, no como contadores locales del proceso. Los reinicios no deben restablecer el número de intentos ni la caducidad de la clave remota. Cuando termine el presupuesto, devuelva las últimas pruebas y exija conciliación; cambiar el código por un "máximo de reintentos superado" genérico descartaría el diagnóstico más seguro.

El estado más difícil debe seguir siendo incómodo. Cuando el registro dice que el envío comenzó y no llegó una finalización autoritativa, el bróker no conoce el resultado remoto. Conserve ese hecho, concílielo y niéguese a convertir la falta de pruebas en un permiso.

FAQ

¿Un timeout puede demostrar que una acción remota no se ejecutó?

Un timeout solo demuestra que venció un plazo. Es un fallo previo únicamente cuando los registros duraderos muestran que el envío nunca comenzó; después del envío, trate el resultado como desconocido hasta que otras pruebas lo resuelvan.

¿Debe un bróker reintentar todas las peticiones GET fallidas?

No. RFC 9110 define GET como idempotente por su semántica prevista, pero un endpoint mal diseñado aún puede provocar efectos. Reintente solo cuando lo respalden tanto el contrato del endpoint como las pruebas del bróker.

¿Es seguro reintentar una respuesta HTTP 500?

No basta con el código de estado. Un 500 completo es un resultado remoto conocido, pero la aplicación puede haber confirmado un cambio antes de generarlo, así que la semántica del método o la deduplicación remota deben justificar el reintento.

¿Una clave de idempotencia siempre evita acciones duplicadas?

No. El servicio remoto debe reconocer la clave, vincularla a la huella de la petición y devolver el resultado almacenado cuando se repita. Una clave que solo existe en el registro del bróker aporta correlación, no deduplicación.

¿Cuál es la respuesta más segura ante un resultado remoto desconocido?

Primero consulte el estado remoto con un identificador estable. Repita solo si la conciliación no muestra efectos, la operación es idempotente o un contrato remoto verificado acepta la misma clave y huella.

¿Por qué separar la configuración de conexión de la ejecución remota?

La separación indica si una petición de aplicación pudo llegar al otro extremo. Un fallo TLS en una conexión nueva puede demostrar que no hubo intento, mientras que un reinicio después de escribir en una conexión reutilizada puede dejar el resultado desconocido.

¿Cómo debe informar el bróker de un fallo al devolver el resultado?

Regístrelo como phase=result_delivery y conserve el resultado remoto resuelto. El llamador debe recuperar el resultado guardado mediante el identificador de invocación en vez de enviar otra vez la acción.

¿El cierre de un canal SSH cuenta como finalización correcta?

No por sí solo. RFC 4254 recomienda un estado de salida, pero no lo exige, por lo que un cierre sin estado, señal o marcador de aplicación puede dejar el resultado desconocido.

¿Qué pruebas debe contener un error del bróker de secretos?

Incluya el identificador de invocación, la fase, el resultado, la disposición de reintento, el estado de envío, la huella, los marcadores de finalización y el identificador del resultado cuando exista. Excluya credenciales y datos remotos sensibles sin filtrar.

¿Cómo puede un equipo comprobar que los reintentos son seguros?

Mate el bróker en cada transición duradera e inyecte fallos antes del envío, durante las escrituras y el enmarcado, después de confirmar el resultado y durante la entrega. Compruebe que ningún registro enviado vuelva a aparecer como no intentado.

Sallyport

Sallyport ejecuta llamadas de API y comandos SSH por tu agente de IA. Las claves se quedan en una bóveda local de tu Mac; tú apruebas cada ejecución y cada acción queda en un registro sellado.

© 2026 Sallyport · Código abierto bajo Apache-2.0 · Oleg Sotnikov