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.
wrangler deploy// 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>; }
| 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 |
*.sandbox.analyticai.com en Cloudflare si usas Preview URLssleepAfterworkers.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.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.