#!/usr/bin/env node // sandbox-runner.js — rootless isolated execution (bwrap if available, else direct) // Listens 127.0.0.1:9091 POST /run {code, language, timeoutMs} const http = require('http'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawn } = require('child_process'); const PORT = parseInt(process.env.SANDBOX_RUNNER_PORT || '9091', 10); const TMP_BASE = process.env.SANDBOX_TMP || '/tmp/sandbox'; const PYTHON_BIN = process.env.PYTHON_BIN || '/opt/runners/task-runner-python/.venv/bin/python'; const NODE_BIN = process.env.NODE_BIN || '/usr/local/bin/node'; function hasBwrap(){ try{ fs.accessSync('/usr/bin/bwrap', fs.constants.X_OK); return true;}catch{ return false; } } function canUseBwrap(){ if (!hasBwrap()) return false; // test if unprivileged namespaces allowed (kernel) try{ const { spawnSync } = require('child_process'); const r = spawnSync('/usr/bin/bwrap', ['--unshare-all','--die-with-parent','--ro-bind','/','/','--tmpfs','/tmp','--','/bin/true'], {timeout:2000}); return r.status===0; }catch{ return false; } } const USE_BWRAP = canUseBwrap(); console.log(`[sandbox-runner] bwrap=${USE_BWRAP} python=${PYTHON_BIN}`); function runInSandbox(code, language, timeoutMs=30000){ return new Promise((resolve)=>{ const id = Math.random().toString(36).slice(2,9); const dir = path.join(TMP_BASE, id); fs.mkdirSync(dir, {recursive:true}); const timeout = Math.min(Math.max(timeoutMs, 1000), 120000); let cmd, args, opts={}; // Write code file if (language==='python' || language==='py'){ const f = path.join(dir,'script.py'); fs.writeFileSync(f, code); if (USE_BWRAP){ cmd='/usr/bin/bwrap'; args=['--unshare-all','--die-with-parent','--ro-bind','/','/','--tmpfs','/tmp','--proc','/proc','--dev','/dev','--bind',dir,dir,'--chdir',dir,'--', PYTHON_BIN, '-I','-B', f]; } else { cmd=PYTHON_BIN; args=['-I','-B', f]; opts.cwd=dir; } } else { // javascript / typescript const f = path.join(dir,'script.js'); fs.writeFileSync(f, code); if (USE_BWRAP){ cmd='/usr/bin/bwrap'; args=['--unshare-all','--die-with-parent','--ro-bind','/','/','--tmpfs','/tmp','--proc','/proc','--dev','/dev','--bind',dir,dir,'--chdir',dir,'--', NODE_BIN, '--disallow-code-generation-from-strings','--disable-proto=delete', f]; } else { cmd=NODE_BIN; args=['--disallow-code-generation-from-strings','--disable-proto=delete', f]; opts.cwd=dir; } } let stdout='', stderr='', killed=false; const child = spawn(cmd, args, {...opts, timeout}); const timer = setTimeout(()=>{ killed=true; try{child.kill('SIGKILL');}catch{} }, timeout+500); child.stdout.on('data',d=>{ stdout += d.toString().slice(0,200000); }); child.stderr.on('data',d=>{ stderr += d.toString().slice(0,200000); }); child.on('close',(code)=>{ clearTimeout(timer); // cleanup try{ fs.rmSync(dir,{recursive:true,force:true});}catch{} resolve({ exitCode: killed? 124 : (code===null?1:code), stdout: stdout.slice(0,100000), stderr: stderr.slice(0,100000) + (killed? `\n[timeout ${timeout}ms]`:'' ), executionId: id, bwrap: USE_BWRAP }); }); child.on('error',(e)=>{ clearTimeout(timer); try{ fs.rmSync(dir,{recursive:true,force:true});}catch{} resolve({exitCode:1, stdout:'', stderr: e.message, executionId:id}); }); }); } const server=http.createServer((req,res)=>{ const url=new URL(req.url,`http://localhost:${PORT}`); if ((url.pathname==='/'||url.pathname==='/healthz'||url.pathname==='/health') && req.method==='GET'){ res.writeHead(200,{'Content-Type':'application/json'}); res.end(JSON.stringify({status:'ok', bwrap:USE_BWRAP, pid:process.pid})); return; } if (url.pathname==='/run' && req.method==='POST'){ let body=''; req.on('data',c=>body+=c); req.on('end', async()=>{ try{ const {code, language, timeoutMs}=JSON.parse(body||'{}'); if(!code){ res.writeHead(400); res.end(JSON.stringify({error:'missing code'})); return;} const r=await runInSandbox(code, language||'python', timeoutMs||30000); res.writeHead(200,{'Content-Type':'application/json'}); res.end(JSON.stringify(r)); }catch(e){ res.writeHead(500); res.end(JSON.stringify({error:e.message}));} }); return; } res.writeHead(404); res.end(JSON.stringify({error:'not found'})); }); server.listen(PORT,'127.0.0.1',()=>console.log(`[sandbox-runner] listening 127.0.0.1:${PORT}`)); process.on('SIGTERM',()=>server.close(()=>process.exit(0)));