# ¿Qué demuestran las pruebas de salida forzada sobre una puerta de enlace de agentes?

Una puerta de enlace que guarda credenciales para un agente de IA debe seguir siendo segura cuando su proceso desaparece en el peor momento posible. Una prueba del flujo normal puede demostrar que aparece una aprobación, que una solicitud tiene éxito y que después existe un registro de auditoría. No puede demostrar si una credencial escapó durante una solicitud interrumpida o si el registro de auditoría miente sobre un trabajo que nunca terminó.

Las pruebas de salida forzada revelan las diferencias entre afirmaciones como «la solicitud comenzó», «la credencial se adjuntó», «el sistema remoto la recibió» y «el registro es duradero». Son hechos separados. Tratarlos como un solo evento es la forma en que los equipos terminan enviando una puerta de enlace de acciones que parece controlada hasta que un fallo convierte un reintento normal en un despliegue duplicado o en una llamada saliente imposible de rastrear.

En una puerta de enlace para macOS, distingue una salida normal de la muerte del proceso. Apple documenta la terminación normal de una aplicación como una ruta del ciclo de vida en la que la aplicación puede guardar el estado y ejecutar el tratamiento de terminación. Una terminación forzada puede no concederle ese tiempo. Apple también identifica SIGKILL como un tipo de terminación que puede producirse cuando un usuario fuerza la salida de una aplicación. Tu plan de pruebas debe asumir que los controladores de limpieza, las escrituras aplazadas y la telemetría de mejor esfuerzo no se ejecutan.

La pregunta útil no es «¿la aplicación se reinicia?». La pregunta útil es: en cada punto de interrupción, ¿puedes decir exactamente qué efectos secundarios son posibles, cuáles son imposibles y qué pruebas sobreviven?

## Una salida forzada debe atravesar la limpieza

Una prueba de salida forzada solo es válida cuando elimina la oportunidad de la puerta de enlace de ordenar las cosas. Si la prueba llama a una función de apagado educada, espera a que se vacíen las colas, vacía los registros y después sale, has probado una terminación ordenada. Eso importa, pero no representa el fallo que mantiene despiertos a los ingenieros de seguridad.

Usa dos modos de terminación y etiquétalos correctamente:

- Una solicitud de terminación normal prueba la gestión de cancelaciones, el cierre ordenado de archivos y la forma en que la interfaz informa de una sesión interrumpida.
- Una terminación forzada prueba el estado de la memoria, los búferes de archivos, los sockets abiertos y el trabajo en curso cuando no se ejecuta ninguna limpieza de la aplicación.

En macOS, un arnés de pruebas puede usar `kill -TERM` para el primer caso y `kill -KILL` para el segundo. `SIGTERM` da al proceso la oportunidad de gestionar la terminación. `SIGKILL` no. No llames «salida forzada» a ambos casos en la tabla de resultados, porque responden preguntas distintas.

```sh
# Find the gateway process you started for the test.
pgrep -fl Sallyport

# Graceful termination case.
kill -TERM 48192

# Abrupt termination case.
kill -KILL 48192

# Confirm the process is gone.
ps -p 48192
# PID TTY           TIME CMD
# 48192 ttys003    0:00.42 gateway-test
# After SIGKILL, ps should print no process row.
```

El ID del proceso no es el identificador de la prueba. Asigna a cada ejecución un ID de ejecución, un ID de acción y un ID de solicitud remota. Incluye esos ID en los registros del accesorio y en los de la puerta de enlace. Sin identificadores compartidos, acabarás comparando marcas de tiempo y adivinando si un POST remoto pertenecía a la ejecución que mataste.

No pruebes contra un endpoint de producción, aunque creas que la llamada es inofensiva. El propósito de este trabajo es crear resultados ambiguos deliberadamente. Construye un receptor que pueda pausar antes de aceptar un cuerpo, después de leer los encabezados, después de registrar el cuerpo o antes de liberar su respuesta. Una API real de terceros no ofrecerá de forma fiable esos límites y puede aplicar reintentos o almacenamiento en caché que no pediste.

## La solicitud necesita puntos de interrupción explícitos

No puedes matar un proceso «durante la inyección de credenciales» si la implementación no define qué significa eso. Añade puntos de espera observables alrededor de la cadena de acciones. Son controles de prueba, no un lenguaje de políticas ni un mecanismo de autorización de producción.

Para una acción HTTP, usa una secuencia como esta:

1. La puerta de enlace acepta una descripción de acción autorizada y asigna un ID de acción.
2. Escribe un registro de intención, si el diseño lo exige antes del trabajo de red.
3. Resuelve la credencial dentro del proceso protegido.
4. Construye la solicitud saliente e inyecta la credencial.
5. Abre la conexión y escribe la solicitud.
6. Recibe suficiente respuesta para clasificar el resultado remoto.
7. Escribe un registro de finalización o de resultado desconocido y después devuelve un resultado al agente.

Instala una pausa en el límite posterior a cada operación numerada. La pausa debe poder observarse desde otro proceso. Un socket local solo para pruebas, una tubería con nombre o un descriptor de archivo controlado por el arnés sirven. Una llamada a `sleep` no funciona bien, porque las variaciones de tiempo convierten una prueba de límites en una carrera.

Un protocolo compacto para el accesorio puede tener este aspecto:

```json
{
  "action_id": "fq-2026-07-22-017",
  "hold_after": "request_headers_built",
  "release": false
}
```

La puerta de enlace se pausa después de crear los encabezados, pero antes de cualquier intento de conexión. El arnés confirma el estado, mata el proceso y después pregunta al receptor si vio una conexión. El resultado esperado es cero solicitudes entrantes y cero encabezados con credenciales. Si el receptor vio una solicitud en este punto, tu punto de control está demasiado tarde o una tarea en segundo plano cruzó el límite sin que se contabilizara.

Haz que los puntos de control sean precisos. «Antes de la E/S de red» es demasiado amplio si la resolución DNS, la creación de la conexión, la negociación TLS y la transmisión del cuerpo se ejecutan en tareas distintas. No necesitas un punto de control dentro de cada llamada de biblioteca, pero sí la estructura suficiente para afirmar si el servidor remoto pudo recibir credenciales o un cuerpo que cambiara el estado.

En SSH se aplica la misma idea, con pruebas distintas. Detén el proceso antes de que `sp-ssh` reciba una solicitud de conexión respaldada por credenciales, después de que se inicie el asistente pero antes de la autenticación, después de la autenticación pero antes de que comience el comando y después de que el comando remoto devuelva el resultado pero antes de confirmar la finalización local. Registra el inicio y la salida del comando remoto en un host de prueba desechable. No deduzcas que el comando se ejecutó solo porque existe una conexión TCP abierta.

## Matar antes de la inyección no debe dejar rastro remoto

El caso de fallo más temprano tiene el resultado esperado más claro: si la puerta de enlace muere antes de adjuntar una credencial o iniciar el transporte, ningún servicio externo debe ver nada de esa acción.

Parece obvio, pero las implementaciones suelen mezclar preparación y envío. Una biblioteca cliente puede comenzar una conexión mientras otra tarea obtiene o da formato a un encabezado. Una envoltura de reintento puede reservar una solicitud saliente antes de que el código escriba el registro previsto en el diario. Una devolución de llamada de métricas puede escribir una descripción de acción que contenga un parámetro de consulta URL que tu ruta normal de ocultación nunca ve.

Prueba cuatro interrupciones previas al envío:

- Después de la autorización, pero antes de buscar cualquier credencial.
- Después de buscar la credencial, pero antes de construir la solicitud.
- Después de construir la solicitud, pero antes de iniciar una conexión de socket.
- Después de comenzar la configuración de la conexión, pero antes de que salgan del proceso bytes que contengan credenciales.

Cada interrupción tiene una afirmación distinta. Antes de buscar la credencial, inspecciona los diagnósticos y registros seguros para comprobar que no haya valores ni marcadores que después puedan sustituirse. Después de buscarla, inspecciona las mismas salidas y verifica que el secreto nunca haya salido del límite del proceso. Antes de la conexión, el receptor no debe tener ningún registro de conexión. Durante la configuración de la conexión, el receptor puede ver un protocolo de enlace fallido o un intento de conexión, pero no debe recibir un encabezado de autorización, un campo de autenticación básica, un encabezado de credencial personalizado ni un intento de autenticación SSH.

Esta distinción importa porque un intento de conexión no es una acción autenticada. No informes «no ocurrió ninguna acción» si el extremo remoto registró un intento de conexión. Informa de la verdad concreta: no se envió ninguna credencial y no se recibió ninguna solicitud de aplicación. Los registros de seguridad pierden utilidad cuando borran pruebas que los operadores necesitarán durante una revisión de incidentes.

El diseño de Sallyport hace que valga la pena probar directamente este límite: el agente nunca debe tener una credencial en texto plano, mientras la aplicación ejecuta la acción HTTP o SSH respaldada por credenciales y devuelve un resultado. La prueba no termina porque el agente no tenga el secreto. Termina cuando una puerta de enlace eliminada tampoco puede convertir una acción medio preparada en una solicitud saliente que contenga un secreto.

## La inyección es un evento local, no una prueba del envío

La inyección de credenciales es donde los equipos cometen el error contable más perjudicial. Registran «credencial usada» cuando el código construye un encabezado o entrega una identidad a una biblioteca SSH. Ese evento demuestra que la puerta de enlace se preparó para autenticarse. No demuestra que ningún par haya recibido la credencial.

Mantén separados al menos cuatro estados locales:

| Estado | Lo que puedes afirmar honestamente | Lo que no puedes afirmar |
|---|---|---|
| Credencial resuelta | El proceso protegido leyó una credencial para esta acción | Que un par la recibió |
| Solicitud ensamblada | El proceso creó en memoria una solicitud con credenciales | Que se abrió una conexión |
| Escritura de transporte iniciada | El proceso intentó enviar bytes | Que la aplicación remota los procesó |
| Resultado remoto observado | El proceso recibió pruebas del lado remoto | Que el registro de finalización es duradero |

Mata el proceso después de resolver la credencial y otra vez después de crear el objeto de solicitud, pero antes de iniciar la escritura de transporte. En ambos casos, el agente no debe recibir ningún secreto y el accesorio remoto no debe registrar ninguna solicitud que contenga credenciales. El diario de acciones debe mostrar un estado de preparación interrumpida o ningún registro persistente, según el punto en que se encuentre tu límite de durabilidad. No inventes una acción completada solo para que el diario parezca ordenado.

Después mata el proceso en el primer punto en que la biblioteca de transporte pueda escribir. Esta es la prueba incómoda. El proceso local puede haber llamado a una función de escritura, pero el núcleo, el proxy, la capa TLS o el servicio remoto pueden no haber recibido la solicitud completa. El resultado esperado debe ser `outcome=unknown`, salvo que el receptor tenga pruebas positivas de que recibió o no recibió la solicitud.

Evita registrar todos los encabezados de la solicitud para facilitar esta prueba. Es un atajo que crea una segunda ruta para los secretos. En su lugar, haz que el accesorio calcule un marcador no reversible a partir del valor de la credencial recibida y guarde solo ese marcador. La puerta de enlace puede guardar una referencia a la credencial o un identificador interno. Durante la prueba, compara identificadores y marcadores sin imprimir la credencial.

Un registro útil del accesorio tiene esta forma:

```json
{
  "action_id": "fq-2026-07-22-017",
  "connection_seen": true,
  "headers_complete": true,
  "credential_marker": "sha256:7b8c...",
  "body_complete": false,
  "response_sent": false
}
```

Usa una credencial exclusiva para pruebas, cuyo valor no exista fuera del accesorio. Aun así, mantenla fuera de capturas de pantalla, del historial del shell y de los registros normales. Un secreto desechable sigue teniendo forma de secreto, y los hábitos de prueba suelen acabar en el código de producción.

## La E/S de red crea un estado desconocido honesto

Una vez que una solicitud que cambia el estado puede salir de la máquina, la certeza local termina antes de que el sistema remoto necesariamente responda. Este es el caso que debe definir el comportamiento de los reintentos, el texto de la interfaz y la semántica de auditoría.

Imagina un POST que pide a un servicio de despliegue iniciar una publicación. La puerta de enlace escribe la solicitud completa. El servicio persiste el trabajo de despliegue. Antes de que la respuesta llegue a la puerta de enlace, matas el proceso. Después del reinicio, hay tres realidades posibles:

1. El servicio nunca recibió la solicitud.
2. El servicio recibió la solicitud, pero la rechazó.
3. El servicio aceptó la solicitud y creó un despliegue.

La puerta de enlace local puede no saber cuál ocurrió. Un registro marcado como `failed` es falso en el caso tres. Uno marcado como `succeeded` es falso en los casos uno y dos. Márcalo como `unknown` y conserva el ID de acción, el ID de solicitud remota, el destino, el método y el punto en que se detuvo la observación local.

Aquí es donde los reintentos ciegos causan daños. A los equipos les gustan los reintentos automáticos porque los fallos de red transitorios son habituales y las demostraciones exitosas parecen fluidas. Para un GET, normalmente es aceptable reintentar si no causa problemas de contabilidad ni de límites de frecuencia en el servidor. Para un POST, PATCH, comando remoto o llamada a una API capaz de escribir, el reintento necesita un mecanismo de idempotencia o una reconciliación posterior.

Usa la función de idempotencia del sistema remoto cuando exista. Proporciona un token de idempotencia específico de la acción, no uno reutilizado para toda la sesión del agente. Si la API remota no lo admite, crea una ruta de consulta remota que pueda responder si el ID de acción fue aceptado antes de reintentar. Si no existe ninguna de las dos opciones, pide a una persona que decida. Es menos elegante que la recuperación automática y mucho más seguro que ejecutar la acción dos veces.

Prueba esta secuencia exacta con tu receptor:

```text
Gateway writes intent record
Gateway sends POST with action ID fq-2026-07-22-017
Receiver stores action ID and body
Receiver delays its HTTP response
Harness kills gateway with SIGKILL
Gateway restarts
Agent requests retry
Gateway checks receiver for action ID before another POST
```

El resultado debe mostrar una acción remota almacenada y un registro local con un resultado interrumpido o desconocido que después se reconcilia con el estado remoto. Si aparece un segundo POST antes de la comprobación de reconciliación, la prueba ha encontrado un fallo de reintento. Si el diario sobrescribe la incertidumbre y deja solo un registro de éxito limpio, ha encontrado un fallo de auditoría.

No dependas de los eventos de cierre TCP como prueba de que una aplicación remota no actuó. Un socket puede cerrarse después de que el par ya haya entregado la solicitud a su código de aplicación. Lo que importa es el registro de acción duradero del receptor.

## La confirmación de auditoría debe tener una promesa de durabilidad definida

Un registro de auditoría no se vuelve fiable solo porque contenga una cadena de hashes. Una cadena de hashes puede revelar alteraciones o eliminaciones dentro de la secuencia que sobrevivió. No puede recuperar un evento que nunca llegó al almacenamiento duradero antes de que muriera el proceso.

Escribe la promesa en una sola frase. Por ejemplo: «Antes de que una puerta de enlace comience una acción externa que cambie el estado, registra de forma duradera la intención de la acción; después de observar un resultado, registra ese resultado de forma duradera antes de devolvérselo al agente». Después prueba los verbos «comience» y «de forma duradera», en lugar de tratar un objeto en memoria con marca de tiempo como un registro.

POSIX define `fsync()` como una solicitud para transferir los datos de un archivo al almacenamiento asociado y establece que no devuelve el control hasta que esa acción termina o informa de un error. El estándar también advierte que las garantías de almacenamiento dependen de la implementación y la configuración. Esa advertencia no es una excusa para omitir la llamada. Significa que debes precisar qué puede prometer tu software y probar el comportamiento de recuperación que controla.

Para un registro cifrado y encadenado mediante hash, prueba al menos estas interrupciones:

- Mata el proceso después de escribir una entrada propuesta, pero antes de la barrera de durabilidad.
- Mata el proceso después de la barrera de durabilidad, pero antes de actualizar el índice en memoria.
- Mata el proceso después de que una entrada de finalización sea duradera, pero antes de que el agente reciba la respuesta.
- Mata el proceso durante la compactación, rotación o cualquier proyección del diario.

Después de cada reinicio, ejecuta el comando de verificación sin conexión e inspecciona tanto la secuencia de auditoría sin procesar como las proyecciones visibles para el usuario. Sallyport expone `sp audit verify` para verificar su registro de auditoría cifrado y encadenado mediante hash sin necesitar la clave de la bóveda. Eso lo hace útil en este conjunto de pruebas, pero la verificación debe ser una afirmación entre varias, no la prueba completa.

Tus resultados esperados necesitan matices. Que falte un registro después de matar el proceso antes de su límite de durabilidad puede ser aceptable si no comenzó ninguna acción externa. Que falte después de que la puerta de enlace iniciara una solicitud que cambiara el estado no es aceptable si el diseño prometía una intención escrita por anticipado. Un registro presente con resultado desconocido suele ser correcto. Un registro que afirma la finalización antes de observar la respuesta remota es incorrecto, aunque la prueba normalmente parezca funcionar.

Mantén separado el historial de ejecuciones del agente del historial de llamadas de acciones al inspeccionar la recuperación. Una sesión puede terminar abruptamente mientras varias acciones tienen resultados distintos. Una sola entrada de ejecución que diga «terminada» no sustituye los registros de cada llamada: una solicitud pudo llegar al sistema remoto y otra no salir nunca de la memoria.

## El estado de aprobación no debe sobrevivir a su objeto

Un reinicio pone a prueba el estado de autorización tanto como la durabilidad. Si una puerta de enlace autoriza a un proceso de agente concreto para una sesión, matarla no debe convertir esa autorización en un permiso reutilizable por otro proceso que casualmente haga una solicitud parecida.

La regla segura más sencilla es que la aprobación de una sesión pertenezca a una identidad de proceso observada y muera con esa ejecución. Después del reinicio, un proceso de agente nuevo necesita una autorización de sesión nueva. Un proceso que haya permanecido activo mientras la puerta de enlace se reiniciaba también necesita una decisión nueva, salvo que la puerta de enlace pueda restablecer exactamente la vinculación de identidad y el producto documente deliberadamente ese comportamiento. No restaures la aprobación desde un registro de caché impreciso como el nombre del comando, el directorio de trabajo o una etiqueta de proceso amigable.

Prueba el límite de reinicio con dos agentes que parezcan similares desde la línea de comandos, pero difieran en la autoridad de firma o el origen del ejecutable. Aprueba el primero. Detén la puerta de enlace durante una acción. Reiníciala. Después haz que el segundo agente emita la misma acción. La puerta de enlace debe mostrar una decisión de aprobación nueva, no heredar la sesión del primer agente.

La aprobación por llamada necesita otra prueba. Mata la puerta de enlace mientras la tarjeta de aprobación está visible, reiníciala y repite la acción. El evento antiguo de la interfaz no debe autorizar la llamada nueva. Mata el proceso después de que el usuario apruebe, pero antes de resolver la credencial, y confirma que el proceso nuevo no puede reutilizar la aprobación antigua. Estas pruebas detectan un error habitual: persistir el booleano «approved» sin vincularlo a un ID de acción, una sesión concreta y una condición de caducidad.

Los controles fijos de Sallyport dan una forma clara a esta prueba. Su compuerta de bóveda deniega las acciones mientras está bloqueada, la autorización de sesión se vincula a un proceso de agente nuevo y determinadas credenciales pueden exigir aprobación en cada uso. Las pruebas de salida forzada deben demostrar que esos límites siguen siendo ciertos cuando la interfaz, el estado de la bóveda y la cadena de acciones se reinician en momentos distintos.

## Crea una matriz de resultados antes de ejecutar el conjunto

Una prueba que solo dice «matar en la etapa X» deja demasiado margen para la interpretación. Crea una matriz con filas para los puntos de interrupción y columnas para los resultados observables. Revisa el resultado esperado antes de escribir el código de prueba. Si el equipo no puede acordar qué debe ocurrir, la implementación aún no tiene un contrato de fallo definido.

Usa estas columnas:

| Punto de interrupción | ¿El sistema remoto puede ver una conexión? | ¿Puede ver credenciales? | Estado de acción local permitido | Resultado del agente tras la recuperación | Pruebas necesarias |
|---|---:|---:|---|---|---|
| Antes de buscar la credencial | No | No | No iniciada o interrumpida | Aprobación nueva o ruta de reintento | Solo traza de la puerta de enlace |
| Después de resolver la credencial | No | No | Preparación interrumpida | Aprobación nueva o ruta de reintento | Solo traza de la puerta de enlace |
| Durante la escritura de la solicitud | Sí | Posiblemente | Desconocido | Reconciliar antes de reintentar | Registro del receptor y registro local |
| Después de la aceptación remota | Sí | Sí | Desconocido hasta observar o reconciliar | Reconciliar antes de reintentar | Registro remoto duradero |
| Después de la finalización duradera | Sí | Sí | Completada | Devolver el resultado almacenado o reconciliado | Registro de auditoría verificado |

Las palabras «posiblemente» y «desconocido» no son debilidades. Son una descripción honesta del trabajo distribuido. La palabra peligrosa es «fallida» cuando la puerta de enlace no tiene pruebas de que el servicio remoto no actuara.

Ejecuta cada fila varias veces, pero no conviertas la repetición en un sustituto del control. Cien eliminaciones aleatorias pueden no encontrar la línea entre añadir una entrada al diario y su barrera de durabilidad. Una sola eliminación controlada en un punto de espera puede establecer qué ocurre en esa línea. Repite después para detectar carreras, errores de planificación y movimientos accidentales del punto de control.

Recoge artefactos de ambos lados en cada ejecución: la línea temporal de eventos del arnés, la lista duradera de acciones del accesorio remoto, la información de salida del proceso, los diagnósticos de recuperación de la puerta de enlace, la salida de verificación de auditoría y la salida visible para el agente. Incluye el ID de ejecución en el nombre del archivo. Si una prueba falla, conserva las pruebas antes de volver a ejecutarla. La segunda ejecución suele destruir la única pista útil.

## Trata las discrepancias como trabajo de diseño, no como ruido de prueba

Cuando una prueba de salida forzada produce una discrepancia entre el diario de la puerta de enlace y el accesorio remoto, resiste la tentación de modificar la afirmación hasta que pase. La discrepancia suele ser precisamente el hallazgo de la prueba.

Si el accesorio informa de una solicitud aceptada y la puerta de enlace no registra ninguna intención, adelanta el límite de intención duradera o detén la acción antes de ese límite. Si la puerta de enlace informa de una finalización y el accesorio no registra ninguna acción, determina si la puerta de enlace confundió una escritura local con un resultado remoto. Si un agente reiniciado puede actuar sin una aprobación nueva, corrige la vinculación de identidad en lugar de añadir un tiempo de espera más largo.

El mejor resultado no es un panel lleno de pruebas verdes. Es un contrato de fallo que un operador pueda usar bajo presión: esta acción no salió de la máquina; esta acción pudo llegar al sistema remoto y necesita reconciliación; esta acción terminó y su registro superó la verificación. Esas son las únicas categorías que permiten defender una decisión posterior a un fallo.

Ejecuta este conjunto cada vez que cambies la gestión de credenciales, las bibliotecas de transporte, el registro, el comportamiento de los reintentos, la supervisión de procesos o la gestión de aprobaciones. Una puerta de enlace se gana la confianza comportándose de forma predecible cuando ya no tiene oportunidad de explicarse.
