Temporal Policies en AgentCore: 98 despliegues denegados y un 409 que nunca llegó
Tabla de Contenidos

Le pedí a un agente Strands algo razonable: «ejecuta las pruebas y despliega». Emitió run_tests y deploy en el mismo turno, y el Gateway rechazó el despliegue. Repetí la tarea en diez sesiones y obtuve diez rechazos.

La política que lo bloqueó decía exactamente eso: «permite deploy solo si hubo un run_tests completado en los últimos cinco minutos». Cuando el deploy llegó al Gateway, las pruebas todavía estaban corriendo. La política hizo su trabajo; el que no esperó fue el agente.

Las Temporal Policies de AgentCore Policy permiten escribir reglas que dependen de lo que el agente hizo antes en la sesión y que se aplican en el Gateway, fuera del código del agente. La documentación cubre bien el caso feliz: secuencias obligatorias, límites por sesión, ventanas de tiempo. Lo que no cubre es qué pasa cuando el agente paraleliza, cuando una solicitud se deniega o cuando despliegas una política nueva con conversaciones vivas. Para averiguarlo armé un Gateway con herramientas simuladas, escribí las hipótesis antes de ejecutar nada y lancé 5,294 llamadas contra él.

📌 TL;DR — Lo que encontré

  • Con llamadas en paralelo, el deploy dependiente se denegó 98 de 100 veces. En secuencia, 0 de 600, incluso sin retardo agregado. Un agente Strands paralelizó en las 10 sesiones.
  • Las solicitudes denegadas también consumen el presupuesto de un límite basado en ::request: 40 de 40 pruebas.
  • Una sesión obsoleta llega como HTTP 200 con código JSON-RPC -32005, no como el 409 ConflictException que menciona la documentación.
  • Solo los cambios en políticas temporales invalidan sesiones vivas. Las políticas Cedar sin estado y el cambio de modo del Gateway no las tocan.
  • La validación por defecto rechaza el permit simple que pide la documentación, y también un forbid creado antes que su permit.
  • Evaluar la parte temporal cuesta entre 20 y 27 ms con hasta ~1,000 llamadas de historial.

Si no leíste el artículo de AgentCore Policy, ahí explico el modelo default-deny y Cedar. Aquí asumo que ya lo conoces.

Qué Son las Temporal Policies

Una política Cedar normal no tiene estado: decide mirando solo la solicitud actual. Una Temporal Policy le agrega memoria de la sesión. Se escribe en Dogwood, un lenguaje de políticas de código abierto compatible con Cedar (todo Cedar válido es Dogwood válido), y sus condiciones van en un bloque when temporal { ... } con operadores como formerly within, since within, count y sum.

Para que esto funcione hacen falta dos piezas. La primera es la sesión de política: el identificador lo generas tú y lo envías en el header x-amzn-bedrock-agentcore-policy-session-id desde la primera solicitud. El Gateway no lo genera; lo asocia a la identidad autenticada del llamador y lo expira después de 24 horas sin actividad. La segunda es el evento. Cada acción produce hasta tres tipos —request, response (solo si la herramienta terminó con éxito) y error— y los predicados temporales buscan eventos de esos tipos dentro de una ventana de tiempo.

Esta es la política de secuencia que usé:

permit (
    principal,
    action == AgentCore::Action::"ToolsTarget___deploy",
    resource == AgentCore::Gateway::"<ARN-DEL-GATEWAY>"
)
when temporal {
    formerly within 5m AgentCore::Action::"ToolsTarget___run_tests"::response{
        eventResource: resource
    }
};

deploy se permite si en los últimos cinco minutos hubo una respuesta de run_tests. Para que esa respuesta quede en el historial, la documentación pide además un permit sin condiciones para run_tests, porque una acción solo se registra como response si alguna política la permitió. Ese permit simple reaparece en el Hallazgo 4.

Las cuotas documentadas son 20 políticas temporales por engine, 3 operadores por política y una ventana máxima de 24 horas.

Cómo Lo Medí

El montaje fue deliberadamente simple. Una Lambda simulada con cinco herramientas (run_tests, deploy, read_secret, send_webhook y rollback), con datos falsos y sin salida a red. Un Gateway con autenticación AWS_IAM en us-east-1. Y un harness en Python que firma con SigV4 y escribe cada llamada en un JSONL inmutable con timestamp, request ID, decisión, latencia y la respuesta completa. Todas las tablas de este artículo salen de esos archivos por script, ninguna está hecha a mano. Para la prueba con agente usé Strands y Claude Sonnet con el perfil de inferencia us.

Un detalle para leer los datos: el Gateway devuelve las denegaciones como HTTP 200 con un error JSON-RPC -32002, no como un 4xx. La decisión hay que leerla en el cuerpo de la respuesta, no en el código HTTP.

El método es el mismo del A/B test del Optimization Loop: antes de ejecutar escribí cada hipótesis con el criterio que la refutaría, y registré cada cambio de diseño posterior (terminaron siendo diez). Los scripts, las políticas y el harness están en github.com/codecr/agentcore-temporal-policies.

El piloto con N=5 me corrigió una suposición antes de gastar en serio. Yo había definido «retardo cero» como «el cliente envía la solicitud dependiente apenas recibe la respuesta». Al medirlo, ese cero era en realidad un hueco de unos 15 ms entre una cosa y la otra, así que agregué una condición en paralelo a la prueba de secuencia y lo anoté como la primera desviación.

🔍 ProTip #1: Corre un piloto con N=5 por condición antes de escalar cualquier medición contra un servicio. El mío costó menos de un centavo y me mostró que mi «retardo cero» no era cero. Con N=100 desde el inicio habría gastado el tiempo midiendo otra cosa.

Hallazgo 1: Si Tu Agente Paraleliza, Pierde el Despliegue

Esta es la pregunta que dio origen al artículo. Mi hipótesis era que el evento response de run_tests tardaría en quedar disponible, de modo que un deploy enviado justo después de recibir la respuesta se denegaría en algunas sesiones. El criterio para refutarla era cero denegaciones con retardo 0 en 100 sesiones.

Condición Sesiones deploy denegado Tasa (IC 95% Wilson)
Secuencial, retardo 0 ms 100 0 0.0% [0.0, 3.7]%
Secuencial, 50 / 100 / 250 / 500 / 1000 ms 100 por celda 0 0.0% [0.0, 3.7]% por celda
Paralelo (dos hilos a la vez) 100 98 98.0% [93.0, 99.4]%
Agente Strands, prompt «paralelo» 5 5 N=5, sin intervalo útil
Agente Strands, prompt «neutro» 5 5 N=5, sin intervalo útil

La hipótesis queda refutada. Con un hueco mediano de unos 15 ms entre la respuesta de run_tests y el envío de deploy, no hubo una sola denegación en 600 sesiones secuenciales: el evento ya está disponible cuando la respuesta llega al cliente.

El problema aparece cuando las dos solicitudes salen a la vez. deploy se evalúa casi de inmediato, mientras run_tests tarda unos 770 ms en responder (mediana medida), así que el Gateway ve un deploy sin pruebas completadas y lo deniega. Es el comportamiento correcto de la política. AWS ya recomienda enviar la solicitud dependiente después de recibir la respuesta previa (sección Sequencing actions that depend on a prior response), y su blog sobre Dogwood menciona que los agentes hacen llamadas en paralelo. Lo que agrega este experimento es la magnitud: 98 de 100.

Los dos deploy que sí pasaron en la condición paralela (repeticiones 14 y 92) tuvieron latencias de 1,045 y 1,152 ms, contra una mediana de 764 ms, lo que es compatible con que su evaluación ocurriera más tarde de lo normal.

Con un agente de por medio, la pregunta cambia: ¿el modelo espera la respuesta o paraleliza? Le di a un agente Strands la tarea «ejecuta las pruebas y despliega» con dos variantes de prompt, una que pedía paralelismo y otra neutra, en 5 sesiones cada una. En las 10 sesiones emitió run_tests y deploy en el mismo turno, incluida la variante neutra, y el deploy fue denegado en las 10. Ninguna sesión reintentó el deploy: cada una cerró con dos llamadas al Gateway y dos ciclos de modelo, así que el agente recibió el rechazo y terminó sin volver a intentarlo.

Un detalle más para quien tenga que depurar esto: cuando deploy se llamó sin ningún run_tests previo, el mensaje fue Tool Execution Denied: Tool call not allowed due to policy enforcement [No policy applies to the request (denied by default).]. El mensaje no distingue «todavía no» de «nunca».

🎓 ProTip #2: Si dos acciones de tu agente dependen una de la otra, no dejes que el modelo decida el orden. Serializa en el cliente o en el framework, o diseña una sola herramienta que haga ambas cosas por dentro. La segunda opción te cuesta visibilidad, porque la política ya no ve el paso intermedio; la primera te cuesta latencia. Elige sabiendo cuál de las dos pagas.

Hallazgo 2: Las Denegaciones También Gastan Tu Límite

Aquí mi hipótesis también estaba equivocada, y esta vez en una dirección que la documentación no anticipa.

La documentación del esquema de eventos dice que request se registra «for each authorized request» y que lo denegado produce un evento error. Con esa lectura, una ráfaga de solicitudes denegadas no debería afectar un límite que cuenta ::request. Para comprobarlo combiné dos políticas. La primera es un límite por sesión que bloquea send_webhook cuando hay más de cinco eventos ::request en diez minutos (el conteo incluye la solicitud actual):

forbid (
    principal,
    action == AgentCore::Action::"ToolsTarget___send_webhook",
    resource == AgentCore::Gateway::"<ARN-DEL-GATEWAY>"
)
when temporal {
    exists (n: Long).
        (count for (t: Timepoint).
            where (formerly within 10m (AgentCore::Action::"ToolsTarget___send_webhook"::request{ eventResource: resource } && tp(t)))) == n
        && n > 5
};

La segunda es una regla Cedar sin estado que deniega cualquier send_webhook dirigido a https://blocked.invalid/hook. En cada sesión nueva envié K llamadas a esa URL, todas denegadas, y después hasta 8 llamadas permitidas, anotando en cuál de ellas aparecía el primer bloqueo por límite. Veinte pruebas por cada valor de K, en orden aleatorio.

Si las denegadas no cuentan, el límite salta siempre en la sexta llamada permitida. Si cuentan, salta en la 6−K.

K (denegadas previas) Pruebas Primer bloqueo por límite, entre las permitidas Si NO cuentan Si SÍ cuentan
0 20 6 6 6
3 20 3 6 3
10 20 1 6 1

Las 40 pruebas con K mayor que cero siguieron el patrón 6−K sin excepción. Los mensajes de denegación lo confirman por otro camino: de las 260 llamadas denegadas por la regla de URL, 160 nombran solo esa política y 100 nombran las dos. Esas 100 son las denegadas número 6 a 10 de cada prueba con K=10 (cinco por prueba, veinte pruebas). Mientras una regla rechazaba las solicitudes, el límite las iba contando. Eso sí, no parsees esa lista de políticas determinantes: en los mensajes que nombran las dos, el orden salió 55 veces de una forma y 45 de la otra.

La causa no está documentada, así que esto es interpretación mía. La guía de Dogwood describe request como el único tipo de evento «de decisión»: ingerirlo es lo que dispara la autorización, mientras que response y error son solo historial. Eso es consistente con que el evento quede registrado antes de conocer el veredicto, pero la guía no lo dice de forma explícita.

El alcance es acotado: solo probé denegaciones de una regla Cedar sin estado, no la denegación por defecto cuando ninguna política aplica. La consecuencia práctica es incómoda. Con sesiones amplias por usuario, que la documentación lista como estrategia válida, un agente atascado reintentando contra una regla que lo rechaza podría agotar su propio límite y quedar bloqueado hasta que pase la ventana.

⚠️ ProTip #3: Si tu límite cuenta ::request, cada intento denegado cuenta. Según la documentación, response solo se registra cuando la acción termina con éxito, así que contar ::response debería contar solo lo que de verdad se ejecutó. No probé ese cambio, pero es lo primero que probaría.

Hallazgo 3: La Sesión Obsoleta No Llega Como 409

Según la documentación (sección Session invalidation), agregar o actualizar una política temporal invalida las sesiones activas del engine, y la siguiente solicitud que reutilice una sesión invalidada falla con HTTP 409 ConflictException. Para recuperarte, abres una sesión nueva. Probé cada disparador con tres sesiones vivas: esperé a que la política estuviera ACTIVE, agregué 10 segundos y llamé dos veces con cada sesión.

Por la ruta MCP del Gateway (tools/call) nunca recibí un 409. Las 42 observaciones (21 sesiones invalidadas, dos llamadas cada una) devolvieron lo mismo: HTTP 200 con este cuerpo JSON-RPC.

{"code": -32005, "message": "Tool Execution Denied: Policy session is stale. Policies were updated after the session started. Start a new session and retry."}
Disparador ¿Invalida sesiones vivas?
Crear política temporal Sí (3 de 3, en dos corridas)
Actualizar política temporal Sí (3 de 3)
Borrar política temporal Sí (3 de 3, en dos corridas)
update-policy sin cambio real (la política ya estaba ACTIVE) Sí (3 de 3, en dos corridas)
Crear o borrar una política Cedar sin estado No (0 de 3)
Gateway de ENFORCE a LOG_ONLY, y de vuelta No (0 de 3)

De la tabla salen varios comportamientos que la documentación no menciona. Solo invalidan las sesiones los cambios en políticas temporales; una política Cedar sin estado o un cambio de modo del Gateway no las afectan, aunque el blog de lanzamiento de AWS habla de cualquier cambio en las políticas del engine. Borrar una política temporal también invalida, aunque la documentación solo menciona agregar o actualizar. Una sesión obsoleta no se recupera sola: las 21 sesiones invalidadas siguieron obsoletas en la segunda llamada, mientras que una sesión nueva funcionó en las 7 de 7 pruebas donde el disparador sí invalidó. Y un update-policy sin cambio efectivo también invalida.

Ese último caso lo encontré por accidente. El disparador que había diseñado como «de LOG_ONLY a ACTIVE» terminó siendo un update-policy sobre una política que ya estaba ACTIVE, porque el paso anterior falló (lo cuento en los gotchas operativos).

De la documentación se deduce una consecuencia de diseño: una sesión nueva empieza con el historial vacío, así que desplegar una política temporal también reinicia los contadores de límite y las precondiciones de secuencia de todas las conversaciones vivas.

💡 ProTip #4: Trata el -32005 como una señal de control del flujo y no como un error para el usuario. Tu cliente debería capturarlo, abrir una sesión nueva y volver a establecer las precondiciones (por ejemplo, correr las pruebas otra vez). Si no lo manejas, cada reintento con el mismo ID va a fallar: en mis pruebas, ninguna sesión obsoleta se recuperó sola.

La detección tiene que mirar el cuerpo, porque el código HTTP es 200 tanto en la denegación (-32002) como en la sesión obsoleta (-32005). Este es el criterio que usó mi harness para clasificar las respuestas, con el caso de la sesión obsoleta agregado:

DENEGADO = -32002
SESION_OBSOLETA = -32005

def clasificar(cuerpo):
    err = cuerpo.get("error") if isinstance(cuerpo, dict) else None
    if isinstance(err, dict):
        if err.get("code") == SESION_OBSOLETA:
            return "STALE"  # sesión nueva, rehacer precondiciones, reintentar una vez
        if err.get("code") == DENEGADO:
            return "DENY"
        return "OTRO"
    return "TOOL_ERROR" if cuerpo.get("result", {}).get("isError") else "ALLOW"

La recuperación del comentario (sesión nueva, rehacer las precondiciones y un solo reintento) no forma parte del experimento: es lo que haría, no algo que medí.

Hallazgo 4: La Validación Estricta Contra el Patrón Documentado

Estos dos gotchas me costaron una tarde, y ambos vienen de la validación por defecto de las políticas (FAIL_ON_ANY_FINDINGS).

El permit simple que pide la documentación falla. Para que run_tests quede registrada como response, necesitas un permit sin condiciones para esa acción. Con la validación por defecto, la política pasa a CREATE_FAILED con dos motivos, uno por cada tipo de principal:

Overly Permissive: Policy Engine will allow every request for the specified principal (AgentCore::IamEntity), action (ToolsTarget___run_tests) and resource (arn:aws:bedrock-agentcore:us-east-1:<CUENTA>:gateway/<GATEWAY>) combination if the policy is added or updated

El segundo motivo es idéntico, pero con AgentCore::OAuthUser, aunque mi Gateway usa AWS_IAM. Lo resolví con --validation-mode IGNORE_ALL_FINDINGS solo para los permit simples, y mantuve la validación estricta en las políticas temporales.

Un forbid creado antes que su permit también falla. El límite de count que usé para medir latencia (con un umbral inalcanzable) falló dos veces con Overly Restrictive: Policy Engine will deny every request for the specified principal ..., primero con n > 100000 y después con n > 5000. Pasó al tercer intento cambiando una sola cosa: creé primero el permit de la acción y después el forbid. Lo vi en un único caso (count sobre read_secret), así que no lo generalizo.

Esto deja un problema operativo. El orden fail-closed, con el forbid primero para que nunca exista una ventana sin límite, es incompatible con la validación estricta. Si aplicas el permit y después el forbid, durante un instante la acción queda permitida sin su límite. En mi caso lo evité creando todo en un entorno sin tráfico. En producción vas a necesitar otra estrategia; una opción es crear las políticas en un engine nuevo antes de asociarlo al Gateway.

🚨 ProTip #5: Si automatizas la creación de políticas en un pipeline, prueba el orden de permit y forbid con la validación estricta antes de depender de él. Y deja escrito en tu runbook en qué orden vas a crearlas y qué ventana de exposición estás aceptando.

Hallazgo 5: La Evaluación Temporal Cuesta Unos 20 ms

La hipótesis era que count y sum se volverían más lentos con un historial grande, mientras que formerly se mantendría casi plano. Medí la latencia de evaluación temporal con el atributo aws.agentcore.policy.temporal.latency_ms de los spans de Transaction Search, en una sola sesión por operador con 1,000 llamadas consecutivas.

Operador Historial previo P50 P99
count ~10 llamadas 21 ms 26 ms
count ~100 21 ms 24 ms
count ~1,000 24 ms 27 ms
formerly ~10 21 ms 39 ms
formerly ~100 20 ms 25 ms
formerly ~1,000 20 ms 24 ms

Con el criterio que fijé antes de medir (refutada si la diferencia de P99 entre 10 y 1,000 llamadas es menor al 10%), la hipótesis queda refutada para count: la diferencia es de 3.8%. Las medianas por decil muestran algo más fino. count sube de 20 a 24 ms a lo largo de las 1,000 llamadas, unos 0.29 ms por cada 100, mientras que formerly se queda en 21 ms en los diez deciles. Hay crecimiento, pero es tan pequeño que no representa un problema operativo. El piso de unos 20 ms existe incluso con el historial casi vacío, y frente a los ~800 ms de la llamada completa (que incluye el Gateway y mi Lambda simulada) representa alrededor del 3%.

Hay que leer esta tabla con cuidado. Son 20 muestras por ventana, así que el P99 es prácticamente el máximo. La ventana de ~10 llamadas siempre corresponde a las primeras 20 de la corrida, de modo que el tamaño del historial y el calentamiento están mezclados; los 39 ms de formerly probablemente vienen de ahí. Además conté llamadas y no eventos: cada llamada deja al menos un request y un response, así que el historial real es mayor que el nominal. Una llamada de formerly agotó el tiempo de espera (la n=640).

El ID de Sesión Lo Debe Elegir Tu Backend

La documentación ya cubre este punto; aquí solo lo cuantifico. Un límite por sesión no sirve contra quien elige su propio session ID. Con 20 pruebas por condición: con el ID fijo, el bloqueo llegó en la llamada 6 en 20 de 20; rotando el ID en cada llamada, 0 de 20 pruebas se bloquearon; y con el ID derivado en un backend mediante un HMAC del usuario, otra vez 20 de 20, con aislamiento correcto entre usuarios en 20 de 20.

La documentación también advierte que usar un ID nuevo en cada solicitud desactiva en la práctica las políticas temporales.

La derivación que usé es una línea de HMAC con una clave que solo conoce el backend:

import hashlib, hmac, secrets

class BackendSesion:
    def __init__(self):
        self._clave = secrets.token_bytes(32)  # en producción, desde Secrets Manager

    def session_id_para(self, usuario):
        return hmac.new(self._clave, usuario.encode("utf-8"), hashlib.sha256).hexdigest()

El resultado son 64 caracteres hexadecimales, dentro del formato que acepta el header (1 a 128 caracteres [A-Za-z0-9-]). En el experimento derivé el ID solo del usuario, lo que da una sesión amplia por usuario. Si quieres sesiones por conversación, agrega el ID de la conversación al mensaje del HMAC.

🔧 ProTip #6: Genera el session ID en tu backend a partir del usuario y la conversación (un HMAC alcanza), nunca en el agente. Un límite por sesión que el agente puede reiniciar con un UUID nuevo no protege nada.

Gotchas Operativos

Estos no son propios de las Temporal Policies, pero te los vas a encontrar si repites el experimento.

Para medir latencia necesitas Transaction Search, y el destino de segmentos de X-Ray pasa por un estado PENDING. Mientras tanto, CreateDelivery falla con X-Ray Delivery Destination is supported with CloudWatch Logs as a Trace Segment Destination y UpdateTraceSegmentDestination con Updates are not allowed while the current status is PENDING. Espera a que llegue a ACTIVE. Cada span de política trae un aws.request.id igual al x-amzn-RequestId de la respuesta, y con eso puedes unir spans y llamadas.

Pasar una política temporal de ACTIVE a LOG_ONLY falló con UPDATE_FAILED en las dos corridas. En la segunda, el motivo fue Policy references an action that does not exist on the target gateway. Verify the target and tool names, then try again. La acción sí existe en el Gateway.

El llamador firma con SigV4 usando el servicio bedrock-agentcore, un detalle que la documentación de autenticación de entrada no menciona.

Qué Garantiza una Temporal Policy y Qué No

Lo que observé que garantiza Lo que no te garantiza
Deniega deploy si no hubo un run_tests completado Que tu agente espere: paralelizó en las 10 sesiones y el deploy fue denegado en las 10
Bloquea la llamada 6 de un límite por sesión (20 de 20) Un límite si el agente elige su ID (0 de 20 bloqueos con ID rotado)
Cuenta las solicitudes denegadas en un límite ::request Que cuente solo lo ejecutado, salvo que cuentes ::response (no probado)
Invalida sesiones vivas al cambiar políticas temporales Un 409 (llega un -32005), ni que la sesión nueva conserve el historial
Evalúa en 20 a 27 ms con historial de hasta ~1,000 llamadas Que esas cifras apliquen a sum, since, otro modelo, otra región u otra hora del día

Límites del Experimento

La prueba con agente usó una sola tarea explícita, un solo modelo y 5 sesiones por variante. Todo corrió en un solo día (entre las 10:54 y las 17:50, hora de Costa Rica), en una región, una cuenta, un Gateway y con una Lambda simulada, y las 20 muestras por ventana de la prueba de latencia son pocas.

Esto es lo que quedó sin probar:

  • Cómo reacciona un agente Strands con reintentos por defecto ante un -32005, y si el reinicio de contadores que se deduce de la documentación se ve igual con un agente real.
  • Contar ::response en lugar de ::request, y la denegación por defecto cuando ninguna política aplica.
  • Si el 409 ConflictException existe en otra capa, como el plano de control.
  • El paso de LOG_ONLY a ENFORCE a nivel de política. No encontré la causa del UPDATE_FAILED; crear la política directamente en LOG_ONLY queda pendiente.
  • Una estrategia para crear permit y forbid en producción sin ventana de exposición.
  • Los operadores sum y since, y la autenticación CUSTOM_JWT.
  • Qué pasa cuando el agente corre en AgentCore Runtime y envía su propio header de sesión: si prevalece el session ID que viaja en el token de identidad (WAT) o el del agente. La documentación no lo dice y no llegué a probarlo.
  • Un Gateway sin autenticación, donde según la documentación todos los llamadores con el mismo ID comparten el historial.
  • Un backend desplegado para el session ID. El mío fue un módulo local y todos los usuarios simulados compartían la misma identidad IAM, así que evalué la lógica de derivación, no un servicio.

Esos son los huecos que más me gustaría cerrar.

Conclusión

El experimento se resume en tres decisiones de diseño. Serializa en el cliente las acciones que dependen unas de otras, porque tu agente puede paralelizar sin que se lo pidas: el mío lo hizo en las 10 sesiones. Genera el session ID en tu backend, porque un límite que el agente puede reiniciar no limita nada. Y maneja el -32005, porque cada vez que despliegues una política temporal vas a invalidar conversaciones vivas, y la sesión nueva arranca sin el historial que tenía la anterior.

El servicio se comportó casi siempre como describe la documentación. La sorpresa más grande fue el agente, que no se comportó como yo suponía. Las demás estuvieron en los bordes: qué cuenta como evento, qué código llega cuando una sesión queda obsoleta, en qué orden acepta la validación tus políticas. Esos bordes son los que conviene probar antes de producción.

🚀 ProTip Final: Antes de confiar en una política temporal para proteger algo importante, mídela con tu propio agente, tu propio modelo y tu propia estrategia de sesiones. En mi experimento, la sorpresa más grande vino del agente, no de la política.

Si vienes del artículo de AgentCore Policy, las Temporal Policies son la pieza que le faltaba: Cedar decide sobre una solicitud; Dogwood, sobre la historia de la sesión. Y si te interesa el método, el A/B test del Optimization Loop lo aplica con un resultado negativo incluido.


¿Ya estás usando Temporal Policies, o sigues controlando secuencias y límites dentro del código del agente? ¿Cómo estás generando el session ID? Si replicaste alguna de estas pruebas o mediste lo que yo dejé fuera, me interesa mucho comparar — los comentarios están abiertos.

¡Nos vemos en el próximo artículo! 🚀


Recursos Oficiales 📚

Preguntas frecuentes

¿Por qué un agente que llama a run_tests y deploy en paralelo recibe una denegación con una política de secuencia?

Porque el Gateway evalúa deploy en cuanto llega, y el evento response de run_tests solo existe cuando esa herramienta termina. Con las dos llamadas enviadas a la vez, 98 de 100 deploy fueron denegados. Cuando el cliente esperó la respuesta antes de enviar la siguiente solicitud (un hueco mediano de unos 15 ms), no hubo ninguna denegación en 600 sesiones. En diez sesiones con un agente Strands, ambas llamadas salieron en el mismo turno y el deploy fue denegado en las diez, con N=5 por variante y una tarea explícita.

¿Las solicitudes denegadas cuentan para un límite de Temporal Policies en AgentCore Policy?

En mi prueba, sí. Con un límite que bloquea la sexta llamada a una herramienta en 10 minutos y K solicitudes previas denegadas por una regla Cedar sin estado, el bloqueo llegó en la llamada 6−K en 40 de 40 pruebas con K mayor que cero. La documentación dice que el evento request se registra para solicitudes autorizadas, así que el resultado contradice una lectura literal. Solo probé denegaciones por una regla Cedar sin estado, no la denegación por defecto cuando ninguna política aplica, y el mecanismo exacto no está documentado.

¿Qué devuelve AgentCore Gateway cuando una sesión de Temporal Policies queda obsoleta?

Por la ruta MCP del Gateway (tools/call) observé HTTP 200 con un error JSON-RPC de código -32005 y el mensaje ‘Tool Execution Denied: Policy session is stale. Policies were updated after the session started. Start a new session and retry.’, en las 42 observaciones. La documentación habla de HTTP 409 ConflictException; ese código puede existir en otra capa que no verifiqué. Solo crear, actualizar o borrar políticas temporales invalidó sesiones. Una política Cedar sin estado y el cambio de modo del Gateway no lo hicieron.

¿Cuánta latencia agrega la evaluación de Temporal Policies?

En una sola sesión por operador, la latencia de evaluación temporal medida en los spans fue de unos 20 a 27 ms. Con el operador count, la mediana pasó de 21 ms con unas 10 llamadas previas a 24 ms con unas 1,000, aproximadamente 0.29 ms por cada 100 llamadas. Con formerly se mantuvo plana en 20-21 ms. La llamada completa de extremo a extremo tardó alrededor de 800 ms, incluyendo el Gateway y una Lambda simulada. Son ventanas de unas 20 muestras, así que el P99 es casi el máximo.

Portada de AgentCore in Production, de Gerardo Arroyo

Hay un libro detrás de esto.

AgentCore in Production son 229 páginas sobre llevar agentes Bedrock de demo a producción: políticas Cedar, una arquitectura de referencia de once componentes y el playbook operativo del día uno.

Kindle + tapa blanda · 12 capítulos · 229 páginas

Conseguirlo en Amazon →
Escrito por

Gerardo Arroyo Arce

Arquitecto de Soluciones y autor de AgentCore in Production: The Operator’s Playbook for AWS Bedrock Agents — 229 páginas sobre llevar agentes Bedrock de demo a producción. AWS Golden Jacket con pasión por compartir conocimiento. Como miembro activo de AWS Community Builders, ex-AWS Ambassador y AWS User Group Leader, me dedico a construir puentes entre la tecnología y las personas. Desarrollador Java de corazón y consultor independiente, llevo la arquitectura cloud más allá de la teoría a través de conferencias internacionales y soluciones del mundo real. Mi curiosidad insaciable por aprender y compartir me mantiene en constante evolución junto a la comunidad tech.

Inicia la conversación