Spaces:
Paused
Paused
| /** | |
| * 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, | |
| })); | |
| } | |