AWS Builder Center
Guardrails más allá del modelo: cierra la brecha que tu agente deja abierta

Guardrails más allá del modelo: cierra la brecha que tu agente deja abierta

Construiste un agente genial y lo pusiste en producción. Hasta hiciste la tarea de seguridad y le pusiste un guardrail al modelo para que valide lo que entra y lo que sale, así que te fuiste a dormir sin preocupaciones. Y aun así quedó una puerta abierta que ese guardrail no alcanza a cubrir y en este artículo quiero contarte cómo cerrarla.

Construiste un agente genial y lo pusiste en producción. Hasta hiciste la tarea de seguridad y le pusiste un guardrail al modelo para que valide lo que entra y lo que sale, así que te fuiste a dormir sin preocupaciones. Y aun así quedó una puerta abierta que ese guardrail no alcanza a cubrir.
Lo que pasa es que un agente hace mucho más que llamar al modelo: también llama herramientas, trae datos de afuera y habla con servidores MCP. Y el daño de verdad casi nunca lo hace el texto, lo hace la acción, esa herramienta que borra un registro, que transfiere plata o que devuelve datos que no debía. El problema es que entre el momento en que el modelo decide "llama a esta herramienta con estos parámetros" y el momento en que la herramienta se ejecuta, no hay nada revisando. Justo ahí está la brecha de la que quiero hablarte.
En este artículo quiero contarte cómo cerrarla. La idea es extender la cobertura del guardrail desde el límite del modelo hasta el límite de la herramienta, con tres checkpoints de validación construidos con los hooks del ciclo de vida de Strands y la API ApplyGuardrail de Bedrock. Sin tocar la lógica de tus herramientas ni la de tu agente. Pero antes de llegar al código conviene asentar dos cosas: qué es exactamente un guardrail y hasta dónde llega.
Este artículo asume que ya construiste un agente con Strands. Si todavía no, empieza por Tu primer agente de IA con Strands Agents . Aquí lo que me importa es protegerlo.

Qué es un guardrail y hasta dónde llega

Un guardrail en un sistema de IA es un control que se interpone entre una entrada no confiable y una acción sensible: una capa que inspecciona lo que entra y lo que sale del modelo, y bloquea o modifica lo que viola una política. Es la misma idea de un WAF que filtra tráfico HTTP antes de que llegue a tu app, solo que el guardrail lo hace con lenguaje natural.
El problema que resuelve aparece en cuanto pones un modelo en producción. Un modelo de lenguaje responde a lo que le pidas, y con la instrucción correcta puedes sacarlo de su comportamiento esperado: que suelte PII, que genere contenido tóxico, que hable de temas que no debería o que siga una instrucción inyectada. El modelo por sí solo no distingue una petición legítima de un ataque, mientras que el guardrail sí lo hace porque evalúa todo contra una política explícita que tú defines.
Amazon Bedrock Guardrails  es la implementación gestionada de esa capa en AWS. Le defines políticas (detección de PII, filtros de contenido para toxicidad y odio, temas prohibidos, filtros de palabras) y Bedrock las aplica en cada invocación del modelo: valida el prompt antes de inferir y la respuesta después. Hasta puedes hacerlo obligatorio por cuenta con una política de IAM , de modo que ninguna llamada al modelo quede sin chequear.
Ese es justo el alcance del guardrail del modelo, y también su límite: cubre lo que entra y sale del modelo. Lo que pasa entre esos dos puntos, cuando el agente ejecuta herramientas, queda fuera. Ahí es donde empieza la brecha que mencioné arriba, y vale la pena verla con detalle.

El límite del modelo no es el único límite

Un agente no es solo el modelo, es el modelo más las herramientas que ejecuta, los datos que trae de afuera y los otros sistemas con los que habla, y todos esos son límites de confianza que el guardrail del modelo nunca llega a ver. Míralo en el flujo de una sola petición:
El prompt entra validado y la respuesta final sale validada, pero en medio el agente decide llamar una herramienta, la ejecuta con parámetros que el modelo eligió, y recibe de vuelta datos que vienen de afuera. Nada de eso cruza el límite del modelo. Estos son los puntos que quedan sin cubrir:
  • Los parámetros que el modelo decide pasarle a una herramienta.
  • Los resultados que devuelve una herramienta.
  • Los datos que llegan de un servidor MCP o de un pipeline de RAG.
  • El tráfico entre agentes en un sistema multi-agente.
Si en el camino se coló un dato con PII o una instrucción maliciosa, ahí no hay guardrail que lo vea. Por eso la validación en el límite de la herramienta no reemplaza a la del modelo, la complementa: son dos capas que cubren dos superficies distintas.

Tres checkpoints para cerrar la brecha

La idea es poner un chequeo en cada frontera donde los datos entran o salen de tu agente. Son tres, y cada uno usa un hook distinto del ciclo de vida de Strands.
CheckpointQué validaHookQué hace si marca
1. EntradaLo que llega al modelo (usuario, otros agentes, MCP, RAG)BeforeInvocationEventReemplaza el mensaje antes de que el modelo lo vea
2. HerramientaLos parámetros que el modelo le pasa a la herramientaBeforeToolCallEventCancela la ejecución de la herramienta
3. SalidaEl resultado que devuelve la herramientaAfterToolCallEventReemplaza el resultado por un mensaje de bloqueo
El checkpoint 2 es el que de verdad cierra la brecha: el modelo ya decidió qué mandar, pero nadie verificó si eso es seguro de ejecutar. El checkpoint 3 es clave para herramientas que traen contenido externo, como una búsqueda web que trae páginas de sitios que no controlas.
No todos tienen que pegar igual de fuerte. En el checkpoint 1 puedes usar un guardrail completo (PII, filtro de contenido, temas prohibidos). El checkpoint 2 puede ser más liviano, un guardrail específico de esa herramienta o checks locales de regex y schema. El checkpoint 3 se enfoca en contenido no deseado de los datos externos. Mezclar chequeos deterministas rápidos (regex, schema, allowlist) con evaluaciones de guardrail por IA te deja bajar la latencia donde no necesitas la artillería pesada.
Así se ve el agente completo con los tres checkpoints en su lugar:

El GuardrailHook: una clase, tres checkpoints

Todo esto vive en una sola clase que implementa HookProvider de Strands. Registra los tres callbacks, uno por evento del ciclo de vida, y los tres comparten un método _check que llama a ApplyGuardrail.
Empecemos por lo esencial. Primero los imports y el constructor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import boto3
from strands.hooks import HookProvider, HookRegistry
from strands.hooks.events import (
BeforeInvocationEvent,
BeforeToolCallEvent,
AfterToolCallEvent,
)

class GuardrailHook(HookProvider):

def __init__(self, guardrail_id, guardrail_version, region_name, tool_names=None):
self.client = boto3.client("bedrock-runtime", region_name=region_name)
self.guardrail_id = guardrail_id
self.guardrail_version = guardrail_version
self.tool_names = tool_names # None = aplica a todas las herramientas
Ese tool_names opcional es importante y vuelve más adelante: te deja acotar un hook a herramientas específicas, o pasar None para aplicarlo a todas.
El registro de los callbacks es el corazón de la clase. Aquí es donde conectas cada checkpoint con su evento:
1
2
3
4
def register_hooks(self, registry: HookRegistry, **kwargs):
registry.add_callback(BeforeInvocationEvent, self.validate_inbound) # Checkpoint 1
registry.add_callback(BeforeToolCallEvent, self.validate_input) # Checkpoint 2
registry.add_callback(AfterToolCallEvent, self.validate_output) # Checkpoint 3
Y el método que hace el trabajo de verdad, el que llama a Bedrock. Le pasas el contenido y te dice si es seguro:
1
2
3
4
5
6
7
8
9
def _check(self, content, source="INPUT"):
"""Llama a ApplyGuardrail. Devuelve True si el contenido es seguro."""
response = self.client.apply_guardrail(
guardrailIdentifier=self.guardrail_id,
guardrailVersion=self.guardrail_version,
source=source, # "INPUT" aplica políticas de entrada; "OUTPUT" las de salida
content=[{"text": {"text": content}}],
)
return response["action"] != "GUARDRAIL_INTERVENED"
Cuando el guardrail interviene, la respuesta trae action == "GUARDRAIL_INTERVENED", así que _check devuelve False y el callback que lo llamó sabe que tiene que bloquear.
Ahora sí, los tres callbacks. Cada uno hace su chequeo y bloquea a su manera.
El checkpoint 1 revisa el último mensaje del usuario antes de que llegue al modelo. Si no pasa, limpia los mensajes y mete uno de bloqueo:
1
2
3
4
5
6
7
8
9
10
11
12
13
async def validate_inbound(self, event: BeforeInvocationEvent):
for msg in reversed(event.messages):
if msg.get("role") == "user":
for block in msg.get("content", []):
text = block.get("text", "")
if text and not self._check(text):
event.messages.clear()
event.messages.append({
"role": "user",
"content": [{"text": "Request blocked by safety guardrail."}],
})
return
break
El checkpoint 2 revisa los parámetros que el modelo quiere pasarle a la herramienta. Si alguno no pasa, setea event.cancel_tool y la herramienta nunca corre:
1
2
3
4
5
6
7
8
async def validate_input(self, event: BeforeToolCallEvent):
if self.tool_names and event.tool_use.get("name") not in self.tool_names:
return
tool_input = event.tool_use.get("input", {})
for param_value in tool_input.values():
if isinstance(param_value, str) and not self._check(param_value):
event.cancel_tool = "This request was blocked by a safety guardrail."
return
El checkpoint 3 revisa el resultado de la herramienta con source="OUTPUT". Si trae contenido no deseado, reemplaza el resultado por un error antes de que vuelva al modelo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async def validate_output(self, event: AfterToolCallEvent):
if self.tool_names and event.tool_use.get("name") not in self.tool_names:
return
content_parts = [
block["text"]
for block in event.result.get("content", [])
if "text" in block
]
content = "\n".join(content_parts)
if content and not self._check(content, source="OUTPUT"):
event.result = {
"toolUseId": event.result["toolUseId"],
"status": "error",
"content": [{"text": "Content blocked by safety guardrail."}],
}
Fíjate en el patrón de los checkpoints 2 y 3: la primera línea de cada uno se salta el chequeo si el nombre de la herramienta no está en tool_names. Eso es lo que te deja tener guardrails distintos por herramienta, que es lo que vemos ahora.

Registrar el hook: uno para todas, o uno por herramienta

Registrar el hook no toca ni tus herramientas ni tu agente. Le pasas la instancia en el parámetro hooks=[...] del Agent y listo, los callbacks corren solos en cada evento.
Para un solo guardrail que aplique a todo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from strands import Agent
from strands.models import BedrockModel
from guardrail_hook import GuardrailHook
from tools import web_search, get_customer_data

model = BedrockModel(model_id="us.amazon.nova-pro-v1:0", region_name="us-east-2")

guardrail_hook = GuardrailHook(
guardrail_id="tu-guardrail-id", # cópialo de la consola de Bedrock > Guardrails
guardrail_version="1", # usa "DRAFT" para pruebas
region_name="us-east-2",
)

agent = Agent(
model=model,
tools=[web_search, get_customer_data],
system_prompt="You are a helpful assistant.",
hooks=[guardrail_hook],
)
Pero no todas las herramientas corren el mismo riesgo. Una búsqueda web trae contenido de sitios que no controlas, así que quieres un filtro de salida estricto. Una herramienta que devuelve datos de clientes necesita más bien detección de PII. Para eso registras un hook por guardrail, cada uno con su tool_names:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
web_search_hook = GuardrailHook(
guardrail_id="gr-websearch-id", guardrail_version="1",
region_name="us-east-2", tool_names={"web_search"},
)
customer_data_hook = GuardrailHook(
guardrail_id="gr-customerdata-id", guardrail_version="1",
region_name="us-east-2", tool_names={"get_customer_data"},
)

agent = Agent(
model=model,
tools=[web_search, get_customer_data],
system_prompt="You are a helpful assistant.",
hooks=[web_search_hook, customer_data_hook],
)
Strands corre todos los hooks en cada evento, pero cada uno se saltea si el nombre de la herramienta no coincide con su tool_names. Así igualas la rigurosidad al riesgo de cada herramienta, en vez de meter una sola política para todo.

El ángulo de seguridad: default-deny y menor privilegio

Los tres checkpoints son el mecanismo, pero la postura detrás importa igual. Un par de principios que vale la pena tener presentes:
  • Trata toda entrada externa como no confiable: el prompt del usuario, el RAG, la salida de las herramientas, lo que venga de MCP. Si entró de afuera, se valida.
  • Separa las herramientas de lectura de las de escritura, y pide aprobación humana para las acciones destructivas. El daño lo hace la acción, así que la acción destructiva es la que más control necesita.
  • Mantén una allowlist de acciones, la menor cantidad de herramientas posible y permisos mínimos por herramienta.
  • Audita cada decisión: quién pidió qué, qué se permitió y qué se negó. Tu equipo de seguridad lo va a agradecer.
Y algo que me gusta de este diseño: el GuardrailHook es un HookProvider standalone. Lo construyes una vez y lo publicas como librería interna. El mismo paquete sirve para varios agentes, se despliega igual en Lambda, en ECS o en Amazon Bedrock AgentCore Runtime , y le inyectas el guardrail ID por entorno (dev, staging, prod) desde la configuración. Puedes cambiar la política o sumar checks de regex sin tocar el código del agente.

La demo: los tres checkpoints, uno por uno

El repo trae una demo reproducible con dos herramientas, web_search y get_customer_data, y dos agentes: uno sin el hook y otro con él. El agente seguro recibe un argumento (1, 2 o 3) para que puedas disparar cada checkpoint por separado y ver cuál interviene.
El ataque base es una inyección indirecta: un documento externo que la herramienta web_search "trae", y que adentro esconde una instrucción que le pide al agente llamar a get_customer_data y pegar el email y la tarjeta del cliente al final de la respuesta. El payload no viene del usuario, viene del contenido que la herramienta trajo de afuera. Por eso el guardrail del modelo no lo atrapa.
Cada corrida del agente seguro usa un prompt pensado para un checkpoint distinto:
1
2
3
python agente_seguro.py 1 # Checkpoint 1: bloquea la entrada del usuario
python agente_seguro.py 2 # Checkpoint 2: bloquea los parámetros de get_customer_data
python agente_seguro.py 3 # Checkpoint 3: bloquea la salida de web_search
  • Checkpoint 1 (entrada). El prompt del usuario pide contenido de odio y violencia. El content filter lo bloquea en validate_inbound antes de que el modelo lo vea.
  • Checkpoint 2 (parámetros). El prompt mete PII (un email y una tarjeta) como customer_id. Cuando el modelo intenta llamar a get_customer_data con eso, el guardrail de PII cancela la llamada en validate_input.
  • Checkpoint 3 (salida). El prompt es inocente; el ataque vive en el contenido que trae web_search. El guardrail valida ese resultado en validate_output y lo reemplaza por un mensaje de bloqueo antes de llegar al modelo.
En cada caso, el hook imprime en consola una línea [GUARDRAIL] Checkpoint N que te dice cuál intervino, y la respuesta final nunca contiene la PII. El agente sin el hook, en cambio, devuelve el email y la tarjeta: el ataque funciona.
No voy a mentir, la primera vez que corrí el agente sin guardrails me quedé mirando la pantalla un rato. Yo le había pedido algo totalmente inocente, y aun así el agente terminó entregando datos que no debía, solo porque un documento de afuera se lo dijo. Ver el ataque funcionar con mis propias manos fue lo que me convenció de que el límite del modelo no alcanza.

Cierre

Bedrock Guardrails cubre el límite del modelo, y eso está muy bien, pero tu agente también llama herramientas y trae datos de afuera que nunca pasan por ese chequeo. Los tres checkpoints cierran esa brecha: BeforeInvocationEvent para la entrada, BeforeToolCallEvent para los parámetros de la herramienta, AfterToolCallEvent para la salida. Un solo GuardrailHook te sirve para un guardrail compartido o para guardrails distintos por herramienta, y se despliega igual desde tu máquina hasta producción.
Si construiste algo que la gente usa, protege también a la gente que lo usa. Te dejo el repo ekoparty-guardrails-demo  con la demo completa para que la clones y la corras. Si pruebas esto en tu propio agente y te topas con algo raro, cuéntame en los comentarios. Nos vemos en la nube. ☁️

Recursos

Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article