DEV Community

Cover image for Un agente borró mi base de datos en producción: lo que mis logs dicen que el post viral de HN omite
Juan Torchia
Juan Torchia

Posted on • Originally published at juanchi.dev

Un agente borró mi base de datos en producción: lo que mis logs dicen que el post viral de HN omite

Un agente borró mi base de datos en producción: lo que mis logs dicen que el post viral de HN omite

La solución correcta para evitar que un agente destruya producción es darle menos autonomía, no más guardrails. Sé que suena raro — la industria entera te va a vender lo contrario. Dejame explicar con mis propios logs por qué esa distinción importa más de lo que parece.


El post de Hacker News explotó esta semana. Score 689, cientos de comentarios, el hilo de turno donde todos se horrorizan y después siguen desplegando agentes con las mismas credenciales de siempre. Lo leí dos veces. Es un buen relato del accidente. Es un pésimo análisis de la causa raíz.

Porque el problema que describe no es nuevo para mí. Tengo logs. Los abrí.

Hace unas semanas escribí sobre CrabTrap, el proxy LLM-as-a-judge que puse delante de mi agente en producción. También sobre los agentes async y lo que el debugging no te dice. En ambos casos dejé algo sin resolver: qué pasa cuando el juez falla, cuando el proxy deja pasar algo que no debería. Hoy quiero agarrar esa hebra.


AI agent deleted production database: qué dice HN y qué omite

El relato viral tiene una estructura clásica: agente con permisos amplios, tarea ambigua, contexto mal delimitado, acción irreversible. El autor concluye que necesitaba "mejores guardrails". Los comentarios coinciden. Se cierran con una lista de herramientas.

Mi tesis es la opuesta: el problema no es que los guardrails fallaron. El problema es que diseñamos para el happy path y los guardrails son el parche sobre esa decisión.

Un guardrail es un mecanismo reactivo. Llega después de que ya decidiste darle al agente acceso a producción, credenciales reales, y un scope lo suficientemente amplio como para que pueda hacer daño. Es como poner un airbag en un auto al que le sacaste los frenos y lo mandaste cuesta abajo.

Lo que mis logs muestran es más incómodo que eso.


Lo que casi pasó: tres operaciones destructivas en mis propios logs

Abrí los logs de CrabTrap de los últimos 30 días. Filtré por operaciones con verbos destructivos: DELETE, DROP, TRUNCATE, rm -rf, variantes de reset. Encontré 23 llamadas que llegaron al proxy con alguna de esas intenciones. De esas 23:

  • 17 fueron bloqueadas por el juez LLM antes de ejecutarse.
  • 4 pasaron el juez pero fallaron por restricciones de permisos en Railway (el usuario de DB no tenía acceso a DDL).
  • 2 pasaron todo y ejecutaron algo que no debería haber ejecutado.

Esos dos casos son los que importan. No porque hayan sido catastróficos — no lo fueron — sino porque muestran exactamente dónde rompió la cadena.

Caso 1: DELETE sin WHERE

-- Lo que el agente quería ejecutar
-- Contexto: "limpiá los registros de test del entorno de staging"
DELETE FROM sessions;

-- Lo que debería haber ejecutado
DELETE FROM sessions WHERE environment = 'staging' AND created_at < NOW() - INTERVAL '7 days';
Enter fullscreen mode Exit fullscreen mode

El proxy dejó pasar el DELETE FROM sessions porque el juez evaluó la intención como válida (limpiar sesiones de staging) sin validar que la query no tenía cláusula WHERE. El agente tenía razón en lo que quería hacer. La implementación era un desastre.

¿El resultado? Borré 14.000 filas de sesiones de producción mezcladas con las de staging porque compartían la misma tabla. No era crítico — las sesiones son regenerables — pero si esa tabla hubiera sido orders o payments, la conversación sería diferente.

Caso 2: Cascade silencioso

-- El agente ejecutó esto en respuesta a "eliminá el usuario de test id=9981"
DELETE FROM users WHERE id = 9981;

-- Lo que no sabía (y yo tampoco había documentado bien):
-- users tiene ON DELETE CASCADE sobre:
--   → orders (→ order_items → inventory_movements)
--   → documents
--   → audit_logs
-- En total: 847 filas en 5 tablas
Enter fullscreen mode Exit fullscreen mode

El proxy no tenía forma de saber que ese CASCADE existía. Yo tampoco lo había documentado en el contexto que le pasé al agente. El juez aprobó la operación porque era semánticamente correcta. El schema hizo el resto.

Esto es lo que el post de HN no dice: los guardrails operan sobre la intención, no sobre los efectos secundarios del schema. Y los efectos secundarios del schema son invisibles para cualquier LLM que no tenga el ERD completo en contexto — lo cual, a escala, es imposible.


Por qué los guardrails de los frameworks son teatro

Revisé los guardrails que ofrecen los tres frameworks de agentes más usados hoy. No voy a nombrarlos para no hacer publicidad gratuita, pero el patrón es el mismo en todos:

# Patrón típico de "guardrail" en frameworks de agentes
# (pseudocódigo representativo, no de un framework específico)

BLOCKED_OPERATIONS = ["DROP TABLE", "TRUNCATE", "DELETE FROM users"]

def validate_query(query: str) -> bool:
    # Chequeo de string matching. Eso es todo.
    for blocked in BLOCKED_OPERATIONS:
        if blocked.upper() in query.upper():
            return False
    return True

# El problema: esto pasa sin problemas
validate_query("delete from users where id=1")  # → True
validate_query("DROP   TABLE sessions")          # → True (espacios extra)
validate_query("EXEC sp_executesql @q")          # → True (SQL dinámico)
Enter fullscreen mode Exit fullscreen mode

String matching sobre queries SQL. En 2025. Con agentes que generan SQL dinámico basado en contexto natural.

Cuando armé CrabTrap, reemplacé ese patrón por evaluación semántica — el proxy manda la query + el contexto al modelo y pregunta si la operación es destructiva en ese contexto específico. Es mejor. Pero como muestran los dos casos de arriba, tampoco es suficiente cuando el problema está en el schema, no en la query.

La solución que encontré — y que el post de HN no menciona — es más aburrida: usuarios de base de datos con permisos mínimos, separados por entorno, sin acceso a DDL, y con restricciones de row-level security cuando aplica. No es sexy. No es un framework. Es lo que debería existir antes de que el agente empiece a hablar con la base de datos.

Esto conecta con algo que vengo arrastrando desde el análisis del supply chain attack sobre Bitwarden CLI: la superficie de confianza se diseña antes del incidente, no después. Cuando empezás a remendar después, ya tomaste las decisiones que importaban.


Los errores de diseño que el incidente de HN normaliza sin querer

El relato viral, aunque bien intencionado, deja pasar tres supuestos sin cuestionarlos:

1. Que el agente necesitaba acceso directo a la base de datos.

En la mayoría de los casos, no lo necesita. El agente debería hablar con una API de dominio que exponga operaciones nombradas, validadas y auditadas. eliminarUsuarioDePrueba(id) en vez de DELETE FROM users WHERE id=?. La diferencia es que la función de dominio conoce el cascade, la función de dominio tiene validaciones de negocio, y la función de dominio puede ser testeada con casos límite sin tocar producción.

2. Que los entornos de staging y producción son lo suficientemente distintos.

No lo son si comparten schema, si el agente usa las mismas credenciales, o si "staging" es simplemente un flag en una variable de entorno que el agente puede ignorar o malinterpretar. Yo lo aprendí a las malas con el caso del DELETE sin WHERE.

3. Que el problema es nuevo.

No lo es. En el cyber café donde laburé de adolescente, el diagnóstico de red a las 11pm con el local lleno me enseñó algo que ningún tutorial explica: los sistemas fallan en las intersecciones, no en los componentes. La conexión no se caía por el router solo ni por el ISP solo — se caía en el punto donde los dos se hablaban mal. Los agentes destruyen DBs en la intersección entre autonomía amplia, permisos generosos y contexto incompleto. No en ninguno de esos tres factores solos.


FAQ: AI agents y bases de datos en producción

¿Qué permisos mínimos debería tener un agente que accede a una base de datos?

Depende del caso de uso, pero como regla general: SELECT sobre las tablas que necesita leer, INSERT y UPDATE sobre las tablas que necesita modificar, y cero acceso a DDL (DROP, ALTER, TRUNCATE). Si el agente necesita borrar datos, mejor exponerle una función de dominio con soft delete que acceso directo a DELETE. Para entornos productivos, row-level security es la capa que cierra el perímetro cuando el resto falla.

¿Los guardrails de LangChain, CrewAI o similares son suficientes para prevenir operaciones destructivas?

En mi experiencia, no. Son útiles como primera capa, pero operan sobre patrones de texto o sobre intención semántica sin acceso al schema real. El problema de los cascades silenciosos, las foreign keys implícitas o los triggers de base de datos es invisible para cualquier guardrail que no tenga el ERD completo en contexto. Son necesarios pero no suficientes.

¿Qué es mejor: un proxy LLM-as-a-judge o permisos restrictivos en la DB?

Las dos capas, en ese orden de prioridad. Los permisos restrictivos son el piso: definen qué puede pasar físicamente. El proxy es el techo: detecta operaciones semánticamente peligrosas antes de que lleguen al piso. Si tenés que elegir uno, elegí los permisos. El proxy sin permisos mínimos es un guardrail de texto sobre una conexión con permisos de superusuario.

¿Cómo separo staging de producción para que un agente no pueda confundir los dos?

Usuarios de base de datos distintos, credenciales distintas, y — si podés — bases de datos distintas en hosts distintos. No alcanza con un flag ENV=staging en la configuración. El agente no lee variables de entorno con el mismo nivel de certeza que un proceso determinístico: su contexto es el prompt, y el prompt puede estar incompleto o mal construido. La separación física es la única que no puede ser malinterpretada.

¿Vale la pena agregar confirmación humana antes de operaciones destructivas?

Sí, pero con criterio. Human-in-the-loop en cada operación destruye la utilidad del agente. Lo que funciona es un sistema de clasificación: operaciones de lectura → automático; operaciones de escritura idempotentes → automático con log; operaciones destructivas o irreversibles → confirmación humana siempre. El truco es que esa clasificación tiene que estar en la capa de infraestructura, no en el prompt.

¿El problema del agente que borró la DB en HN es representativo de lo que pasa en producción?

Más de lo que la industria admite. La diferencia entre ese caso y los míos fue el nivel de permisos que tenía el usuario de base de datos. En el caso de HN, tenía acceso total. En el mío, el acceso DDL estaba bloqueado por Railway, lo que convirtió un potencial desastre en un error de permisos logueable. Esa diferencia no vino de un framework de agentes — vino de una decisión de infraestructura tomada antes de que el agente existiera.


Lo que acepto, lo que no compro y el trade-off honesto

Lo que acepto: los agentes van a seguir rompiendo cosas. No por malicia, sino porque operan sobre contexto incompleto en sistemas diseñados para humanos que entienden el schema implícito. Eso no va a cambiar con mejores prompts ni con mejores guardrails.

Lo que no compro: que la solución es más abstracción encima del mismo problema de permisos. Cada framework nuevo que promete "agentes seguros por defecto" y después expone una conexión con credenciales de superusuario en los ejemplos de la documentación me está mintiendo. Lo vi con TypeScript 7.0 y sus nuevas features de tipado — las abstracciones nuevas no resuelven los problemas de diseño viejos, los ocultan hasta que explotan.

El trade-off honesto: autonomía real tiene un costo de infraestructura que la mayoría no quiere pagar. Separar entornos físicamente, crear usuarios de DB con permisos mínimos, exponer APIs de dominio en vez de acceso directo a tablas, implementar soft deletes, auditar cascades — todo eso toma tiempo. Es más fácil dejar al agente con acceso completo y confiar en que el LLM va a hacer lo correcto.

El post de HN con 689 puntos existe porque ese atajo eventualmente cobra.

Mis dos casos casi-desastre existen porque yo también tomé atajos — el DELETE sin WHERE fue descuido mío en el diseño del schema compartido, el CASCADE silencioso fue documentación que nunca escribí. Los guardrails me salvaron dos veces. La tercera podría no hacerlo.

La diferencia entre diseñar para el happy path y diseñar para el failure path no es una diferencia de herramientas. Es una diferencia de actitud frente al sistema. Y esa actitud se aprende, casi siempre, después de que algo se rompe.

Seguí la conversación en los comentarios: ¿qué operaciones casi-destructivas encontraste en tus propios logs?


Este artículo fue publicado originalmente en juanchi.dev

Top comments (0)