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.
| E2B | Vercel Sandbox | Modal | Daytona | |
|---|---|---|---|---|
| Aislamiento (según ellos) | microVM Firecracker con kernel invitado propio por sesión (página enterprise) | microVM Firecracker con kernel dedicado por sandbox | gVisor, o una VM con su propio kernel Linux; se fija con runtime o Modal decide | Docs: kernel dedicado, filesystem y stack de red propios; las tablas de recursos listan clases Container y Linux VM |
| Arranque | Sin 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 SDK | JavaScript/TypeScript, Python | JavaScript/TypeScript, Python, CLI | Python, JavaScript, Go | Python, TypeScript, Ruby, Go, Java; CLI y REST |
| Duración | Hasta 1 h en Hobby, 24 h en Pro; el timeout se fija al crear | Default 5 min; sesión máx. 45 min en Hobby, 24 h en Pro | Default 5 min; hasta 24 h | Auto-stop tras 15 min de inactividad por defecto (referencia del SDK) |
| Control de egress | allowInternetAccess, allowOut / denyOut (IP, CIDR, dominio) | allow-all, deny-all o política propia de dominios y CIDR; actualizable en runtime | block_network, allowlist de CIDR, allowlist de dominios (beta, solo TLS en 443) | networkBlockAll, networkAllowList (máx. 10 CIDR IPv4), domainAllowList |
| Recursos | Default 2 vCPU / 4 GiB; máx. 8 vCPU / 8 GiB | Default 2 vCPU con 2 GB cada una; máx. 4 (Hobby), 8 (Pro), 32 (Enterprise) vCPUs | cpu en cores fraccionarios, memory en MiB, por sandbox | Default 1 vCPU / 1 GB / 3 GiB de disco; máx. de la org 4 vCPU / 8 GB / 10 GB |
| Modelo de precios | Por segundo: $0.000014 por vCPU-s, $0.0000045 por GiB-s; Hobby $0 + $100 de crédito único, Pro $150/mes + uso | CPU activa $0.128/h (las esperas de I/O no se cobran) + memoria $0.0212/GB-h; Hobby con cuotas gratis | Sandboxes a $0.00003942 por core-s y $0.00000667 por GiB-s (unas 3x las functions); Starter con $30/mes de crédito | Por 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)
- Alcance: un sandbox por sesión o tenant no confiable; nunca compartas un sandbox entre usuarios
- 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á
- Datos de entrada: pasá inputs como archivos, no credenciales; mantené los tokens fuera de las env vars y del filesystem
- Presupuestos: fijá la vida del sandbox y el timeout por comando, y acotá vCPUs y memoria
🔍 Después de cada corrida
- Truncá: limitá el largo de stdout y stderr antes de devolverlos al modelo
- Desconfiá: tratá la salida como dato no confiable; un script puede imprimir una prompt injection directo al contexto de tu agente
- 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)
- 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
- 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
- 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
- Vercel — Entendiendo los Sandboxes (aislamiento, ciclo de vida, modelo de seguridad)
- Vercel — Firewall del Sandbox (políticas de red, credentials brokering)
- Vercel — Precios y cuotas de Sandbox
- Vercel — Referencia del SDK de JS de Sandbox
- E2B — Quickstart (SDK de code interpreter)
- E2B — Acceso a internet y reglas de red
- E2B — Ciclo de vida del sandbox y timeouts
- E2B — Precios
- Modal — Guía de Sandboxes
- Modal — Redes en Sandboxes
- Modal — Seguridad y responsabilidad compartida
- Modal — Referencia de Sandbox.create
- Daytona — Sandboxes
- Daytona — Límites de red
- Daytona — Precios



