|
|
import { NextRequest } from 'next/server'; |
|
|
import { spawn } from 'child_process'; |
|
|
import { writeFile, unlink, mkdir, readFile, readdir } from 'fs/promises'; |
|
|
import { join } from 'path'; |
|
|
import { tmpdir } from 'os'; |
|
|
import { randomUUID } from 'crypto'; |
|
|
|
|
|
export const maxDuration = 60; |
|
|
|
|
|
interface ExecuteRequestBody { |
|
|
code: string; |
|
|
timeout?: number; |
|
|
} |
|
|
|
|
|
interface ExecutionResult { |
|
|
success: boolean; |
|
|
output: string; |
|
|
error: string; |
|
|
executionTime: number; |
|
|
hasCircuitOutput: boolean; |
|
|
images?: string[]; |
|
|
} |
|
|
|
|
|
const DANGEROUS_PATTERNS = [ |
|
|
/os\.environ/, |
|
|
/environ\[/, |
|
|
/getenv\s*\(/, |
|
|
|
|
|
/\bctypes\b/, |
|
|
/\bpickle\b/, |
|
|
/\bmarshal\b/, |
|
|
/\bshelve\b/, |
|
|
/\bcommands\b/, |
|
|
/\bpty\b/, |
|
|
/\bpexpect\b/, |
|
|
|
|
|
/\bos\.system\b/, |
|
|
/\bos\.popen\b/, |
|
|
/\bos\.spawn/, |
|
|
/\bos\.exec/, |
|
|
/\bos\.fork\b/, |
|
|
/\bsubprocess\b/, |
|
|
/\bcommands\b/, |
|
|
|
|
|
/open\s*\(\s*['"]\s*\/etc/, |
|
|
/open\s*\(\s*['"]\s*\/proc/, |
|
|
/open\s*\(\s*['"]\s*\/sys/, |
|
|
/open\s*\(\s*['"]\s*\/dev/, |
|
|
/open\s*\(\s*['"]\s*\/var/, |
|
|
/open\s*\(\s*['"]\s*\/root/, |
|
|
/open\s*\(\s*['"]\s*\/home/, |
|
|
/open\s*\(\s*['"]\s*\/tmp/, |
|
|
/open\s*\(\s*['"]\s*\.env/, |
|
|
/open\s*\(\s*['"]\s*\.\.\//, |
|
|
/open\s*\(\s*f?['"]\s*\{/, |
|
|
|
|
|
/\bsocket\b/, |
|
|
/\burllib\b/, |
|
|
/\brequests\b/, |
|
|
/\bhttpx\b/, |
|
|
/\baiohttp\b/, |
|
|
/\bhttp\.client\b/, |
|
|
/\bftplib\b/, |
|
|
/\bsmtplib\b/, |
|
|
/\btelnetlib\b/, |
|
|
/\bparamiko\b/, |
|
|
|
|
|
/\beval\s*\(/, |
|
|
/\bexec\s*\(/, |
|
|
/\bcompile\s*\(/, |
|
|
/\b__import__\b/, |
|
|
/\bimportlib\b/, |
|
|
/\bbuiltins\b/, |
|
|
/\bglobals\s*\(\s*\)/, |
|
|
/\blocals\s*\(\s*\)/, |
|
|
/\bgetattr\s*\([^,]+,\s*['"]/, |
|
|
/\bsetattr\s*\(/, |
|
|
/\bdelattr\s*\(/, |
|
|
|
|
|
/\b__class__\b/, |
|
|
/\b__bases__\b/, |
|
|
/\b__subclasses__\b/, |
|
|
/\b__mro__\b/, |
|
|
/\b__globals__\b/, |
|
|
/\b__code__\b/, |
|
|
/\b__reduce__\b/, |
|
|
/\b__getstate__\b/, |
|
|
/\b__setstate__\b/, |
|
|
|
|
|
/\bmultiprocessing\b/, |
|
|
/\bthreading\b/, |
|
|
/\bconcurrent\b/, |
|
|
/\basyncio\.subprocess/, |
|
|
]; |
|
|
|
|
|
const ALLOWED_PATTERNS = [ |
|
|
/from qiskit/, |
|
|
/import qiskit/, |
|
|
/from numpy/, |
|
|
/import numpy/, |
|
|
/from scipy/, |
|
|
/import scipy/, |
|
|
/from matplotlib/, |
|
|
/import matplotlib/, |
|
|
]; |
|
|
|
|
|
function validateCode(code: string): { valid: boolean; error?: string } { |
|
|
const codeWithoutComments = code |
|
|
.replace(/#.*$/gm, '') |
|
|
.replace(/'''[\s\S]*?'''/g, '') |
|
|
.replace(/"""[\s\S]*?"""/g, ''); |
|
|
|
|
|
for (const pattern of DANGEROUS_PATTERNS) { |
|
|
if (pattern.test(codeWithoutComments)) { |
|
|
return { |
|
|
valid: false, |
|
|
error: `Security error: Potentially dangerous code pattern detected. For security reasons, certain operations are not allowed in the sandbox.` |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
return { valid: true }; |
|
|
} |
|
|
|
|
|
const createSafetyWrapper = (figureDir: string) => ` |
|
|
import sys |
|
|
import io |
|
|
import os |
|
|
import warnings |
|
|
import builtins |
|
|
from contextlib import redirect_stdout, redirect_stderr |
|
|
|
|
|
# Suppress warnings for cleaner output |
|
|
warnings.filterwarnings('ignore') |
|
|
|
|
|
# Setup figure capture directory |
|
|
_FIGURE_DIR = "${figureDir.replace(/\\/g, '\\\\')}" |
|
|
_figure_counter = [0] |
|
|
|
|
|
# ============================================ |
|
|
# SECURITY SANDBOX SETUP (Second Line of Defense) |
|
|
# Primary security is pattern detection + clean environment |
|
|
# ============================================ |
|
|
|
|
|
# Block dangerous system operations |
|
|
os.system = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.system not allowed in sandbox")) |
|
|
os.popen = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.popen not allowed in sandbox")) |
|
|
if hasattr(os, 'spawn'): |
|
|
os.spawn = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawn not allowed")) |
|
|
if hasattr(os, 'spawnl'): |
|
|
os.spawnl = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnl not allowed")) |
|
|
if hasattr(os, 'spawnle'): |
|
|
os.spawnle = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnle not allowed")) |
|
|
if hasattr(os, 'spawnlp'): |
|
|
os.spawnlp = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnlp not allowed")) |
|
|
if hasattr(os, 'spawnlpe'): |
|
|
os.spawnlpe = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnlpe not allowed")) |
|
|
if hasattr(os, 'spawnv'): |
|
|
os.spawnv = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnv not allowed")) |
|
|
if hasattr(os, 'spawnve'): |
|
|
os.spawnve = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnve not allowed")) |
|
|
if hasattr(os, 'spawnvp'): |
|
|
os.spawnvp = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnvp not allowed")) |
|
|
if hasattr(os, 'spawnvpe'): |
|
|
os.spawnvpe = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.spawnvpe not allowed")) |
|
|
if hasattr(os, 'execl'): |
|
|
os.execl = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execl not allowed")) |
|
|
if hasattr(os, 'execle'): |
|
|
os.execle = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execle not allowed")) |
|
|
if hasattr(os, 'execlp'): |
|
|
os.execlp = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execlp not allowed")) |
|
|
if hasattr(os, 'execlpe'): |
|
|
os.execlpe = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execlpe not allowed")) |
|
|
if hasattr(os, 'execv'): |
|
|
os.execv = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execv not allowed")) |
|
|
if hasattr(os, 'execve'): |
|
|
os.execve = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execve not allowed")) |
|
|
if hasattr(os, 'execvp'): |
|
|
os.execvp = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execvp not allowed")) |
|
|
if hasattr(os, 'execvpe'): |
|
|
os.execvpe = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("os.execvpe not allowed")) |
|
|
if hasattr(os, 'fork'): |
|
|
os.fork = lambda: (_ for _ in ()).throw(PermissionError("os.fork not allowed")) |
|
|
if hasattr(os, 'forkpty'): |
|
|
os.forkpty = lambda: (_ for _ in ()).throw(PermissionError("os.forkpty not allowed")) |
|
|
if hasattr(os, 'killpg'): |
|
|
os.killpg = lambda *args: (_ for _ in ()).throw(PermissionError("os.killpg not allowed")) |
|
|
if hasattr(os, 'kill'): |
|
|
os.kill = lambda *args: (_ for _ in ()).throw(PermissionError("os.kill not allowed")) |
|
|
|
|
|
# Block subprocess module |
|
|
try: |
|
|
import subprocess |
|
|
subprocess.run = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) |
|
|
subprocess.call = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) |
|
|
subprocess.check_call = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) |
|
|
subprocess.check_output = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) |
|
|
subprocess.Popen = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) |
|
|
subprocess.getoutput = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) |
|
|
subprocess.getstatusoutput = lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("subprocess not allowed")) |
|
|
except ImportError: |
|
|
pass |
|
|
|
|
|
# Create restricted open function to block access to sensitive files |
|
|
_original_open = builtins.open |
|
|
_ALLOWED_PATHS = [_FIGURE_DIR, '/tmp/quantum-sandbox'] |
|
|
|
|
|
def _restricted_open(file, mode='r', *args, **kwargs): |
|
|
"""Restricted open that blocks access to sensitive files""" |
|
|
if isinstance(file, (str, bytes)): |
|
|
file_str = file if isinstance(file, str) else file.decode() |
|
|
# Don't block relative paths that are needed for library operation |
|
|
if file_str.startswith('/'): |
|
|
file_str_lower = file_str.lower() |
|
|
|
|
|
# Block reading system sensitive paths |
|
|
blocked_prefixes = ['/etc/passwd', '/etc/shadow', '/proc/self', '/proc/1'] |
|
|
for prefix in blocked_prefixes: |
|
|
if file_str_lower.startswith(prefix): |
|
|
raise PermissionError(f"Access to {prefix} is not allowed in sandbox") |
|
|
|
|
|
# Block reading obvious secrets |
|
|
blocked_patterns = ['.env.local', '.env.', 'secrets', 'credentials', 'private_key'] |
|
|
for pattern in blocked_patterns: |
|
|
if pattern in file_str_lower: |
|
|
raise PermissionError(f"Access to files matching '{pattern}' is not allowed in sandbox") |
|
|
|
|
|
return _original_open(file, mode, *args, **kwargs) |
|
|
|
|
|
builtins.open = _restricted_open |
|
|
|
|
|
# ============================================ |
|
|
# END SECURITY SANDBOX SETUP |
|
|
# ============================================ |
|
|
|
|
|
# Capture all output |
|
|
_stdout_capture = io.StringIO() |
|
|
_stderr_capture = io.StringIO() |
|
|
|
|
|
# Setup matplotlib figure capture |
|
|
try: |
|
|
import matplotlib |
|
|
matplotlib.use('Agg') |
|
|
import matplotlib.pyplot as plt |
|
|
|
|
|
_original_show = plt.show |
|
|
_original_savefig = plt.savefig |
|
|
|
|
|
def _capture_show(*args, **kwargs): |
|
|
"""Capture plt.show() calls and save figures""" |
|
|
figs = [plt.figure(i) for i in plt.get_fignums()] |
|
|
for fig in figs: |
|
|
_figure_counter[0] += 1 |
|
|
filepath = os.path.join(_FIGURE_DIR, f"figure_{_figure_counter[0]}.png") |
|
|
fig.savefig(filepath, format='png', dpi=150, bbox_inches='tight', |
|
|
facecolor='#18181b', edgecolor='none', transparent=False) |
|
|
plt.close('all') |
|
|
|
|
|
def _capture_savefig(fname, *args, **kwargs): |
|
|
"""Capture savefig calls""" |
|
|
_figure_counter[0] += 1 |
|
|
filepath = os.path.join(_FIGURE_DIR, f"figure_{_figure_counter[0]}.png") |
|
|
kwargs_copy = dict(kwargs) |
|
|
kwargs_copy['format'] = 'png' |
|
|
kwargs_copy['dpi'] = kwargs_copy.get('dpi', 150) |
|
|
kwargs_copy['bbox_inches'] = kwargs_copy.get('bbox_inches', 'tight') |
|
|
kwargs_copy['facecolor'] = kwargs_copy.get('facecolor', '#18181b') |
|
|
_original_savefig(filepath, **kwargs_copy) |
|
|
|
|
|
plt.show = _capture_show |
|
|
plt.savefig = _capture_savefig |
|
|
|
|
|
# Also capture Qiskit circuit.draw() with mpl output |
|
|
try: |
|
|
from qiskit import QuantumCircuit |
|
|
_original_draw = QuantumCircuit.draw |
|
|
|
|
|
def _capture_draw(self, output=None, **kwargs): |
|
|
result = _original_draw(self, output=output, **kwargs) |
|
|
if output == 'mpl' and result is not None: |
|
|
_figure_counter[0] += 1 |
|
|
filepath = os.path.join(_FIGURE_DIR, f"figure_{_figure_counter[0]}.png") |
|
|
result.savefig(filepath, format='png', dpi=150, bbox_inches='tight', |
|
|
facecolor='#18181b', edgecolor='none') |
|
|
plt.close(result) |
|
|
return result |
|
|
|
|
|
QuantumCircuit.draw = _capture_draw |
|
|
except ImportError: |
|
|
pass |
|
|
|
|
|
except ImportError: |
|
|
pass |
|
|
|
|
|
# Now execute the user code with output capture |
|
|
with redirect_stdout(_stdout_capture), redirect_stderr(_stderr_capture): |
|
|
try: |
|
|
exec(compile(''' |
|
|
__USER_CODE__ |
|
|
''', '<user_code>', 'exec'), {'__builtins__': builtins, '__name__': '__main__'}) |
|
|
except Exception as e: |
|
|
print(f"{type(e).__name__}: {e}", file=sys.stderr) |
|
|
|
|
|
# Final figure capture - save any remaining open figures |
|
|
try: |
|
|
import matplotlib.pyplot as plt |
|
|
figs = [plt.figure(i) for i in plt.get_fignums()] |
|
|
for fig in figs: |
|
|
_figure_counter[0] += 1 |
|
|
filepath = os.path.join(_FIGURE_DIR, f"figure_{_figure_counter[0]}.png") |
|
|
fig.savefig(filepath, format='png', dpi=150, bbox_inches='tight', |
|
|
facecolor='#18181b', edgecolor='none', transparent=False) |
|
|
plt.close('all') |
|
|
except: |
|
|
pass |
|
|
|
|
|
# Print captured output |
|
|
_stdout_result = _stdout_capture.getvalue() |
|
|
_stderr_result = _stderr_capture.getvalue() |
|
|
|
|
|
if _stdout_result: |
|
|
print(_stdout_result, end='') |
|
|
if _stderr_result: |
|
|
print(_stderr_result, end='', file=sys.stderr) |
|
|
`; |
|
|
|
|
|
function createSafeCode(userCode: string, figureDir: string): string { |
|
|
const escapedCode = userCode |
|
|
.replace(/\\/g, '\\\\') |
|
|
.replace(/'''/g, "\\'\\'\\'"); |
|
|
|
|
|
return createSafetyWrapper(figureDir).replace('__USER_CODE__', escapedCode); |
|
|
} |
|
|
|
|
|
function getSafeEnv(): Record<string, string> { |
|
|
const env: Record<string, string> = { |
|
|
PATH: '/usr/bin:/bin:/usr/local/bin', |
|
|
HOME: '/tmp', |
|
|
PYTHONUNBUFFERED: '1', |
|
|
MPLBACKEND: 'Agg', |
|
|
MallocStackLogging: '0', |
|
|
MallocNanoZone: '0', |
|
|
LANG: 'en_US.UTF-8', |
|
|
LC_ALL: 'en_US.UTF-8', |
|
|
}; |
|
|
if (process.env.PYTHON_PATH) { |
|
|
env.PYTHON_PATH = process.env.PYTHON_PATH; |
|
|
} |
|
|
return env; |
|
|
} |
|
|
|
|
|
function detectCircuitOutput(code: string, output: string): boolean { |
|
|
|
|
|
const circuitPatterns = [ |
|
|
/\.draw\(/, |
|
|
/circuit_drawer/, |
|
|
/plot_histogram/, |
|
|
/plot_bloch/, |
|
|
/\.decompose\(\)/, |
|
|
/print.*circuit/i, |
|
|
]; |
|
|
|
|
|
const outputPatterns = [ |
|
|
/[┌─┬┐│├┼┤└┴┘═║╔╗╚╝]/, |
|
|
/q\d*.*[─┤├]/, |
|
|
/[HXYZTSRx].*├/, |
|
|
]; |
|
|
|
|
|
const hasCircuitCode = circuitPatterns.some(p => p.test(code)); |
|
|
const hasCircuitOutput = outputPatterns.some(p => p.test(output)); |
|
|
|
|
|
return hasCircuitCode || hasCircuitOutput; |
|
|
} |
|
|
|
|
|
async function collectFigures(figureDir: string): Promise<string[]> { |
|
|
const images: string[] = []; |
|
|
try { |
|
|
const files = await readdir(figureDir); |
|
|
const pngFiles = files.filter(f => f.endsWith('.png')).sort(); |
|
|
|
|
|
for (const file of pngFiles) { |
|
|
const filepath = join(figureDir, file); |
|
|
const data = await readFile(filepath); |
|
|
images.push(data.toString('base64')); |
|
|
|
|
|
await unlink(filepath).catch(() => { }); |
|
|
} |
|
|
} catch { |
|
|
|
|
|
} |
|
|
return images; |
|
|
} |
|
|
|
|
|
async function executeCode(code: string, timeout: number): Promise<ExecutionResult> { |
|
|
const startTime = Date.now(); |
|
|
const execId = randomUUID(); |
|
|
const tempDir = join(tmpdir(), 'quantum-sandbox'); |
|
|
const figureDir = join(tempDir, `figures_${execId}`); |
|
|
const tempFile = join(tempDir, `exec_${execId}.py`); |
|
|
|
|
|
try { |
|
|
|
|
|
await mkdir(tempDir, { recursive: true }); |
|
|
await mkdir(figureDir, { recursive: true }); |
|
|
|
|
|
|
|
|
const safeCode = createSafeCode(code, figureDir); |
|
|
await writeFile(tempFile, safeCode, 'utf-8'); |
|
|
|
|
|
return await new Promise<ExecutionResult>(async (resolve) => { |
|
|
let stdout = ''; |
|
|
let stderr = ''; |
|
|
let killed = false; |
|
|
|
|
|
|
|
|
const pythonPath = process.env.PYTHON_PATH || 'python3'; |
|
|
|
|
|
const pythonProcess = spawn(pythonPath, [tempFile], { |
|
|
timeout: timeout * 1000, |
|
|
env: getSafeEnv() as NodeJS.ProcessEnv, |
|
|
cwd: tempDir, |
|
|
}); |
|
|
|
|
|
pythonProcess.stdout.on('data', (data: Buffer) => { |
|
|
stdout += data.toString(); |
|
|
}); |
|
|
|
|
|
pythonProcess.stderr.on('data', (data: Buffer) => { |
|
|
stderr += data.toString(); |
|
|
}); |
|
|
|
|
|
const timeoutId = setTimeout(() => { |
|
|
killed = true; |
|
|
pythonProcess.kill('SIGKILL'); |
|
|
}, timeout * 1000); |
|
|
|
|
|
pythonProcess.on('close', async (exitCode: number | null) => { |
|
|
clearTimeout(timeoutId); |
|
|
const executionTime = Date.now() - startTime; |
|
|
|
|
|
|
|
|
const images = await collectFigures(figureDir); |
|
|
|
|
|
if (killed) { |
|
|
resolve({ |
|
|
success: false, |
|
|
output: stdout, |
|
|
error: `Execution timeout (>${timeout}s). The code took too long to execute.`, |
|
|
executionTime, |
|
|
hasCircuitOutput: false, |
|
|
images, |
|
|
}); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const cleanStderr = stderr |
|
|
.split('\n') |
|
|
.filter(line => !line.includes('UserWarning') && |
|
|
!line.includes('DeprecationWarning') && |
|
|
!line.includes('FutureWarning')) |
|
|
.join('\n') |
|
|
.trim(); |
|
|
|
|
|
const success = exitCode === 0 && !cleanStderr; |
|
|
|
|
|
resolve({ |
|
|
success, |
|
|
output: stdout.trim(), |
|
|
error: cleanStderr, |
|
|
executionTime, |
|
|
hasCircuitOutput: detectCircuitOutput(code, stdout), |
|
|
images, |
|
|
}); |
|
|
}); |
|
|
|
|
|
pythonProcess.on('error', (err: Error) => { |
|
|
clearTimeout(timeoutId); |
|
|
resolve({ |
|
|
success: false, |
|
|
output: '', |
|
|
error: `Failed to start Python: ${err.message}`, |
|
|
executionTime: Date.now() - startTime, |
|
|
hasCircuitOutput: false, |
|
|
images: [], |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
} finally { |
|
|
|
|
|
try { |
|
|
await unlink(tempFile); |
|
|
} catch { |
|
|
|
|
|
} |
|
|
try { |
|
|
|
|
|
const files = await readdir(figureDir).catch(() => []); |
|
|
for (const file of files) { |
|
|
await unlink(join(figureDir, file)).catch(() => { }); |
|
|
} |
|
|
await unlink(figureDir).catch(() => { }); |
|
|
} catch { |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
export async function POST(request: NextRequest) { |
|
|
try { |
|
|
const body: ExecuteRequestBody = await request.json(); |
|
|
const { code, timeout = 30 } = body; |
|
|
|
|
|
if (!code || typeof code !== 'string') { |
|
|
return new Response( |
|
|
JSON.stringify({ error: 'Invalid request: code string required' }), |
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } } |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
if (code.length > 50000) { |
|
|
return new Response( |
|
|
JSON.stringify({ error: 'Code too long (max 50KB)' }), |
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } } |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
const validation = validateCode(code); |
|
|
if (!validation.valid) { |
|
|
return new Response( |
|
|
JSON.stringify({ |
|
|
success: false, |
|
|
output: '', |
|
|
error: validation.error, |
|
|
executionTime: 0, |
|
|
hasCircuitOutput: false, |
|
|
images: [], |
|
|
}), |
|
|
{ status: 400, headers: { 'Content-Type': 'application/json' } } |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
const safeTimeout = Math.min(Math.max(timeout, 5), 60); |
|
|
|
|
|
const result = await executeCode(code, safeTimeout); |
|
|
|
|
|
return new Response(JSON.stringify(result), { |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
}); |
|
|
} catch (error) { |
|
|
console.error('Execute API error:', error); |
|
|
|
|
|
return new Response( |
|
|
JSON.stringify({ |
|
|
success: false, |
|
|
output: '', |
|
|
error: error instanceof Error ? error.message : 'Execution failed', |
|
|
executionTime: 0, |
|
|
hasCircuitOutput: false, |
|
|
images: [], |
|
|
}), |
|
|
{ status: 500, headers: { 'Content-Type': 'application/json' } } |
|
|
); |
|
|
} |
|
|
} |
|
|
|