Desarrollo IATutorial

Ejecutá Código Escrito por Agentes Dentro de un Sandbox: Timeouts, Límites y Sin Egress

14 de octubre de 2026
13 min de lectura
Cubo de vidrio transparente con una pequeña esfera de metal sobre una superficie clara
Compartir:

Tu agente acaba de escribir un script de Python y quiere ejecutarlo. Si tu respuesta es exec() o subprocess dentro del mismo proceso que tiene la URL de tu base de datos y tus API keys, alcanza con una página web o un README envenenado para convertir la salida del modelo en ejecución de código en tu servidor.

En este tutorial armo una tool de code interpreter como corresponde: el código generado corre en un sandbox descartable, con timeout duro, recursos acotados y egress bloqueado por defecto. Te dejo código de quickstart de E2B, Vercel Sandbox, Modal y Daytona tomado de sus docs actuales, una tabla comparativa solo con hechos y la lista de lo que el aislamiento no cubre. Los snippets están adaptados de la documentación de cada proveedor, no son benchmarks míos.

1. El Problema: El Código Generado Es Input No Confiable

Una tool de code interpreter deja que el modelo calcule en vez de adivinar: parsear un CSV, hacer un gráfico, correr un test. La trampa es que ese código lo escribe un sistema que también lee texto no confiable — páginas web, issues, PDFs, salidas de otras tools. Una prompt injection convierte ‘resumí esta página’ en ‘ejecutá este snippet’, y tenés ejecución remota de código con un paso extra.

El Error Más Común

Correr código generado con exec(), eval(), child_process o subprocess dentro del contenedor de tu propia app — o en un ‘sandbox’ que comparte tus variables de entorno, tu filesystem y tu red. Un contenedor es una frontera de empaquetado, no una promesa sobre código hostil, y cualquier cosa dentro del proceso puede leer los secretos que pusiste ahí.

La solución no es un producto. Es un stack corto: correr el código en algo descartable, darle un presupuesto de tiempo y otro de recursos, decidir a qué puede llegar la red antes de que arranque y dejar las credenciales afuera. Los proveedores de abajo venden la caja descartable; el resto es política que escribís vos.

2. Conceptos Mínimos Antes de Tocar Código

Seis ideas cubren casi todo. Con esto claro, los snippets de cada proveedor se leen como variaciones de un mismo tema.

🧱

Frontera de aislamiento

lo que separa al código de tu host

Un contenedor comparte el kernel del host y se apoya en namespaces y cgroups. Una microVM arranca su propio kernel invitado bajo un hipervisor (Firecracker es el habitual). gVisor está en el medio: un kernel en espacio de usuario que intercepta syscalls. Cada uno es algo distinto que un atacante tiene que romper.

🌐

Política de egress

la fuga que olvidás

El aislamiento no dice nada sobre a dónde pueden ir los bytes. Estos sandboxes suelen llegar a todo internet por defecto, así que código inyectado puede hacer POST de tus datos a cualquier lado. Denegá por defecto y permití solo lo que la tarea necesita.

⏱️

Timeouts

dos relojes, no uno

El timeout por comando frena un script colgado; la vida del sandbox frena todo, incluido el sandbox que te olvidaste. Poné los dos y llamá a stop o kill en un bloque finally.

📏

Límites de recursos

CPU, memoria, disco

Acotá vCPUs y memoria por sandbox para que una fork bomb o una reserva gigante agote su propia caja, no tu factura ni a tus vecinos.

💾

Estado persistente

feature o trampa

Algunos proveedores hacen snapshot y reanudan por defecto. Sirve para el workspace de un agente de código, no para ejecución one-shot: archivos sobrantes, paquetes instalados y scripts plantados te siguen a la próxima corrida.

🔑

Secretos

mantenelos afuera

Todo lo que está dentro del sandbox lo puede leer el código que corre ahí. Preferí inyectar credenciales en el borde de la red, o llamar a tu propia API desde el host, antes que pasar keys como variables de entorno.

El modelo mental a conservar

Un sandbox protege a tu host del código. No protege los datos que le entregás al código, los servicios a los que tu política de red le deja llegar, ni al modelo de lo que el código imprime de vuelta. Dibujá el modelo de amenazas alrededor de esas tres cosas y recién después elegí la capa de aislamiento.

3. Cuatro Proveedores, Solo Hechos de Sus Propias Docs

Limité la tabla a lo que cada proveedor afirma en su documentación o su página de precios al 14 de octubre de 2026. Cuando una página no daba un número, la celda lo dice en vez de adivinar. Precios y límites cambian: tomala como un mapa hacia las páginas de Fuentes, no como una cotización.

E2BVercel SandboxModalDaytona
Aislamiento (según ellos)microVM Firecracker con kernel invitado propio por sesión (página enterprise)microVM Firecracker con kernel dedicado por sandboxgVisor, o una VM con su propio kernel Linux; se fija con runtime o Modal decideDocs: kernel dedicado, filesystem y stack de red propios; las tablas de recursos listan clases Container y Linux VM
ArranqueSin cifra en las páginas que leí‘Milisegundos’ (sin número)Sin cifra en las páginas que leí‘Menos de 90 ms’ (afirmación del proveedor)
Lenguajes de SDKJavaScript/TypeScript, PythonJavaScript/TypeScript, Python, CLIPython, JavaScript, GoPython, TypeScript, Ruby, Go, Java; CLI y REST
DuraciónHasta 1 h en Hobby, 24 h en Pro; el timeout se fija al crearDefault 5 min; sesión máx. 45 min en Hobby, 24 h en ProDefault 5 min; hasta 24 hAuto-stop tras 15 min de inactividad por defecto (referencia del SDK)
Control de egressallowInternetAccess, allowOut / denyOut (IP, CIDR, dominio)allow-all, deny-all o política propia de dominios y CIDR; actualizable en runtimeblock_network, allowlist de CIDR, allowlist de dominios (beta, solo TLS en 443)networkBlockAll, networkAllowList (máx. 10 CIDR IPv4), domainAllowList
RecursosDefault 2 vCPU / 4 GiB; máx. 8 vCPU / 8 GiBDefault 2 vCPU con 2 GB cada una; máx. 4 (Hobby), 8 (Pro), 32 (Enterprise) vCPUscpu en cores fraccionarios, memory en MiB, por sandboxDefault 1 vCPU / 1 GB / 3 GiB de disco; máx. de la org 4 vCPU / 8 GB / 10 GB
Modelo de preciosPor segundo: $0.000014 por vCPU-s, $0.0000045 por GiB-s; Hobby $0 + $100 de crédito único, Pro $150/mes + usoCPU activa $0.128/h (las esperas de I/O no se cobran) + memoria $0.0212/GB-h; Hobby con cuotas gratisSandboxes a $0.00003942 por core-s y $0.00000667 por GiB-s (unas 3x las functions); Starter con $30/mes de créditoPor segundo: $0.0504 por vCPU-h, $0.0162 por GiB-h; $200 de crédito gratis

Versiones verificadas el 14 de octubre de 2026

@vercel/sandbox 3.8.0, @e2b/code-interpreter 2.9.0 (npm) y e2b-code-interpreter 2.11.0 (PyPI), modal 1.6.1, daytona 0.224.1 (PyPI) y @daytona/sdk 0.224.1 (npm), leídas de los registros de npm y PyPI.

Cómo leer la fila de aislamiento

Es lo que cada proveedor afirma, no lo que medí yo. Firecracker y gVisor son tecnologías de aislamiento reales con trade-offs distintos, pero ninguna es una garantía: un bug del hipervisor o del kernel rompe cualquiera de las dos, y una política de red permisiva las saltea a todas.

Daytona: dos afirmaciones que no encajan

Las docs de Daytona dicen que cada sandbox tiene un kernel dedicado, y las mismas docs rotulan el snapshot por defecto daytona-small como clase ‘Container’, al lado de snapshots daytona-vm-* de clase ‘Linux VM’. Comparativas de terceros que encontré describen los sandboxes por defecto de Daytona como contenedores OCI/Docker comunes; no pude confirmarlo en las páginas de Daytona. Antes de correr código hostil ahí, preguntá qué clase te toca y preferí la clase VM si necesitás una frontera de kernel.

4. Paso 1 — El Contrato de la Tool y Vercel Sandbox (TypeScript)

Empezá por el contrato, porque es igual para todos los proveedores: el modelo manda código y un presupuesto de tiempo; la tool devuelve stdout, stderr y un exit code, truncados; el host nunca ejecuta nada. Abajo corre sobre Vercel Sandbox (@vercel/sandbox 3.8.0 al publicar): creás un sandbox con egress denegado, escribís el archivo, lo corrés con una señal de abort por comando y siempre frenás la VM.

TypeScript — runCode sobre Vercel Sandbox

import { Sandbox } from "@vercel/sandbox";

const MAX_OUT = 20_000; // characters handed back to the model

export async function runCode(code: string, budgetMs = 15_000) {
  const sandbox = await Sandbox.create({
    timeout: 60_000,           // hard session cap in ms (default 5 min)
    resources: { vcpus: 1 },   // default is 2
    networkPolicy: "deny-all", // blocks egress, DNS included
    persistent: false,         // one-shot: discard the filesystem on stop
  });
  try {
    await sandbox.writeFiles([
      { path: "agent-code.mjs", content: Buffer.from(code) },
    ]);
    const run = await sandbox.runCommand(
      "node",
      ["/vercel/sandbox/agent-code.mjs"],
      { signal: AbortSignal.timeout(budgetMs) }, // aborted call rejects
    );
    return {
      exitCode: run.exitCode,
      stdout: (await run.stdout()).slice(0, MAX_OUT),
      stderr: (await run.stderr()).slice(0, MAX_OUT),
    };
  } finally {
    await sandbox.stop(); // always, even when the call above throws
  }
}

Con deny-all desde el arranque no hay npm ni pip install. Si la tarea necesita paquetes, usá el patrón de actualización en vivo de la doc: instalás con red en un paso de setup confiable, bajás a deny-all y recién entonces corre el código no confiable. Para acceso parcial la política acepta una lista de dominios, y para credenciales existe el brokering: el firewall inyecta el header y el secreto nunca entra al sandbox.

TypeScript — instalar, bloquear o intermediar una credencial

// 1) Trusted setup with network, then lock before untrusted code
const sandbox = await Sandbox.create({ persistent: false }); // allow-all
await sandbox.runCommand("npm", ["install", "mathjs"]);
await sandbox.update({ networkPolicy: "deny-all" });

// 2) Or allow-list only what the task needs
const scoped = await Sandbox.create({
  networkPolicy: { allow: ["ai-gateway.vercel.sh"] },
});

// 3) Or let the firewall inject the credential (secret stays outside)
const brokered = await Sandbox.create({
  networkPolicy: {
    allow: {
      "ai-gateway.vercel.sh": [{
        transform: [{ headers: { Authorization: "Bearer " + process.env.AI_GATEWAY_TOKEN } }],
      }],
    },
  },
});

Por qué persistent: false y por qué stop()

Los Vercel Sandboxes son persistentes por defecto: el filesystem se snapshotea al frenar y se restaura al reanudar, y los snapshots cuentan para el storage. Eso va bien con el workspace de un agente de larga vida y está mal para ejecución one-shot. La doc también da un timeout de sesión por defecto de 5 minutos y un tope de 45 minutos en Hobby, y un sandbox que olvidás frenar sigue sumando cargos de memoria provisionada hasta que vence. La AbortSignal cancela la llamada del SDK; no encontré en la doc que mate el proceso dentro de la VM, así que tomá el timeout del sandbox como el muro duro.

5. Paso 2 — E2B Code Interpreter (TypeScript y Python)

El @e2b/code-interpreter de E2B te da runCode con un resultado estilo Jupyter, así el agente puede mandar Python de varias líneas y leer errores estructurados. Fijá la vida en la llamada de creación y ojo con las unidades: milisegundos en JavaScript (timeoutMs), segundos en Python (timeout). Apagar internet es un solo flag, y las reglas de red cubren el caso de la allow-list.

TypeScript — sin internet, 60 s de vida

import { Sandbox } from "@e2b/code-interpreter"; // needs E2B_API_KEY

const sbx = await Sandbox.create({
  timeoutMs: 60_000,          // JavaScript: milliseconds
  allowInternetAccess: false, // same as denyOut: ["0.0.0.0/0"]
});
try {
  const execution = await sbx.runCode(aiGeneratedCode);
  if (execution.error) {
    console.error(execution.error.name, execution.error.value);
    console.error(execution.error.traceback);
  } else {
    console.log(execution.logs);
  }
} finally {
  await sbx.kill();
}

En Python la misma idea se escribe en snake_case, y la opción network recibe listas explícitas de allow y deny. Denegar todo el tráfico primero y permitir direcciones puntuales después es la forma que muestra la doc.

Python — denegar todo, permitir dos rangos

from e2b_code_interpreter import Sandbox

sbx = Sandbox.create(
    timeout=60,  # Python: seconds
    network={
        "deny_out": lambda ctx: [ctx.all_traffic],
        "allow_out": ["1.1.1.1", "8.8.8.0/24"],
    },
)
try:
    execution = sbx.run_code(ai_generated_code)
    if execution.error:
        print(execution.error.traceback)
    else:
        print(execution.logs)
finally:
    sbx.kill()

El filtrado por dominio en E2B tiene aristas

Según la doc: si filtrás por dominio tenés que denegar todo el resto en denyOut, los dominios no se soportan en listas de deny, el filtrado por dominio cubre HTTP en el puerto 80 (header Host) y TLS en 443 (SNI), QUIC/HTTP3 no está soportado, y el nameserver por defecto 8.8.8.8 queda auto-permitido apenas usás un dominio. Las reglas allow ganan a las deny, y una conexión TCP bloqueada puede igual reportar el socket como abierto, así que probá con un request a nivel aplicación, no con un chequeo de puerto.

6. Pasos 3 y 4 — Modal y Daytona (Python)

Los Sandboxes de Modal corren comandos en un contenedor que definís en runtime. Podés elegir el runtime — gVisor, o una VM con su propio kernel — o dejarlo sin fijar y que decida Modal. La vida máxima por defecto es de 5 minutos (configurable hasta 24 horas), CPU y memoria son parámetros explícitos y la red se corta con un flag.

Python — Sandbox de Modal con block_network

import modal

app = modal.App.lookup("code-interpreter", create_if_missing=True)

sb = modal.Sandbox.create(
    image=modal.Image.debian_slim().pip_install("pandas"),
    app=app,
    timeout=120,         # seconds of max lifetime (default 300)
    cpu=1.0,             # fractional cores
    memory=1024,         # MiB
    block_network=True,  # drops all outbound traffic
)
try:
    p = sb.exec("python", "-c", ai_generated_code, timeout=15)
    print(p.stdout.read())
    print(p.stderr.read())
    print("exit code:", p.wait())
finally:
    sb.terminate()

Daytona (daytona y @daytona/sdk 0.224.1) crea un sandbox, corre código con process.code_run y recibe un timeout en segundos. Los defaults documentados son 1 vCPU, 1 GB de RAM, 3 GiB de disco y auto-stop tras 15 minutos de inactividad. El egress se fija al crear con network_block_all, network_allow_list o domain_allow_list; son mutuamente excluyentes y una mezcla conflictiva devuelve un 400.

Python — sandbox efímero de Daytona con red bloqueada

from daytona import Daytona, DaytonaConfig, CreateSandboxFromSnapshotParams

daytona = Daytona(DaytonaConfig(api_key="YOUR_API_KEY"))  # read it from env

sandbox = daytona.create(
    CreateSandboxFromSnapshotParams(
        network_block_all=True,  # or domain_allow_list="example.com,*.daytona.io"
        ephemeral=True,          # deleted when stopped
        auto_stop_interval=5,    # minutes of inactivity (default 15)
    )
)
try:
    response = sandbox.process.code_run(ai_generated_code, timeout=10)
    if response.exit_code != 0:
        print("failed:", response.result)
    else:
        print(response.result)
finally:
    sandbox.delete()

Los tiers de Daytona pueden pisarte

En organizaciones Tier 1 y Tier 2 el acceso de red se restringe a nivel org y no se puede anular por sandbox; Tier 3 y 4 arrancan con internet completo, que los ajustes del sandbox pueden reemplazar. La allow list de CIDR acepta solo IPv4, con 10 entradas como máximo. En Modal, block_network no se combina con los parámetros de allowlist, y su allowlist de dominios es beta y solo TLS en 443.

7. Una Rutina de Blindaje Alrededor del Sandbox

El proveedor te da la caja. Esta es la rutina que le armo alrededor para que la caja realmente sirva:

🚦 Antes de cada corrida (diseño)

  1. Alcance: un sandbox por sesión o tenant no confiable; nunca compartas un sandbox entre usuarios
  2. Egress: deny-all, o una allow-list de dominios para la única API que la tarea necesita; instalá dependencias en un paso de setup confiable y después bloqueá
  3. Datos de entrada: pasá inputs como archivos, no credenciales; mantené los tokens fuera de las env vars y del filesystem
  4. Presupuestos: fijá la vida del sandbox y el timeout por comando, y acotá vCPUs y memoria

🔍 Después de cada corrida

  1. Truncá: limitá el largo de stdout y stderr antes de devolverlos al modelo
  2. Desconfiá: tratá la salida como dato no confiable; un script puede imprimir una prompt injection directo al contexto de tu agente
  3. Limpiá: stop, kill o delete en un bloque finally, y logueá exit code, duración y la política de red usada

🧪 En cada release (CI)

  1. Test de egress: corré un script que intente llegar a un endpoint canario tuyo y verificá que falle, y que en las allow-lists solo ande el host previsto
  2. Test de límites: un loop infinito y un devorador de memoria; verificá que se disparen tu timeout y tu tope y que no quede ningún sandbox facturable corriendo
  3. Deriva del proveedor: releé las páginas de Fuentes; límites, defaults y precios cambian

8. Errores Comunes (y Qué No Cubre Cada Capa)

La columna izquierda es lo que hacen las integraciones que funcionan; la derecha es lo que aparece en los post-mortems.

✅ Hacé esto

  • • Denegá el egress primero y abrí el agujero más chico que la tarea necesite
  • • Fijá la vida del sandbox y un timeout por comando, y frená la VM en finally
  • • Usá un sandbox nuevo y no persistente para código one-shot; persistí solo workspaces deliberados
  • • Mantené los secretos afuera: intermediá credenciales en el borde o llamá a tu API desde el host
  • • Truncá la salida antes de que vuelva al contexto del modelo
  • • Pineá la versión del SDK y releé los límites antes de cambiar de plan

❌ No esto

  • • exec() o subprocess en el contenedor de tu app ‘porque está en Docker’
  • • Confiar en una allow-list de dominios como filtro de contenido: el match por SNI se puede falsear y algunos modos dejan el DNS alcanzable
  • • Pasar API keys como env vars del sandbox ‘solo por esta corrida’
  • • Reutilizar un sandbox persistente entre usuarios o sesiones
  • • Asumir que las unidades de timeout coinciden entre SDKs (E2B: ms en JS, segundos en Python)
  • • Tratar ‘aislado’ como ‘seguro’ y saltearse el modelo de amenazas

Regla de oro

El sandbox responde una sola pregunta: ¿qué puede hacerle este código a mi host? Todavía tenés que responder las otras tres — qué datos le di, a dónde puede mandar bytes y qué voy a hacer con lo que devuelve.

Conclusión

Ya tenés el loop completo: un contrato de tool (código y presupuesto adentro, stdout, stderr y exit code afuera), un sandbox descartable en E2B, Vercel Sandbox, Modal o Daytona, egress denegado antes de que corra la primera línea, dos relojes y la salida tratada como no confiable. Los proveedores difieren en tecnología de aislamiento, límites de vida y modelo de precios, pero la política que escribís alrededor es la misma.

Esta semana elegí un proveedor, envolvelo en el contrato runCode de arriba y sumá dos tests de CI: un intento de egress canario y un loop infinito. Si pasan los dos, ‘el agente puede ejecutar código’ deja de ser un pasivo y pasa a ser una feature con un radio de daño medido. Si tu agente además instala skills o clona repos, leé los artículos relacionados de abajo: el sandbox es una capa, no toda la defensa.

Machete del Tutorial

Contrato

  • • código + presupuesto de tiempo
  • • stdout, stderr, exit code
  • • truncar y tratar como dato

Sandbox

  • • uno por sesión, no persistente
  • • egress deny-all por defecto
  • • vida + timeout por comando

Fuera de él

  • • secretos se quedan en el host
  • • CI: tests de egress y límites
  • • releé las páginas del proveedor

Fuentes

Diego Rodriguez

Diego Rodriguez

Ingeniero Senior Full-Stack & AI

Diego tiene mas de 9 anos de experiencia construyendo aplicaciones potenciadas por IA de produccion, desde orquestacion de LLMs y pipelines RAG hasta deteccion de riesgos con ML y sistemas de trading algoritmico.

Conoce mas sobre Diego →