zelin-bot / src /sandbox.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* 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,
}));
}