Después del guardrail que me salvó la infra: así quedó mi arquitectura de agentes autónomos en producción
¿Por qué asumimos que los agentes autónomos van a fallar de forma contenida? Hace un tiempo que me hago esa pregunta, pero no fue académica hasta que uno de mis agentes estuvo a punto de destruir mi infra en Railway. Lo que vino después —el rediseño, la arquitectura de permisos, la capa de observabilidad que agregué desde cero— es lo que no suele aparecer en los threads de Twitter que celebran "el agente que hizo todo solo".
Esto es el día después. La resaca del incidente. Lo que queda cuando el guardrail frena el caos y vos tenés que construir algo que no vuelva a fallar de la misma manera.
Arquitectura de agentes autónomos en producción: qué rompí y qué reconstruí
El incidente original lo documenté en el post de guardrails. No voy a repetir el relato completo, pero el resumen operativo es este: un agente con acceso write a mi API de Railway ejecutó una secuencia de pasos válidos individualmente que, en combinación, casi vacía un volumen de Postgres en producción. El guardrail lo frenó. Me quedé mirando el log con el corazón en la boca.
Lo que me molestó no fue que fallara. Es que yo había asumido que el scope de permisos era suficiente. Tenía el agente limitado a ciertos endpoints. Lo que no había modelado es que la combinación de endpoints válidos podía producir efectos destructivos.
Eso me obligó a pensar diferente. No en permisos planos —"este agente puede hacer X"— sino en grafos de permisos con contexto temporal y secuencia.
Mi arquitectura antes del incidente se parecía a esto:
Agente → API Gateway → Servicios
(auth token) (sin contexto de secuencia)
Limpia. Simple. Equivocada.
El grafo de permisos que construí post-incidente
Lo primero que hice después de respirar fue dibujar el grafo real de lo que el agente podía hacer. No lo que yo creía que podía hacer: lo que efectivamente podía ejecutar dado el token y los endpoints expuestos.
El resultado fue incómodo. Había 14 caminos posibles desde "listar volúmenes" hasta "operación destructiva irreversible", y yo solo había bloqueado 3.
Rediseñé con tres capas:
Capa 1: Permisos atómicos con intención declarada
// Antes: el agente tenía un token con scope "read:volumes write:volumes"
// Después: cada acción declara intención y contexto
interface AccionAgente {
tipo: 'lectura' | 'escritura' | 'eliminacion';
recurso: string;
intencion: string; // descripción legible de por qué
reversible: boolean;
requiereConfirmacion: boolean;
}
const accionPermitida = (accion: AccionAgente, contexto: ContextoEjecucion): boolean => {
// Una acción de escritura después de dos lecturas sobre el mismo recurso
// en la misma sesión dispara revisión manual obligatoria
if (accion.tipo === 'escritura' && contexto.lecturasRecientes.includes(accion.recurso)) {
if (contexto.accionesEnSesion > 3) return false; // corte duro
}
// Eliminaciones nunca son automáticas, sin excepción
if (accion.tipo === 'eliminacion' && !accion.requiereConfirmacion) return false;
return true;
};
Capa 2: Estado de sesión con ventana deslizante
// El agente no solo tiene permisos: tiene un presupuesto de acciones por ventana
interface PresupuestoSesion {
accionesTotales: number; // máx 20 por sesión
escrituras: number; // máx 5 por sesión
accionesCriticas: number; // máx 1 por sesión (requieren aprobación)
ventanaMinutos: number; // 30 minutos por defecto
ultimaAccion: Date;
}
// Si el agente llega a 80% del presupuesto, entra en modo solo-lectura
// Si llega al 100%, la sesión se cierra y loguea para revisión
Capa 3: Grafo de transiciones prohibidas
Esto es lo que más tardé en modelar y lo que más me cambió la forma de pensar. No alcanza con bloquear acciones individuales: hay que bloquear secuencias.
// Transiciones que nunca pueden ocurrir en secuencia directa
const TRANSICIONES_PROHIBIDAS = [
['listar_volumenes', 'desmontar_volumen'], // demasiado directo
['escalar_servicio', 'modificar_env_produccion'], // combinación destructiva
['rotar_secretos', 'reiniciar_servicio'], // sin pausa de verificación
] as const;
const validarSecuencia = (historial: string[], accionSiguiente: string): boolean => {
const ultimaAccion = historial[historial.length - 1];
const estaProhibida = TRANSICIONES_PROHIBIDAS.some(
([desde, hacia]) => desde === ultimaAccion && hacia === accionSiguiente
);
if (estaProhibida) {
logger.warn(`Transición prohibida detectada: ${ultimaAccion} → ${accionSiguiente}`);
return false;
}
return true;
};
Este patrón de transiciones prohibidas es lo que hubiera frenado el incidente original antes de que llegara al guardrail de último recurso. El guardrail es una red de seguridad; esto es el andamio que debería haber estado desde el principio.
La capa de observabilidad que agregué desde cero
Antes del incidente tenía logs. Después del incidente tengo trazabilidad de intención.
La diferencia es sutil pero fundamental. Un log dice "el agente ejecutó DELETE /volumes/xyz a las 23:47". La trazabilidad de intención dice "el agente declaró que iba a 'limpiar volúmenes huérfanos', ejecutó estas 7 acciones en secuencia, y la acción 5 desvió de la intención declarada en un 40%".
Eso es lo que implementé:
interface TraceAgente {
sesionId: string;
intencionDeclarada: string; // lo que el agente dijo que iba a hacer
accionesEjecutadas: AccionTrace[];
desviacionDeIntencion: number; // 0-100, calculado por similitud semántica
alertasGeneradas: string[];
tiempoTotal: number;
estadoFinal: 'completado' | 'bloqueado' | 'cancelado' | 'error';
}
interface AccionTrace {
timestamp: Date;
accion: string;
parametros: Record<string, unknown>;
resultado: 'exito' | 'bloqueado' | 'error';
tokensCosto?: number; // si la acción involucra llamada a LLM
latenciaMs: number;
}
Esto corre en Postgres (el mismo stack que ya tenía documentado en el post de Docker Compose en producción) y me da una tabla de sesiones de agentes que puedo auditar. No es glamoroso. Es una tabla SQL con índices. Pero en las dos semanas que lleva corriendo, ya me detectó tres sesiones donde el agente se desvió de la intención declarada antes de que llegara a hacer algo dañino.
Los números concretos de esas dos semanas:
- 47 sesiones de agente ejecutadas
- 3 sesiones bloqueadas por desviación de intención > 60%
- 1 sesión cancelada por presupuesto agotado
- 0 incidentes en producción
Ese 0 me importa. Pero también me importa que el sistema generó 11 alertas que revisé manualmente y en 4 casos el agente tenía razón y yo estaba siendo demasiado conservador. Afinar esos umbrales es trabajo de semanas.
Los errores que cometí al rediseñar (para que no los repitas)
Error 1: Modelé los permisos como si el agente fuera un humano
Cuando diseñé los permisos, pensé en términos de "qué haría un dev humano con estos accesos". El agente no es un humano. Puede ejecutar 20 acciones en 8 segundos sin fatiga, sin duda, sin el freno intuitivo de "espera, esto no se siente bien". El modelo mental tiene que cambiar.
Error 2: Confundí observabilidad con logging
Tenía Datadog, tenía logs estructurados. Pensé que eso era observabilidad. No lo es, al menos no para agentes. Observabilidad de agentes requiere entender la intención y medir la distancia entre lo que el agente dijo que haría y lo que efectivamente hizo. Sin esa dimensión, los logs son un registro de daño, no una herramienta de prevención.
Este tema conecta con algo que ya exploré al diagnosticar deadlocks en producción: el problema no era que no tuviera datos. Era que los datos que tenía no me mostraban el estado del sistema en el momento relevante. Con agentes es igual.
Error 3: Asumí que el contexto del agente era estable
Un agente que ejecuta 15 pasos no tiene el mismo "entendimiento" en el paso 1 que en el paso 15. El contexto acumulado cambia su comportamiento. Yo diseñé los permisos para el agente en el paso 1, no para el agente que ya procesó 14 acciones y tiene el contexto completo de la sesión. Esa asimetría es peligrosa.
Ahora tengo ventanas de contexto con decay: las acciones más viejas de la sesión pierden peso en el cálculo de "qué tan alineado está el agente con su intención declarada". No es perfecto, pero es más honesto que asumir que el contexto es lineal.
Error 4: No modelé el costo de los falsos positivos
El primer sistema que desplegué era tan conservador que bloqueaba al agente cada tres acciones. Terminé apagándolo después de un día porque generaba más fricción que valor. La seguridad que genera fricción excesiva se desactiva. Eso también es un fallo de seguridad, solo que más lento.
Relacionado con lo que encontré al simular supply chain attacks sobre dependencias: la protección que duele demasiado se saca. Hay que calibrar para que el costo del guardrail sea menor que el costo del incidente que previene.
FAQ: Arquitectura de agentes autónomos en producción
¿Qué es un grafo de permisos para agentes y por qué es mejor que permisos planos?
Un grafo de permisos modela no solo qué puede hacer un agente, sino en qué secuencia y bajo qué condiciones de contexto. Los permisos planos dicen "puede leer y escribir". El grafo dice "puede escribir, pero solo si no leyó el mismo recurso más de dos veces en la última ventana, y solo si la intención declarada incluye una operación de escritura". La diferencia es la dimensión temporal y secuencial. Para agentes autónomos que ejecutan cadenas largas de acciones, es la diferencia entre un sistema contenible y uno que falla de formas que no anticipaste.
¿Cuánto agrega esto en latencia por acción del agente?
En mi stack, la validación de permisos + chequeo de secuencia + actualización de estado de sesión agrega entre 8 y 23ms por acción, dependiendo de si hay que consultar el historial completo de la sesión. Para la mayoría de los casos de uso, es aceptable. Si el agente está haciendo acciones que tardan segundos (llamadas a APIs externas, generación con LLM), 20ms es ruido. Si está haciendo operaciones de lectura en memoria que tardan microsegundos, ahí sí tenés que pensar si el overhead vale la pena.
¿Qué hago si el agente necesita hacer una transición que tengo prohibida pero por una razón legítima?
En mi arquitectura, las transiciones prohibidas se pueden desbloquear con aprobación explícita fuera de banda. El agente no puede auto-aprobarse; tiene que emitir una solicitud de desbloqueo que llega a una cola que yo reviso. En la práctica, pasó tres veces en dos semanas y las tres veces el agente tenía razón. Eso me dice que algunas de mis transiciones prohibidas son demasiado restrictivas. Estoy iterando. La alternativa —que el agente se auto-apruebe— no la considero.
¿Cómo medís la "desviación de intención" del agente?
Calculo similitud semántica entre la intención declarada al inicio de la sesión y una descripción textual de las acciones ejecutadas hasta el momento. Uso embeddings con un modelo liviano (no Claude para esto, el costo no escala). Si la similitud baja de un umbral, entra en modo de revisión. El umbral lo calibré empíricamente durante las primeras dos semanas: empecé en 70%, lo bajé a 55% después de demasiados falsos positivos. Sigue siendo una heurística; no es una garantía matemática.
¿Esto funciona con cualquier framework de agentes o es específico de tu stack?
Las tres capas —permisos atómicos con intención, presupuesto de sesión, grafo de transiciones prohibidas— son conceptualmente agnósticas al framework. Implementé esto a mano sobre mi API Gateway porque ningún framework que evalué tenía estas primitivas nativas en 2025. Si usás LangGraph o AutoGen, podés implementar el mismo patrón como middleware entre los nodos del grafo. El código específico cambia; el modelo mental no.
¿Qué pasa con los agentes que crean sub-agentes? ¿Los permisos se heredan?
Esta es la pregunta que más me preocupa y la que todavía no tengo bien resuelta. En mi stack actual, los sub-agentes heredan un subconjunto del presupuesto del padre, nunca el presupuesto completo. Si el agente padre tiene 20 acciones disponibles y crea un sub-agente, ese sub-agente arranca con máximo 5. El grafo de transiciones prohibidas se hereda completo. Pero la intención declarada no se propaga automáticamente: el sub-agente tiene que declarar su propia intención, que luego valido contra la del padre. Es imperfecto. Lo que sí tengo claro es que la herencia total de permisos —que es lo que hacen la mayoría de los frameworks por default— es una bomba de tiempo. También lo exploré en el post sobre agentes que crean cuentas y despliegan solos, aunque desde otro ángulo.
Lo que aprendí y lo que todavía no me cierra
Mi tesis, después de todo esto: los agentes autónomos no fallan por falta de capacidad, fallan por exceso de confianza en el modelo de permisos. Y el modelo de permisos que heredamos viene de sistemas donde el actor tiene estado emocional, fatiga y juicio situacional. Los agentes no tienen ninguno de los tres.
El rediseño que hice no es elegante. Es capas sobre capas de desconfianza formalizada. Validación de secuencias, presupuestos de sesión, trazabilidad de intención. Todo junto suma una arquitectura que es más lenta, más compleja y más difícil de mantener que lo que tenía antes.
Y aun así: cero incidentes en dos semanas. Tres desviaciones detectadas antes de que llegaran a hacer daño. Una infra que sigo confiándole trabajo real.
Lo que no me cierra todavía es la escala. Este sistema funciona para un agente, para tres agentes corriendo en paralelo. No sé cómo se comporta con veinte. El presupuesto de sesión se vuelve un recurso compartido que hay que coordinar, el grafo de transiciones se complejiza, la trazabilidad empieza a pesar en Postgres. Ese es el próximo problema. Por ahora, estoy resolviendo el que tengo.
Si venís del post de async Rust con edge cases en producción, sabés que mi tendencia es validar primero en mi codebase real antes de adoptar un patrón. Con esto no fue diferente. Dos semanas de datos reales valen más que cualquier arquitectura en una whiteboard.
El sistema está parado. Está fallando de formas que puedo ver. Por ahora, eso es suficiente.
Artículos Relacionados
Supply chain en npm vs PyPI: comparé mis dos simulaciones y el vector más peligroso no es el que todos creen
Corrí simulaciones de supply chain attack sobre npm y PyPI por separado. Cuando los puse uno al lado del otro, el patrón que emergió me incomodó: el ecosistema que todo el mundo vigila no es el más vulnerable. Acá va el meta-análisis cruzado con los números reales.
npm audit no alcanza: simulé un supply chain attack sobre mis dependencias de Node y encontré lo que el scanner no ve
npm audit te dice que estás seguro. Lo puse a prueba con metodología real sobre mis dependencias de producción y encontré tres vectores que el scanner ni registra. El ecosistema Node tiene un problema estructural que los badges verdes ocultan.
Mutex deadlock en producción: los patrones que encontré en mi codebase y cómo los diagnostiqué
Tres deadlocks en producción, todos con la misma cara: el servicio dejaba de responder sin error, sin panic, sin log. Lo que encontré al diagnosticarlos cambió cómo pienso el diseño de locks en async Rust.
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.