/** * sandbox.js — Test-Before-Execute para Zelin * ============================================= * Basado en: Ralph Loop (Alibaba, 2025) + ReflexiCoder * "Each iteration: implement → check syntax → sandbox test → only execute if passes" * * CAPACIDADES: * 1. Verificar sintaxis JS antes de ejecutar (node --check) * 2. Ejecutar código en sandbox seguro (vm.runInNewContext, sin acceso a fs/net) * 3. Auto-corregir hasta 3 intentos usando el error como contexto * 4. Ejecutar comandos seguros del servidor Node.js (restart, reload config, stats) * * LÍMITES DE SEGURIDAD: * - Sandbox sin acceso a require, fs, net, process, child_process * - Timeout de 5 segundos máximo * - Comandos del servidor: solo lista blanca explícita * - NUNCA ejecutar comandos de shell arbitrarios */ import vm from 'vm'; import { execSync } from 'child_process'; import { writeFileSync, unlinkSync, existsSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { callAI } from './ai.js'; import * as db from './db.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // ── Sandbox seguro ──────────────────────────────────────────────────────────── export function runInSandbox(code, timeoutMs = 5000) { const result = { success: false, output: null, error: null }; try { // Context mínimo — solo matemática y estructuras de datos const ctx = vm.createContext({ Math, JSON, Date, console: { log: (...a) => result.output = (result.output ?? '') + a.join(' ') + '\n', error: () => {} }, setTimeout: undefined, setInterval: undefined, // sin timers require : undefined, process : undefined, // sin acceso al sistema __sandbox : true, }); vm.runInContext(code, ctx, { timeout: timeoutMs, displayErrors: true }); result.success = true; } catch (e) { result.error = e.message; } return result; } // ── Verificación de sintaxis (node --check) ─────────────────────────────────── export function checkSyntax(code) { const tmpFile = path.join('/tmp', `zelin_check_${Date.now()}.js`); try { writeFileSync(tmpFile, code); execSync(`node --check "${tmpFile}"`, { timeout: 5000 }); return { valid: true }; } catch (e) { const errorLine = e.stderr?.toString() ?? e.message; return { valid: false, error: errorLine.split('\n').slice(0, 5).join('\n') }; } finally { try { unlinkSync(tmpFile); } catch {} } } // ── Ciclo test-before-execute con auto-corrección ──────────────────────────── // Implementa el Ralph Loop: genera → verifica → sandbox → corrige si falla export async function testAndFix(code, purpose, maxAttempts = 3) { let currentCode = code; const history = []; for (let attempt = 1; attempt <= maxAttempts; attempt++) { console.log(`[Sandbox] Intento ${attempt}/${maxAttempts}: verificando...`); // PASO 1: syntax check const syntaxResult = checkSyntax(currentCode); if (!syntaxResult.valid) { const errorCtx = `Error de sintaxis en intento ${attempt}:\n${syntaxResult.error}`; history.push({ attempt, error: syntaxResult.error, type: 'syntax' }); console.warn(`[Sandbox] Sintaxis inválida: ${syntaxResult.error.slice(0, 100)}`); if (attempt < maxAttempts) { // Auto-corrección: pasar el error a la IA para que lo arregle currentCode = await fixCodeWithAI(currentCode, errorCtx, purpose, history); continue; } return { success: false, error: syntaxResult.error, attempts: attempt, code: currentCode }; } // PASO 2: sandbox test (solo para código pequeño y sin side effects) if (currentCode.length < 2000 && !currentCode.includes('await')) { const sandboxResult = runInSandbox(currentCode); if (!sandboxResult.success) { history.push({ attempt, error: sandboxResult.error, type: 'runtime' }); console.warn(`[Sandbox] Error runtime: ${sandboxResult.error}`); if (attempt < maxAttempts) { currentCode = await fixCodeWithAI(currentCode, `Error runtime: ${sandboxResult.error}`, purpose, history); continue; } return { success: false, error: sandboxResult.error, attempts: attempt, code: currentCode }; } console.log(`[Sandbox] ✅ Sandbox OK. Output: ${sandboxResult.output?.slice(0, 100) ?? 'ninguno'}`); } // PASO 3: pasa todos los checks console.log(`[Sandbox] ✅ Código verificado en ${attempt} intento(s)`); return { success: true, code: currentCode, attempts: attempt }; } return { success: false, error: 'Máximo de intentos alcanzado', attempts: maxAttempts, code: currentCode }; } // ── Auto-corrección de código via IA ───────────────────────────────────────── async function fixCodeWithAI(code, errorMessage, purpose, history) { const historyText = history.slice(-2).map(h => `Intento ${h.attempt} (${h.type}): ${h.error.slice(0, 150)}` ).join('\n'); try { const fixed = await callAI([ { role : 'system', content: `Eres un experto en JavaScript/Node.js. Arregla el código que tiene un error. Devuelve SOLO el código corregido, sin explicaciones, sin backticks markdown. El código debe ser JavaScript válido y seguro.`, }, { role : 'user', content: `Propósito del código: ${purpose} Código con error: ${code} Error encontrado: ${errorMessage} ${historyText ? `Intentos previos:\n${historyText}` : ''} Código corregido:`, }, ], 'code', 800); // Limpiar el código de posibles backticks que la IA añada return fixed.replace(/^```(?:javascript|js)?\n?/, '').replace(/\n?```$/, '').trim(); } catch { return code; // si falla la IA, mantener el código anterior } } // ═══════════════════════════════════════════════════════════════════════════════ // COMANDOS DEL SERVIDOR NODE.JS // ═══════════════════════════════════════════════════════════════════════════════ // Lista blanca explícita — NUNCA comandos arbitrarios de shell const SERVER_COMMANDS = { // ── Estado e información ──────────────────────────────────────────────────── 'stats': { description: 'Estadísticas del proceso Node.js (RAM, uptime, CPU)', safe : true, execute : () => { const mem = process.memoryUsage(); return { uptime : Math.round(process.uptime()) + 's', heapUsedMB : (mem.heapUsed / 1024 / 1024).toFixed(1) + 'MB', heapTotalMB: (mem.heapTotal / 1024 / 1024).toFixed(1) + 'MB', rssMB : (mem.rss / 1024 / 1024).toFixed(1) + 'MB', nodeVersion: process.version, pid : process.pid, }; }, }, 'gc': { description: 'Forzar garbage collection (libera RAM)', safe : true, execute : () => { if (global.gc) { global.gc(); return { done: true, msg: 'GC ejecutado' }; } return { done: false, msg: 'GC no disponible (iniciar con --expose-gc)' }; }, }, 'reload_config': { description: 'Recargar config.json sin reiniciar el bot', safe : true, execute : async () => { const { readConfig } = await import('./utils.js'); const cfg = readConfig(); return { done: true, keys: Object.keys(cfg) }; }, }, 'clear_cache_ai': { description: 'Limpiar cache de IA', safe : true, execute : async () => { const { clearCache } = await import('./ai.js'); clearCache(); return { done: true }; }, }, 'db_stats': { description: 'Estadísticas de la base de datos Turso', safe : true, execute : async () => { const count = await db.db.execute({ sql: 'SELECT COUNT(*) as n FROM zelin_memory', args: [] }); const interactions = await db.db.execute({ sql: 'SELECT COUNT(*) as n FROM messages WHERE is_deleted = 0', args: [] }).catch(() => ({ rows: [{ n: '?' }] })); return { memoryRows: count.rows[0]?.n, interactions: interactions.rows[0]?.n }; }, }, 'check_syntax': { description: 'Verificar sintaxis de un archivo JS del proyecto', safe : true, requiresParam: 'filename', // solo archivos dentro de src/ execute : (filename) => { const safe = path.join(__dirname, path.basename(filename)); if (!existsSync(safe)) return { valid: false, error: 'Archivo no encontrado' }; return checkSyntax(require('fs').readFileSync(safe, 'utf8')); }, }, }; // Ejecutar un comando del servidor export async function executeServerCommand(commandName, param = null) { const cmd = SERVER_COMMANDS[commandName]; if (!cmd) { return { success: false, error: `Comando desconocido: ${commandName}. Disponibles: ${Object.keys(SERVER_COMMANDS).join(', ')}` }; } try { const result = cmd.requiresParam ? await cmd.execute(param) : await cmd.execute(); return { success: true, command: commandName, result }; } catch (e) { return { success: false, command: commandName, error: e.message }; } } export function listServerCommands() { return Object.entries(SERVER_COMMANDS).map(([name, cmd]) => ({ name, description: cmd.description, safe : cmd.safe, })); }