CF
Cloudflare Sandbox SDK Entorno de ejecución seguro para IA
● Implementación activa
◆ IA-Ingenieria-MLOps · Nivel Avanzado

Ejecución de código no confiable
aislado y seguro en el edge

Arquitectura completa para AnalyticaAI SaaS: sandboxing por sesión de usuario, intérprete Python con pandas/sklearn, operaciones de archivos CSV y Preview URLs — todo sobre Cloudflare Workers.

Docker local listo
@cloudflare/sandbox v0.7.0
Wrangler 3.x
TypeScript estricto
📊
AnalyticaAI — SaaS de análisis de datos B2B
~800 clientes · Equipos de marketing y finanzas · CSVs hasta 50 MB · Código Python generado por usuarios
Riesgo actual: escape de datos Migración a sandbox
0
Aislamiento actual entre usuarios
exec() compartido sin sandbox
1
Sandbox por sesión de usuario
↑ getSandbox(env, userId)
10m
Sleep automático por inactividad
↑ sleepAfter configurable
50MB
CSV por sesión soportado
↑ writeFile() en /workspace
Flujo de ejecución aislada
Cada request de usuario obtiene su propio Durable Object con contenedor Docker aislado. Ciclo de vida lazy: el contenedor arranca en la primera operación.
🌐
React Frontend
Vite · usuario sube CSV + escribe Python
CF Worker
analytic-worker.ts · rutas POST /run /upload
🔑
getSandbox()
env.Sandbox · key = userId
DURABLE OBJECTS + CONTAINERS
📦
Sandbox user-A
Python 3.11 · pandas · /workspace/A
📦
Sandbox user-B
Python 3.11 · pandas · /workspace/B
💤
Sandbox user-C
sleeping · 10m sin actividad
🛡️
Aislamiento total: Cada usuario tiene su propio sistema de ficheros y proceso Python. Imposible acceder a datos de otro usuario.
💾
Estado persistente: El contexto Python (createCodeContext) mantiene variables entre llamadas dentro de la misma sesión.
🔄
Lazy init: getSandbox() retorna inmediatamente. El contenedor Docker solo arranca en la primera operación efectiva.
5 fases de una sesión de análisis
Desde la subida del CSV hasta la liberación de recursos.
📁
1
Obtener sandbox
Crear o recuperar el sandbox del usuario
getSandbox(env, userId)
📤
2
Subir CSV
Escribir el archivo de datos en /workspace
writeFile('/workspace/data.csv')
▶️
3
Crear contexto
Inicializar intérprete Python con estado
createCodeContext({lang:'python'})
🔬
4
Ejecutar código
runCode() con el Python del usuario. Resultados ricos: tablas, valores
runCode(userCode, {context})
🧹
5
Destruir
Liberar recursos inmediatamente al terminar la sesión
sandbox.destroy()
Implementación
Código de producción — Worker TypeScript
Archivos listos para deploy con wrangler deploy
analytic-worker.ts
wrangler.jsonc
Dockerfile
types.ts
// analytic-worker.ts — AnalyticaAI Sandbox Worker
// Deploy: wrangler deploy

import { getSandbox } from '@cloudflare/sandbox';
import type { Env, RunCodeRequest, RunCodeResponse } from './types';

// REQUIRED: re-exportar la clase Sandbox o el Worker no despliega
export { Sandbox } from '@cloudflare/sandbox';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const userId = request.headers.get('X-User-Id') ?? 'anonymous';

    // Obtener sandbox aislado por usuario (lazy init)
    const sandbox = getSandbox(env.Sandbox, userId);

    if (url.pathname === '/upload' && request.method === 'POST') {
      return handleUpload(request, sandbox, userId);
    }

    if (url.pathname === '/run' && request.method === 'POST') {
      return handleRunCode(request, sandbox);
    }

    if (url.pathname === '/destroy' && request.method === 'POST') {
      await sandbox.destroy();
      return Response.json({ ok: true });
    }

    return new Response('Not found', { status: 404 });
  }
};

// ─── Upload CSV ────────────────────────────────────────────────────────────
async function handleUpload(
  req: Request,
  sandbox: Sandbox,
  userId: string
): Promise<Response> {
  const form = await req.formData();
  const file = form.get('file') as File;

  if (!file) return Response.json({ error: 'No file provided' }, { status: 400 });
  if (file.size > 50 * 1024 * 1024) // 50 MB limit
    return Response.json({ error: 'File too large (max 50 MB)' }, { status: 413 });

  const content = new Uint8Array(await file.arrayBuffer());
  const path = `/workspace/${userId}/data.csv`;

  await sandbox.mkdir(`/workspace/${userId}`, { recursive: true });
  await sandbox.writeFile(path, content);

  return Response.json({ ok: true, path, size: file.size });
}

// ─── Execute User Code (Python) ────────────────────────────────────────────
async function handleRunCode(
  req: Request,
  sandbox: Sandbox
): Promise<Response> {
  const { code, sessionId } = await req.json() as RunCodeRequest;

  // Crear contexto Python con estado persistente por sesión
  const ctx = await sandbox.createCodeContext({ language: 'python' });

  // Pre-cargar pandas y apuntar al CSV del usuario
  await sandbox.runCode(`
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
df = pd.read_csv('/workspace/${sessionId}/data.csv')
print(f"Dataset cargado: {df.shape[0]} filas, {df.shape[1]} columnas")
  `, { context: ctx });

  // Ejecutar el código del usuario (no confiable) de forma segura
  const result = await sandbox.runCode(code, { context: ctx });

  const response: RunCodeResponse = {
    success: !result.error,
    results: result.results,
    stdout: result.stdout,
    error: result.error
  };

  return Response.json(response);
}
// wrangler.jsonc — Configuración exacta requerida por el SDK
{
  "name": "analyticai-worker",
  "main": "src/analytic-worker.ts",
  "compatibility_date": "2025-06-01",
  "compatibility_flags": ["nodejs_compat"],

  // REQUERIDO: definición del contenedor sandbox
  "containers": [{
    "class_name": "Sandbox",
    "image": "./Dockerfile",
    "instance_type": "standard",  // "lite" para dev, "standard" para prod
    "max_instances": 50           // hasta 50 sandboxes paralelos (≈800 users)
  }],

  // REQUERIDO: Durable Object binding
  "durable_objects": {
    "bindings": [{
      "class_name": "Sandbox",
      "name": "Sandbox"
    }]
  },

  // REQUERIDO: SQLite migration para Durable Objects
  "migrations": [{
    "new_sqlite_classes": ["Sandbox"],
    "tag": "v1"
  }],

  // Preview URLs (producción): requiere wildcard DNS *.analyticai.com
  "preview_urls": {
    "domain": "sandbox.analyticai.com"
  }
}
# Dockerfile — AnalyticaAI Sandbox
# Base: Python 3.11 + Node 20 incluidos en la imagen oficial

FROM docker.io/cloudflare/sandbox:0.7.0

# ── Data Science stack ───────────────────────────────────────────────────
RUN pip install --no-cache-dir \
    pandas==2.2.2 \
    numpy==1.26.4 \
    matplotlib==3.9.0 \
    seaborn==0.13.2 \
    scikit-learn==1.5.0 \
    scipy==1.13.1 \
    openpyxl==3.1.3

# ── Limpiar caché de pip para reducir imagen ─────────────────────────────
RUN pip cache purge

# ── Puerto requerido para Preview URLs en dev local ──────────────────────
EXPOSE 8080

# ── Variables de entorno de seguridad ────────────────────────────────────
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    MPLBACKEND=Agg

# Tamaño final estimado: ~1.2 GB
# Cold start: ~800ms en instance_type=standard
# NOTA: evitar apt-get adicional — cada layer aumenta cold start
// types.ts — Tipos TypeScript para el Worker

import type { DurableObjectNamespace } from '@cloudflare/workers-types';

export interface Env {
  /** Durable Object namespace para los sandboxes */
  Sandbox: DurableObjectNamespace;
}

export interface RunCodeRequest {
  /** Código Python enviado por el usuario */
  code: string;
  /** ID de sesión = userId para aislar archivos */
  sessionId: string;
}

export interface CodeResult {
  type: 'text' | 'html' | 'image/png';
  text?: string;
  data?: string;  // base64 para imágenes
}

export interface RunCodeResponse {
  success: boolean;
  results: CodeResult[];
  stdout?: string;
  error?: string;
}

/** Tipo de retorno de getSandbox() */
export interface Sandbox {
  exec(cmd: string): Promise<{ stdout: string; stderr: string; exitCode: number; success: boolean }>;
  runCode(code: string, opts?: { context?: CodeContext }): Promise<RunCodeResponse>;
  createCodeContext(opts: { language: 'python' | 'javascript' | 'typescript' }): Promise<CodeContext>;
  writeFile(path: string, content: string | Uint8Array): Promise<void>;
  readFile(path: string): Promise<Uint8Array>;
  mkdir(path: string, opts?: { recursive: boolean }): Promise<void>;
  listFiles(path: string): Promise<string[]>;
  exposePort(port: number): Promise<{ url: string }>;
  destroy(): Promise<void>;
}
¿Cuándo usar exec() vs runCode()?
Guía de decisión para el equipo de AnalyticaAI según el tipo de tarea.
Caso de uso Método recomendado Razón Outputs posibles Estado persiste
Código Python del usuario (análisis, ML) runCode() Outputs ricos: tablas, gráficos base64, valores tipados text, html, image/png ✓ Con context
Instalar dependencias extra en runtime exec() Necesita exit code y stderr para saber si pip instaló bien stdout, stderr, exitCode Sistema de ficheros
Pipeline de datos (ETL, validación CSV) exec() Control directo del proceso, stderr detallado stdout, stderr, exitCode Ficheros
Análisis exploratorio multi-paso (EDA) runCode() + context Variables de pandas persisten entre celdas (como Jupyter) DataFrames, plots, valores ✓ Variables Python
Ejecutar test suite del código del usuario exec() Exit code determina pass/fail; stderr captura stack traces stdout, exitCode (0/1) Ficheros
Servicio HTTP en el sandbox (dashboard) exec() + exposePort() Arrancar servidor y obtener Preview URL pública Preview URL ✓ Puerto vivo
Antipatrones críticos para AnalyticaAI
Errores frecuentes al migrar de exec() compartido al SDK.
🚨
IDs de sandbox hardcodeados (multi-usuario)
✗ MAL — todos usan el mismo sandbox
// Un sandbox para TODOS los usuarios const sandbox = getSandbox( env.Sandbox, 'analytics-sandbox' // ← FIJO: riesgo de escape! );
✓ BIEN — aislamiento por usuario
// Un sandbox aislado POR usuario const userId = req.headers .get('X-User-Id')!; const sandbox = getSandbox( env.Sandbox, userId // ← dinámico: aislado );
🔒
Olvidar exportar la clase Sandbox
✗ MAL — Worker falla en deploy
import { getSandbox } from '@cloudflare/sandbox'; // Sin export: ERROR en wrangler deploy export default { fetch }; // "Sandbox class not found"
✓ BIEN — export requerido
import { getSandbox } from '@cloudflare/sandbox'; // REQUERIDO o el Worker no despliega export { Sandbox } from '@cloudflare/sandbox'; export default { fetch };
💸
No destruir sandboxes temporales
✗ MAL — fuga de recursos y costes
async function runOneshot(code) { const sandbox = getSandbox( env.Sandbox, randomId() ); const result = await sandbox.runCode(code); return result; // sandbox nunca se destruye ← leak! }
✓ BIEN — cleanup explícito
async function runOneshot(code) { const sandbox = getSandbox( env.Sandbox, crypto.randomUUID() ); try { return await sandbox.runCode(code); } finally { await sandbox.destroy(); // siempre } }
📡
Preview URLs en dominio .workers.dev
✗ MAL — subdomains no soportados
// wrangler.jsonc en producción: "preview_urls": { "domain": "analyticai.workers.dev" // ← NO funciona: workers.dev no // soporta wildcard subdomains }
✓ BIEN — dominio propio con wildcard
// DNS: *.sandbox.analyticai.com → CF // wrangler.jsonc: "preview_urls": { "domain": "sandbox.analyticai.com" // ← requiere wildcard DNS en CF }

✅ Checklist de deploy para AnalyticaAI

  • Docker corriendo localmente — wrangler dev necesita Docker para simular contenedores
  • export { Sandbox } en el entry point del Worker
  • wrangler.jsonc con containers, durable_objects y migrations
  • Dockerfile extiende cloudflare/sandbox:0.7.0 con stack data science
  • userId dinámico como sandboxId — nunca hardcodeado
  • destroy() en finally para sandboxes de uso único
  • Wildcard DNS *.sandbox.analyticai.com en Cloudflare si usas Preview URLs
  • NO usar CommandClient/FileClient internos — usar sandbox.* methods

⚡ Ciclo de vida del contenedor

  • getSandbox() retorna inmediatamente — sin espera, sin cold start aún
  • Primera operación (exec/runCode/writeFile) dispara el arranque del contenedor Docker
  • Cold start ~800ms con imagen data science (pandas + sklearn). Calentar con prefetch en login
  • Sleep automático tras 10 minutos de inactividad — configurable con sleepAfter
  • Mismo sandboxId siempre retorna el mismo Durable Object — estado persistente entre requests
  • destroy() libera recursos inmediatamente — recomendado en sesiones de análisis one-shot
  • max_instances=50 en wrangler.jsonc cubre los ~800 usuarios activos de AnalyticaAI con margen
⚠️
Producción — Preview URLs: El dominio workers.dev NO soporta subdominios wildcard. Para exponer servicios HTTP del sandbox en producción, configura un dominio propio en Cloudflare con DNS *.sandbox.analyticai.com → CF y añádelo en wrangler.jsonc bajo preview_urls.domain.
💡
Integración futura — OpenAI Agents SDK: El SDK expone helpers Shell y Editor vía @cloudflare/sandbox/openai para conectar agentes LLM directamente al sandbox. Esto permite que el agente de AnalyticaAI proponga, ejecute y corrija código de análisis de forma autónoma y aislada.