Cómo diseñar un harness para agentes de IA seguro y determinista
Aprendé a construir un harness para agentes de IA con sandbox de comandos, límites de presupuesto y rollback con Git para evitar desastres en código.
Le pedís a tu agente de código que resuelva un bug en el módulo de pagos. A los tres minutos descubrís que borró un archivo de configuración, ejecutó un script con dependencias rotas y consumió 18 dólares en un bucle ciego. El LLM no falló: soltaste el modelo sin un harness que controle sus límites.
Si estás pensando cómo armar un harness para agentes de IA, en este post vas a ver las cuatro piezas deterministas que tenés que codear alrededor del modelo: poda de contexto, sandbox de comandos, límites de turnos y rollback con Git.
Un modelo probabilístico razona sobre código, pero el control del sistema siempre tiene que ser determinista.
flowchart TD
A["Petición del usuario"] --> B["Filtro de contexto (AST + tests)"]
B --> C["Llamada al LLM"]
C --> D{"¿Llegó al límite? (Costos / Turnos)"}
D -- "Sí" --> E["Abortar sesión y alertar"]
D -- "No" --> F["Sandbox de comandos"]
F --> G{"¿Pasaron los tests?"}
G -- "No" --> H["Rollback automático en Git"]
G -- "Sí" --> I["Git commit atómico"]
Por qué necesitás un harness para tus agentes de IA
El harness es el código común, determinista, que envuelve al modelo. Los límites se definen con código tradicional (if, switches, sandboxes del sistema operativo y APIs de archivos), no con prompts.
Si le decís al modelo por system prompt “no toques archivos fuera de /src”, tarde o temprano los va a tocar. Es la misma idea que en cómo frenar mutaciones destructivas en agentes autónomos: el permiso lo decide el código, no el prompt. El modelo opera con probabilidades léxicas. Tu harness tiene que tirar una excepción dura (PathTraversalError) antes de tocar el disco.
Vamos a seguir un caso puntual durante todo el post: un agente que debe corregir el cálculo del IVA en un servicio Node.js (src/billing.ts) y validar sus cambios contra src/billing.test.ts.
1. Contexto quirúrgico: qué entra y qué queda afuera
El primer error común es pasarle al agente el árbol de archivos entero. Eso llena la ventana de contexto de ruido, sube el costo y degrada la atención del modelo.
Tu harness tiene que filtrar el contexto antes de mandarle el prompt al LLM:
- Solo el archivo objetivo e interfaces: extraé con parsing de AST únicamente las firmas de los tipos importados en
src/billing.tspara no duplicar módulos enteros en el prompt. - El diff exacto del fallo: inyectá únicamente el stack trace acotado de
src/billing.test.tspara ahorrar tokens y enfocar al modelo en la aserción rota. - Poda de dependencias: filtrá por omisión carpetas como
node_modules, builds previos y.cachepara evitar búsquedas recursivas inútiles.
Si el agente necesita leer otro archivo, debe pedírselo al harness mediante una herramienta de solo lectura (read_file). Para evitar vulnerabilidades de path traversal mediante ../../ o symlinks engañosos, el harness resuelve la ruta canónica con fs.realpathSync() y valida que empiece estrictamente con el prefijo del directorio del proyecto.
2. Sandbox de comandos: aislar la ejecución
El agente va a querer correr comandos para verificar sus cambios en src/billing.ts. Darle acceso directo a child_process.exec() o subprocess.run() permite que borre código o ejecute scripts maliciosos. Lo cuento más en detalle en cómo controlar a tus agentes con un sandbox operativo.
El harness debe imponer dos reglas físicas sobre los comandos:
- Contenedores efímeros o namespaces: ejecutá los comandos en un namespace aislado sin interfaz de red activa, porque el runner local no necesita internet para correr tests de unidad.
- Lista de comandos permitidos: validá el comando completo, no sólo el binario. Permitir
npxonpm runa secas es permitir cualquier cosa; permitirnpm testexacto, no. Todo lo demás (curl,rm,bash) queda bloqueado.
Acá tenés un ejemplo de wrapper en TypeScript para ejecutar comandos de forma controlada:
import { execFileSync } from "node:child_process";
import path from "node:path";
// Comandos exactos, no binarios: "npx" o un "npm run" cualquiera pueden ejecutar cualquier cosa.
const ALLOWED_COMMANDS = new Set(["npm test", "npm run lint", "npx tsc --noEmit"]);
const WORKSPACE_DIR = path.resolve("./workspace");
export function runAgentCommand(rawCommand: string): string {
const command = rawCommand.trim().split(/\s+/).join(" ");
if (!ALLOWED_COMMANDS.has(command)) {
throw new Error(`Comando no permitido por el harness: "${command}"`);
}
const [binary, ...args] = command.split(" ");
// En Linux podés usar firejail; en macOS conviene un subproceso con flags POSIX
return execFileSync(binary!, args, {
cwd: WORKSPACE_DIR,
timeout: 30_000,
encoding: "utf-8",
env: { PATH: process.env.PATH, NODE_ENV: "test" },
});
}
Este enfoque tiene tradeoffs técnicos claros. Levantar un contenedor Docker por cada comando suma el arranque del contenedor a cada ejecución. Además, herramientas livianas de aislamiento como Firejail solo funcionan en Linux, porque se apoyan en sus namespaces; en entornos locales con macOS tenés que recurrir a permisos POSIX restringidos o asumir el costo de virtualización.
3. Circuit breakers: límites de turnos y presupuesto
Los modelos pueden entrar en bucles de razonamiento donde corrigen una coma en src/billing.ts, rompen un mock en src/billing.test.ts, corrigen el test y vuelven a romper el tipo original.
Si el agente intenta corregir el cálculo de IVA y al cuarto turno sigue fallando la misma aserción, el harness corta la ejecución en vez de dejarlo probar combinaciones al azar.
Para el costo, el harness intercepta los metadatos de consumo devueltos en la respuesta del proveedor (los tokens exactos de entrada, salida y cache). Multiplica esos enteros por el tarifario configurado en una tabla local en memoria y actualiza un acumulador antes de autorizar la siguiente iteración.
| Métrica de corte | Límite sugerido | Riesgo de omitir el límite |
|---|---|---|
| Turnos de agente | 5 iteraciones | Loops infinitos que generan código spaghetti sobre el archivo original. |
| Presupuesto de tokens | 0.50 USD por tarea | Consumo descontrolado de saldo ante respuestas repetitivas del modelo. |
| Timeout por comando | 30 segundos | Procesos colgados consumiendo CPU y memoria en el worker de CI. |
Para ver de dónde sale ese gasto y cómo bajarlo, en cómo optimizar la inferencia de LLM lo desarmo paso a paso.
4. Rollback atómico con Git
Tu harness aprovecha el historial de Git en lugar de programar un sistema de undo propio.
Antes de que el agente altere src/billing.ts, el harness prepara el terreno de trabajo:
- Crear una rama efímera: generá una rama temporal aislada (por ejemplo,
agent/patch-billing-tax). - Registrar un commit base: guardá un estado limpio conocido antes de conceder permisos de escritura.
- Verificación independiente: cuando el agente declara haber terminado, el harness corre
npm testpor su cuenta sin confiar en el output textual del LLM. - Resolución determinista: si el comando devuelve exit code 0, el harness ejecuta un commit atómico; si devuelve error o se agotaron los turnos, ejecuta
git reset --hard HEADy borra el espacio de trabajo.
Cuándo implementar un harness para agentes de IA
No todas las tareas justifican montar esta infraestructura. Si estás armando un script personal para renombrar variables locales donde vos mirás el diff en tu editor en tiempo real, armar un sandbox con validación de AST es sumar complejidad innecesaria.
Un harness para agentes de IA se vuelve prioritario cuando el agente corre de noche en un worker desatendido o cuando tiene permisos automáticos para crear ramas y abrir pull requests en repositorios compartidos.
Qué hacer el lunes
Si tenés agentes de código operando en tu equipo, podés aplicar estos cambios de inmediato:
- Blindá las escrituras en disco: envolvé la función de escritura con
path.resolve()yfs.realpathSync()para rechazar cualquier ruta fuera de la carpeta del proyecto. - Instalá un contador de turnos rígido: poné un límite de 5 iteraciones en el loop principal y salí del proceso con código de error si se alcanza el tope.
- Calculá el gasto por llamada: extraé el bloque de usage de la respuesta del LLM y acumulá los dólares gastados para frenar la tarea si supera 0.50 USD.
- Automatizá el descarte con Git: ejecutá un stash o creá una rama temporal antes de entregarle los archivos al modelo para recuperar el estado original con un solo comando.
La autonomía de un agente no depende de qué tan inteligente sea el modelo, sino de las restricciones de código que le imponés.