¿La gestión de solicitudes con la bóveda bloqueada puede repetir una llamada?
Prueba la gestión de solicitudes con la bóveda bloqueada para confirmar que las llamadas denegadas de agentes de IA se descartan, nunca quedan en una cola ni se repiten después de abrir la bóveda.

Una bóveda bloqueada debe crear un límite temporal firme. Una solicitud que llega a una puerta de enlace de acciones antes de que se levante ese límite debe fallar en ese momento. No debe quedarse en una cola, sobrevivir a una reconexión ni volverse elegible porque alguien se autentique después en la bóveda.
Parece obvio hasta que pruebas una pila de agentes real. Los agentes reintentan. Los clientes MCP se reconectan. Las bibliotecas HTTP repiten solicitudes después de perder una respuesta. Los workers conservan trabajos. Una interfaz puede mostrar una denegación mientras otro componente ya ha retenido suficiente estado para ejecutar la llamada más tarde. Si solo observas la tarjeta de aprobación o la transcripción del agente, puedes pasar por alto la parte peligrosa.
La prueba descrita aquí responde a una pregunta concreta: ¿la puerta de enlace descartó las llamadas recibidas mientras la bóveda estaba bloqueada o el desbloqueo hizo que alguna de esas llamadas antiguas se ejecutara? Usa un receptor HTTP controlado, dos identificadores de solicitud únicos y pruebas procedentes de ambos lados de la puerta de enlace. Hazla antes de confiar a un agente autónomo cualquier endpoint que pueda modificar dinero, infraestructura, código fuente o datos de clientes.
El bloqueo de la bóveda debe cortar una acción, no posponerla
La regla esperada es sencilla: cuando la bóveda está bloqueada, la puerta de enlace deniega todas las acciones que la necesitan. Desbloquearla cambia la respuesta para una llamada posterior. No cambia la respuesta para una llamada que ya llegó.
La diferencia importa porque una solicitud tiene un ciclo de vida. Un proceso escribe bytes en stdin. Un shim MCP analiza mensajes JSON-RPC. La puerta de enlace identifica una acción configurada, comprueba si puede usar un secreto, inyecta las credenciales si está permitido, abre una conexión saliente y devuelve un resultado. Un error puede conservar la solicitud en varios puntos de ese recorrido.
Un diseño seguro trata la decisión de bloqueo como definitiva para esa invocación. La puerta de enlace puede registrar la denegación, pero no debe conservar un cierre ejecutable, el cuerpo de una solicitud serializada, un trabajo saliente ni un token de reintento que pueda ejecutarse más tarde con una bóveda recién abierta.
A menudo se confunden dos comportamientos distintos:
- Un reintento nuevo es una llamada nueva que el agente envía después de observar un error o un cambio de estado.
- Una repetición es la ejecución de la llamada original, retenida por la puerta de enlace o por uno de sus helpers mientras el acceso estaba denegado.
Un reintento nuevo puede ser legítimo, aunque sigue necesitando la autorización normal. Una repetición cruza un límite de seguridad sin una decisión nueva. Si los confundes, puedes dar por superada la prueba al desbloquear la bóveda, ver que una solicitud llega al destino y asumir que procedía de un reintento deliberado.
El Model Context Protocol no resuelve este problema de diseño. Su transporte stdio usa mensajes JSON-RPC delimitados por saltos de línea entre un proceso de servidor iniciado por el cliente y el propio cliente. Las solicitudes JSON-RPC con un id reciben una respuesta correlacionada, mientras que las notificaciones no reciben ninguna. Esas reglas del protocolo te dan una correlación útil, pero no definen si una puerta de enlace puede conservar una acción denegada para ejecutarla después. Esa decisión debe quedar explícita en tu puerta de enlace.
El momento de llegada es anterior a la llamada HTTP saliente
Una solicitud llega cuando la puerta de enlace tiene información suficiente para decidir si debe ejecutarla, no cuando el servidor de destino ve el tráfico. Si la bóveda está bloqueada en ese momento, la denegación debe producirse antes de inyectar credenciales y antes de entregar el trabajo a cualquier componente que pueda sobrevivir a la decisión.
Aquí es donde las pruebas se vuelven imprecisas. Alguien bloquea la bóveda, indica al agente que llame a una API, espera un error, desbloquea la bóveda y comprueba que no aparezca ninguna solicitud de inmediato. Esa prueba no detecta reintentos retrasados, workers bloqueados, grupos de conexiones ni temporizadores de reintento del cliente. Tampoco detecta que la llamada haya llegado al destino antes de que la interfaz mostrara el error.
Usa tres marcas de tiempo registradas desde lugares independientes:
T_lock: cuando se confirmó que la bóveda estaba bloqueada.T_attempt: cuando el agente envió el identificador de la solicitud antigua.T_unlock: cuando la bóveda volvió a abrirse.
Después de T_unlock, sigue observando el receptor. La espera debe superar todos los reintentos y tiempos de espera configurados en el cliente, la puerta de enlace y cualquier intermediario. Si no conoces esos valores, no elijas un retraso corto que resulte tranquilizador. Averígualos primero o usa un receptor que permanezca disponible el tiempo suficiente para revelar una entrega retrasada.
Una afirmación de aceptación útil es más precisa que «la solicitud bloqueada falló»:
Para el identificador
locked-..., el receptor controlado registra cero ejecuciones antes y después deT_unlock; para el identificadorfresh-..., enviado solo después deT_unlock, el receptor registra exactamente una ejecución.
La afirmación detecta ambos lados del fallo. Revela una llamada antigua que se ejecuta más tarde y demuestra que la prueba no falló simplemente porque el receptor o la configuración de la acción estuvieran rotos.
No uses el mismo contenido para ambas llamadas. Si las dos indican deploy=true, no podrás identificar cuál llegó. Incluye el identificador de la solicitud en la ruta URL, en un campo JSON inofensivo y en un encabezado, si la acción configurada lo permite. La redundancia resulta útil porque revela reescrituras o almacenamiento en caché accidentales.
Las colas y los reintentos ocultan los fallos de repetición
Los fallos de repetición más peligrosos no suelen ser espectaculares. Normalmente proceden de código habitual de fiabilidad escrito por alguien que supuso que un error de autorización se comporta como un fallo de red transitorio.
Considera una secuencia típica incorrecta. El agente envía una llamada de herramienta MCP mientras la bóveda está bloqueada. El shim acepta el mensaje y crea un elemento de trabajo interno. La comprobación de la bóveda devuelve un error de bloqueo, pero el worker lo clasifica como reintentable porque nunca se llegó al destino. El emisor se desconecta o la sesión termina. Más tarde, el usuario desbloquea la bóveda. El worker se despierta, encuentra una credencial utilizable y envía la solicitud HTTP original.
La interfaz puede parecer correcta durante toda la secuencia. El agente original recibió un error. El usuario vio la bóveda bloqueada. El destino recibió una credencial válida solo después del desbloqueo. Aun así, la puerta de enlace transportó una acción a través de un límite donde debería haber terminado.
Estos son los patrones que conviene buscar:
- Un wrapper genérico de reintentos captura todos los errores salvo la entrada mal formada.
- Una cola de trabajos persistente almacena la intención antes de decidir sobre la bóveda.
- Un future o una promise espera al desbloqueo en lugar de devolver un error definitivo.
- Una ruta de reconexión vuelve a enviar una solicitud en memoria después de que el proceso cliente haya desaparecido.
- Un helper en segundo plano mantiene el estado de reintento de forma independiente de la barrera de la bóveda.
La recomendación popular de «reintentar todas las operaciones de red fallidas» es incorrecta en este límite. Es popular porque los fallos de transporte de red son frecuentes y los reintentos suelen mejorar la entrega. Una bóveda bloqueada no es un fallo de transporte. Es una negativa explícita a usar una autoridad. Clasifícala como definitiva para esa invocación.
Esto también se aplica a la cancelación. La desconexión de un cliente no significa necesariamente que se haya cancelado una solicitud HTTP o SSE. La especificación de transporte MCP indica que una desconexión puede ocurrir en cualquier momento y no debe interpretarse por sí sola como una cancelación; cuando el cliente quiere cancelar, requiere una notificación explícita de cancelación. Ese comportamiento tiene sentido para trabajos largos, pero hace que el estado local de la puerta de enlace sea aún más importante: una llamada denegada no debe seguir siendo ejecutable solo porque el estado del transporte se haya vuelto ambiguo.
Crea un receptor que haga visible cada ejecución
Un receptor controlado ofrece mejores pruebas que la transcripción de un chat con el agente. Te indica si una llamada saliente llegó realmente, qué identificador llevaba y cuándo llegó. Mantenlo aislado de producción y haz que su único efecto secundario sea un registro local de solo anexado.
Ejecuta este pequeño receptor Python en una máquina y un puerto accesibles para tu puerta de enlace. Acepta solicitudes POST, escribe una línea JSON por cada llegada y devuelve una respuesta correcta inofensiva. Registra deliberadamente solo una marca corta del encabezado de autorización, no la credencial.
# receiver.py
from http.server import BaseHTTPRequestHandler, HTTPServer
from datetime import datetime, timezone
import hashlib
import json
LOG = "receiver-events.jsonl"
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length).decode("utf-8", errors="replace")
auth = self.headers.get("Authorization", "")
auth_marker = hashlib.sha256(auth.encode()).hexdigest()[:12] if auth else None
event = {
"received_at": datetime.now(timezone.utc).isoformat(),
"method": self.command,
"path": self.path,
"request_id": self.headers.get("X-Replay-Test-Id"),
"auth_marker": auth_marker,
"body": body,
}
with open(LOG, "a", encoding="utf-8") as log:
log.write(json.dumps(event) + "\n")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"received":true}')
def log_message(self, format, *args):
return
HTTPServer(("127.0.0.1", 8787), Receiver).serve_forever()
Inícialo con:
python3 receiver.py
El archivo de salida tendrá un aspecto similar a este:
{"received_at":"2026-07-22T16:42:12.103841+00:00","method":"POST","path":"/replay-test/fresh-8f1c","request_id":"fresh-8f1c","auth_marker":"a4d7e02c1b9f","body":"{\"kind\":\"fresh\"}"}
No pongas un secreto activo en el cuerpo de la solicitud. Configura la acción de prueba con una credencial de prueba específica y de privilegios reducidos en la bóveda, y deja que la puerta de enlace la inyecte mediante el canal HTTP habitual. La huella del encabezado del receptor solo demuestra que llegó algún valor de autorización; no escribe el valor en un archivo que podría sobrevivir a la prueba.
Antes de la prueba con la bóveda bloqueada, envía una solicitud normal mientras la bóveda está abierta. Confirma que el receptor la registra y que la ruta del endpoint configurado es correcta. Después, elimina receiver-events.jsonl o muévelo a otro lugar. Empezar con un registro vacío evita que una solicitud anterior de configuración contamine el resultado.
Ejecuta la prueba con dos llamadas deliberadamente distintas
La prueba necesita un identificador antiguo que se intente mientras la bóveda está bloqueada y un identificador nuevo que se cree solo después del desbloqueo. Usa etiquetas que parezcan aleatorias, pero anótalas antes de comenzar. Las etiquetas fáciles de reconocer simplifican la comparación durante la auditoría.
Por ejemplo:
old request ID: locked-3d4a
fresh request ID: fresh-91ce
Prepara una instrucción para el agente o una solicitud de cliente MCP que llame a tu acción HTTP configurada con esta información:
{
"path": "/replay-test/locked-3d4a",
"headers": {
"X-Replay-Test-Id": "locked-3d4a"
},
"body": {
"kind": "locked-period-attempt",
"request_id": "locked-3d4a"
}
}
El nombre exacto de la herramienta y la forma de sus argumentos dependen de la interfaz de acción que hayas configurado. No simules una prueba aprobada llamando directamente al receptor con un comando de shell. La solicitud debe recorrer la misma ruta de agente, shim MCP, puerta de enlace, bóveda y acción HTTP en la que piensas confiar.
Ahora ejecuta la secuencia sin improvisar:
- Confirma que el registro del receptor está vacío y que la bóveda está bloqueada.
- Inicia un proceso de agente nuevo y envía la llamada
locked-3d4a. - Captura el error del lado del agente y la hora. No vuelvas a enviar la solicitud.
- Mantén el proceso del agente activo durante un breve periodo de observación y después termínalo. Así detectarás tanto los reintentos inmediatos como los vinculados al proceso.
- Abre la bóveda y espera durante todo el periodo de observación. Inspecciona el registro del receptor varias veces.
locked-3d4adebe seguir ausente. - Solo después de esa espera, inicia un proceso de agente nuevo y envía
fresh-91ce. El receptor debe registrar ese identificador una vez.
Mantén las llamadas antigua y nueva en procesos de agente separados. De lo contrario, una aprobación de sesión o una caché interna del cliente puede confundir el resultado. Estás comprobando si una acción antigua puede cruzar el límite de bloqueo, no si un único proceso recuerda algún estado de autorización.
Si la llamada bloqueada genera una solicitud de aprobación después del desbloqueo sin que el agente envíe una llamada nueva, detente. Es una prueba de que la intención se conservó. Si el receptor recibe locked-3d4a en cualquier momento después del desbloqueo, considera que la prueba de seguridad ha fallado, aunque el servidor devuelva 200 sin modificar datos.
Inspecciona ambos registros, pero no sustituyas el receptor por logs
El registro de la puerta de enlace ayuda a reconstruir lo que esta creyó que había ocurrido. El receptor demuestra lo que ocurrió fuera de la puerta de enlace. Necesitas ambos porque cualquiera de las dos fuentes por separado puede crear una falsa sensación de seguridad.
Sallyport conserva un registro Sessions para las ejecuciones de los agentes y un registro Activity para las llamadas individuales, ambos proyectados desde un único registro de auditoría cifrado y encadenado mediante hashes. Su verificador sin conexión está disponible con sp audit verify y no necesita la clave de la bóveda para validar la cadena. Ejecuta ese comando después de la prueba y conserva el resultado junto con el registro del receptor y la transcripción del agente.
Usa los registros para responder preguntas concretas:
- ¿La llamada antigua creó una entrada de actividad y muestra un resultado denegado?
- ¿Qué proceso de agente y qué autoridad de firma de código registró la sesión?
- ¿Existe alguna actividad posterior con el identificador de solicitud antiguo, la ruta del endpoint o una ventana temporal coincidente?
- ¿Comenzó una sesión nueva para la llamada posterior al desbloqueo?
- ¿La verificación de auditoría se completa correctamente para los registros que recopilaste?
No supongas que una entrada de registro que dice «denegado» demuestra que la solicitud nunca salió de la máquina. Un registro recoge la explicación de la puerta de enlace sobre su propia decisión. El receptor controlado aporta la comprobación independiente. Del mismo modo, la ausencia de una entrada puede indicar que usaste términos de búsqueda incorrectos, que las marcas de tiempo no estaban sincronizadas o que la prueba no utilizó la acción prevista. Por eso es importante la solicitud nueva correcta.
Para una prueba de equipo repetible, guarda cuatro elementos bajo un único identificador de ejecución: la transcripción del agente, el archivo JSONL del receptor, la exportación del registro o capturas que identifiquen las llamadas y el resultado de sp audit verify. Evita incluir valores secretos en cualquiera de ellos.
Las notificaciones, los lotes y las reconexiones necesitan casos separados
Una prueba aprobada con una sola solicitud no cubre todas las formas de mensaje que puede enviar un cliente agente. Las solicitudes con identificadores son las más fáciles de probar porque JSON-RPC exige que la respuesta lleve el mismo identificador. Las notificaciones no tienen identificador ni reciben respuesta, así que eliminan la prueba habitual de que la puerta de enlace las rechazó. JSON-RPC indica explícitamente que un servidor no debe responder a las notificaciones.
Para una ruta compatible con notificaciones, incluye en el contenido saliente una marca visible en el receptor, como notification-77b2. Bloquea la bóveda, provoca que la notificación se emita una vez, desbloquea y comprueba que la marca nunca llegue. No deduzcas que hay seguridad a partir del silencio de la interfaz del cliente, porque el silencio es el comportamiento esperado del protocolo.
Si tu cliente o shim acepta entradas por lotes, estas merecen su propia prueba. Incluye dos llamadas inofensivas en el lote: una durante el bloqueo y otra que solo se envíe después del desbloqueo en un lote separado. No pongas llamadas antiguas y nuevas en el mismo lote, porque una puerta de enlace que procese parte del lote antes de un cambio de estado puede producir un resultado imposible de interpretar.
Las pruebas de reconexión deben variar una sola cosa cada vez. Ejecuta estos casos en pruebas separadas:
- Mantén el proceso del agente activo durante el desbloqueo.
- Termina el proceso del agente antes del desbloqueo.
- Reinicia solo el cliente MCP o el shim antes del desbloqueo.
- Desconecta la ruta de red después del error de bloqueo y vuelve a conectarla después del desbloqueo.
La expectativa no cambia. El identificador antiguo del receptor nunca debe aparecer. Si un identificador antiguo aparece solo después de una reconexión, has encontrado una ruta de repetición vinculada a la recuperación del transporte y no a la interfaz de la bóveda.
La aprobación por sesión no puede reparar una acción retenida
La autorización por sesión y la aprobación por llamada deciden si una acción actual puede continuar. No hacen segura la conservación de una acción que fue rechazada mientras la bóveda estaba bloqueada.
El orden importa. La barrera de la bóveda es absoluta: mientras esté bloqueada, toda acción se deniega. Solo después de que se abra, la puerta de enlace puede considerar el proceso que hay detrás de una decisión de autorización de sesión o solicitar aprobación por llamada para una credencial configurada. Invertir este modelo mental lleva a los equipos a preguntarse si una tarjeta de aprobación anterior debería autorizar una acción retrasada. No debería, porque la llamada bloqueada ya no tendría que existir como trabajo ejecutable.
Prueba los límites por separado. Primero demuestra que una llamada realizada durante el bloqueo nunca se ejecuta después del desbloqueo. Después, con la bóveda abierta, comprueba que un proceso de agente nuevo produce el comportamiento esperado de autorización de sesión. Por último, si una credencial tiene activada la aprobación por llamada, verifica que cada uso nuevo vuelve a solicitarla. Combinar las tres cosas en una ejecución larga dificulta localizar los fallos.
También existe un detalle sutil relacionado con la identidad del proceso. Una aprobación de sesión pertenece a una ejecución concreta de un proceso de agente, no a la idea de «el mismo asistente». Cuando reinicies un proceso, trátalo como nuevo hasta que la puerta de enlace lo identifique y autorice según sus propias reglas. No dejes que un script de prueba oculte esto reutilizando un proceso con una conexión antigua.
Convierte el resultado en un criterio de lanzamiento
Ejecuta esta prueba cada vez que cambies el envío de la puerta de enlace, el código del ciclo de vida de la bóveda, el shim MCP, el comportamiento de los reintentos, la configuración del cliente HTTP o el helper que realiza acciones SSH. Un fallo de repetición suele entrar durante un cambio de fiabilidad porque el código parece inofensivo en la revisión: una cola, un manejador de reconexión o una cláusula catch demasiado amplia.
Un lanzamiento debe fallar si alguna de estas afirmaciones es falsa:
- El receptor controlado tiene cero entradas para cada identificador enviado mientras la bóveda estaba bloqueada, incluso después de abrirla.
- Un identificador nuevo enviado después del desbloqueo llega una vez al receptor mediante la misma acción configurada.
- Un cliente o agente reiniciado no puede hacer que aparezca el identificador antiguo.
- Los registros identifican los eventos denegados y permitidos esperados sin duplicados inexplicables.
- La cadena de auditoría se verifica correctamente para las pruebas conservadas.
Siempre que sea posible, incorpora el caso negativo a una prueba de integración automatizada. El arnés de pruebas debe bloquear la bóveda, enviar la llamada antigua, abrir la bóveda, esperar durante una ventana de reintento limitada y comprobar que el receptor no contiene el identificador antiguo. Después debe enviar la llamada nueva y comprobar que llega una vez. Mantén el receptor local y desechable para que la prueba no tenga más autoridad que su propio archivo de registro.
El resultado incorrecto se resume fácilmente: una persona abre la bóveda para autorizar la siguiente acción y, en su lugar, se ejecuta una acción anterior. No aceptes una puerta de enlace que solo muestre el error correcto de bloqueo. Haz que demuestre, mediante un observador externo, que olvidó la solicitud antigua.
FAQ
¿Cómo demuestro que una solicitud bloqueada se descartó en lugar de retrasarse?
Una denegación solo demuestra que la puerta de enlace rechazó una llamada en un momento concreto. Una prueba de descarte va más allá: desbloquea la bóveda, espera más que todas las ventanas de reintento relevantes y demuestra que el identificador de la solicitud antigua nunca llegó a un destino controlado. Después, envía una solicitud nueva con otro identificador para confirmar que el flujo de prueba sigue funcionando.
¿Puede un agente reintentarlo simplemente después de que desbloquee la bóveda?
No. La puerta de enlace no puede impedir que un agente haga una solicitud nueva después de saber que la bóveda está abierta. La prueba debe distinguir una solicitud nueva posterior al desbloqueo de la solicitud antigua anterior al desbloqueo mediante identificadores diferentes y un receptor que las registre por separado.
¿Basta un tiempo de espera como prueba de que no ocurrió ninguna acción?
Un tiempo de espera es ambiguo: el cliente puede reintentarlo, la puerta de enlace puede seguir procesándolo o el destino puede haberlo recibido antes de que apareciera el tiempo de espera. Trata el tiempo de espera como un fallo de la prueba hasta que el registro del receptor y los registros de la puerta de enlace aclaren qué ocurrió.
¿Cómo debo probar las notificaciones JSON-RPC mientras la bóveda está bloqueada?
Las notificaciones requieren más cuidado porque JSON-RPC no proporciona al emisor una respuesta con la que pueda correlacionar el resultado. Para una acción sensible, usa en la prueba una solicitud con identificador o añade una marca independiente en el receptor que haga visible la ejecución.
¿Reiniciar el agente hace segura una solicitud antigua?
Un proceso de agente nuevo debe volver a superar la autorización de sesión si ese control está activado, pero eso por sí solo no demuestra que las llamadas realizadas durante el bloqueo se hayan descartado. Prueba por separado el reinicio del proceso y el desbloqueo de la bóveda, y comprueba si alguno de los dos libera una intención antigua.
¿Las claves de idempotencia resuelven las repeticiones después del desbloqueo?
Solo si el endpoint de destino hace que la entrega duplicada no tenga consecuencias y registra la clave de idempotencia. Una clave de idempotencia evita efectos repetidos en el destino, pero no autoriza a la puerta de enlace a conservar una acción que llegó mientras la bóveda estaba bloqueada.
¿Puedo ejecutar esta prueba contra una API de producción?
Usa un receptor que controles, una ruta de prueba distinta y datos sin valor operativo. No dirijas esta prueba a endpoints de producción de facturación, despliegues, DNS o control de código fuente solo porque ya existan.
¿Cuál es la prueba manual más rápida de repetición?
Primero comprueba en el receptor controlado que no aparezca el identificador antiguo. Después del desbloqueo, envía un identificador nuevo y confirma que llega una sola vez. Si el identificador antiguo aparece después del desbloqueo, considera el resultado un fallo de seguridad aunque el destino no haya cambiado nada.
¿Qué pruebas de auditoría debo conservar para esta prueba?
Necesitas un registro que recoja la denegación o el estado bloqueado, la identidad de la sesión del agente y el valor de correlación de la solicitud. Un registro de auditoría resistente a manipulaciones ayuda a demostrar que esos datos no se editaron después, pero no sustituye las pruebas del receptor.
¿Qué comportamiento debe garantizar una puerta de bóveda?
Una interfaz limpia significa que la puerta de la bóveda rechaza la llamada antes de que entre en una cola de trabajo persistente o en un mecanismo de reintento. Abrir la bóveda cambia los permisos de las llamadas que llegan después, pero no debe convertir las llamadas rechazadas anteriormente en trabajo ejecutable.