eval-workspace
v1.5.0
Published
A local HTTP service for sandboxed multi-language code execution (Python, JS, Java, Rust) with auto-provisioning toolchains. Zero runtime dependencies.
Downloads
155
Maintainers
Readme
eval-workspace
A local HTTP service for running untrusted code in Python, JavaScript, Java, and Rust, with resource limits (CPU time, memory, wall-clock timeout, process count) and automatic toolchain provisioning — no root required.
This is the Node.js port of eval_workspace (the original Python version). Same design, zero runtime dependencies in both.
What it does
- Multi-language execution:
POST /runwith{"language": "python", "code": "..."}, get back stdout/stderr/exit code - Local sandboxing: each execution runs via a
bash -cwrapper applyingulimitbefore exec:- CPU time limit (
ulimit -t) - Memory limit (
ulimit -v) for Python/Java/Rust; Node subprocesses use--max-old-space-sizeinstead (V8 reserves large virtual address space upfront, soulimit -vkills it before user code even runs) - Process count limit (fork-bomb protection,
ulimit -u) - Wall-clock timeout via
SIGKILLto the whole process group (catches processes waiting on I/O rather than burning CPU) - No inherited environment variables
- Fresh temp directory per execution, deleted after
- CPU time limit (
- Auto-provisioning toolchains: if a language runtime isn't found on
PATH, it's downloaded as an official prebuilt binary into~/.eval_workspace/toolchains/— no system package manager, no root:- Node.js — official prebuilt tarball from nodejs.org (for spawning sandboxed JS subprocesses, separate from whatever Node runs this tool itself)
- Java — Temurin JDK prebuilt tarball from Adoptium
- Rust — via
rustup, which already installs to user-space by design - Python — detected only; not auto-installed (no good universal static build to fetch)
- Zero runtime dependencies — built entirely on Node's built-in
http,child_process,fs, andhttpsmodules, plus the systemtarandbashbinaries (present by default on virtually all Linux/macOS systems)
Why bash specifically, not sh
Resource limits are applied via the POSIX ulimit shell builtin. On Debian/Ubuntu-family systems, /bin/sh is usually dash, whose ulimit implementation does not support -u (max process count) — this was caught during testing (dash rejected it as "Illegal option"). bash's ulimit supports the full flag set this tool needs, so the sandboxing wrapper invokes bash explicitly rather than relying on whatever sh happens to be symlinked to.
Honest limitations — read this before relying on it
This is local (ulimit-based) sandboxing, not container/VM isolation. It reliably stops:
- Infinite loops (CPU limit + wall-clock timeout)
- Memory bombs (memory limit)
- Fork bombs (process count limit)
- Runaway processes waiting on stdin/network (wall-clock timeout)
It does not provide the same guarantees as Docker, gVisor, or Firecracker against a genuinely adversarial payload. There's no filesystem or namespace isolation beyond a scratch temp directory, and no syscall filtering. If your threat model includes deliberately hostile code trying to escape the sandbox (not just buggy/runaway code), you need real container/VM isolation — this tool does not provide that.
Java requires a specific class name. Submitted Java code must define public class Main.
Network access is required to auto-install missing toolchains. If unavailable, auto-install fails cleanly with a structured error rather than hanging or crashing — but it can't conjure a compiler out of nowhere either.
What's actually been tested vs. what hasn't
Tested end-to-end, with real HTTP requests and real subprocess execution:
- Python execution (basic output, errors) — via real HTTP calls to a running server
- JavaScript execution via real Node.js — via real HTTP calls to a running server
- CPU-limit enforcement (infinite loop correctly killed, correctly attributed)
- Wall-clock timeout enforcement (sleeping process correctly killed)
- Memory-limit enforcement (large allocation correctly triggers a clean failure)
- The
dashvsbashulimit -uincompatibility (caught in testing, fixed by invoking bash explicitly) - Clean error handling for missing toolchains, blocked network during install, malformed JSON, missing fields, oversized request bodies, and unknown routes
- Limit-clamping logic (abuse-scale requests get capped to sane maximums)
- The actual
/run,/languages, and/healthHTTP endpoints, running live
Not testable in the environment this was built in, and not yet verified on a real machine:
- Java compilation + execution (the build environment had a JRE but no JDK)
- Rust compilation + execution (Rust wasn't installed, and the sandbox had no network to fetch it)
- The actual toolchain download/extract logic for Node and JDK (needs real network access to confirm the URLs and archive layouts are exactly right)
If you're the first to run this somewhere with real network and a full toolchain set, treat the untested parts above as "should work, please verify" rather than "confirmed working."
Install
npm install -g eval-workspaceOr without installing globally:
npx eval-workspaceUsage
Check what's already available:
npm run check
# or, if installed globally:
eval-workspace-checkStart the server:
eval-workspaceStarts on http://127.0.0.1:8765 by default. Override with env vars:
EVAL_WORKSPACE_HOST=0.0.0.0 EVAL_WORKSPACE_PORT=9000 eval-workspaceRun some code:
curl -X POST http://127.0.0.1:8765/run \
-H "Content-Type: application/json" \
-d '{"language": "python", "code": "print(2 + 2)"}'{"ok": true, "stdout": "4\n", "stderr": "", "exitCode": 0, "timedOut": false, "durationSeconds": 0.02, "error": ""}Custom limits per request:
curl -X POST http://127.0.0.1:8765/run \
-H "Content-Type: application/json" \
-d '{"language": "javascript", "code": "console.log(1)", "limits": {"cpuSeconds": 10, "memoryMb": 512}}'Check server + toolchain health:
curl http://127.0.0.1:8765/healthAPI reference
POST /run
| Field | Type | Required | Notes |
|---|---|---|---|
| language | string | yes | python, javascript/js, java, or rust |
| code | string | yes | max 100,000 characters |
| stdin | string | no | piped to the program's stdin |
| limits | object | no | see below, all optional |
limits fields (all clamped server-side to the max shown):
| Field | Default | Max |
|---|---|---|
| cpuSeconds | 5 | 30 |
| memoryMb | 256 | 1024 |
| wallClockSeconds | 10 | 60 |
| maxProcesses | 16 | 32 |
| maxOutputBytes | 200000 | 1000000 |
GET /languages
Returns the list of supported language identifiers.
GET /health
Returns toolchain availability for each language.
Using it as a library instead of a service
const { run } = require("eval-workspace/src/executor");
const result = await run("python", "print('hello')");
console.log(result);
// { ok: true, stdout: 'hello\n', stderr: '', exitCode: 0, timedOut: false, durationSeconds: 0.02, error: '' }License
MIT
