Secure TypeScript/JavaScript sandbox for AI agent code execution
Run untrusted code with V8 isolates (isolated-vm) for JavaScript and TypeScript, optional Docker for stronger isolation, and controlled subprocess execution for Python and shell.
| Requirement | Notes |
|---|---|
| Node.js 20+ | LTS recommended |
--no-node-snapshot |
Required by isolated-vm on Node 20+. This repo sets NODE_OPTIONS in npm scripts; set it yourself if you run node directly. |
| Python | python3 on PATH (or python on Windows) for language: 'python' |
| Docker | Optional: DockerSandbox, executor: 'docker', or USE_DOCKER_EXECUTOR=true |
cd ts-agent-sandbox
npm install
npm run buildThis writes compiled output to dist/ (ignored by git). Run npm run build after pulling changes before npm start.
The published npm tarball includes dist/, README.md, LICENSE, and .env.example (see the files field in package.json). prepublishOnly runs build before npm publish.
Runtime npm dependencies: express (HTTP API), isolated-vm (JS/TS isolate), typescript (transpile agent TS), dotenv (load .env in src/index.ts only).
Copy .env.example to .env to set PORT, USE_DOCKER_EXECUTOR, etc. The HTTP entrypoint (src/index.ts) loads .env with dotenv before reading process.env. Library-only usage (Sandbox from public-api.ts) does not load .env unless you call dotenv yourself.
From another package (after npm install ts-agent-sandbox), import the public API:
import { Sandbox } from 'ts-agent-sandbox';
const sandbox = new Sandbox({
timeoutMs: 5000,
memoryLimitMb: 128,
allowNetwork: false,
fileSystem: 'none',
});
const result = await sandbox.run(
`
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
}
fibonacci(10);
`,
'javascript'
);
console.log(result.stdout.trim()); // "55"
console.log(result.exitCode); // 0When developing inside this repo, use path imports (or link the package):
import { Sandbox } from './src/sandbox/Sandbox.js';User code is transpiled with the TypeScript compiler API (typescript is a runtime dependency). Override compiler options with tsCompilerOptions on Sandbox.
Python runs in a python3 subprocess (timeouts, trimmed env). This is reliable in real paths (including directories with : in the name). Pyodide/WASM is not used by default.
const r = await sandbox.run(
`
import math
print(math.sqrt(16))
`,
'python'
);Uses /bin/bash -c with a minimal environment and timeout. Weaker than V8 or Docker—treat as convenience only unless you add OS-level isolation.
fileSystem |
Behavior |
|---|---|
none |
No require('fs') shim |
virtual (default) |
In-memory fs under /sandbox |
tmpdir |
Real temp directory on disk, exposed as /sandbox/... inside the isolate |
npm run devDefault listen port is 3000 unless PORT is set in the environment or .env (integer 0–65535; invalid values fall back to 3000).
GET /health—{ "ok": true }POST /execute— JSON body:{ "code": string, "language": string, "options"?: SandboxOptions & { executor?: "docker" } }. Ifoptionsis present, it must be a plain object (not an array or string).- Languages:
javascript,typescript,python,bash
curl -s -X POST http://localhost:3000/execute \
-H "Content-Type: application/json" \
-d '{"code":"console.log(1+1)","language":"javascript","options":{"fileSystem":"none","timeoutMs":5000,"memoryLimitMb":128}}'Requires Docker on the host.
import { DockerSandbox } from 'ts-agent-sandbox';
const dockerSandbox = new DockerSandbox({
image: 'node:22-alpine',
timeoutMs: 5000,
memoryLimitMb: 128,
});
const result = await dockerSandbox.run(`console.log("in container")`, 'javascript');Or use the default isolate and switch per run:
await sandbox.run(code, 'javascript', { executor: 'docker' });Environment: If USE_DOCKER_EXECUTOR=true, all JavaScript and TypeScript runs use the Docker executor (no per-request flag needed). Otherwise, pass "executor": "docker" inside the JSON options for /execute, or use DockerSandbox / sandbox.run(..., { executor: 'docker' }) from code.
| Field | Description |
|---|---|
timeoutMs |
Wall-clock limit (default 10000) |
memoryLimitMb |
Isolate or Docker memory hint (default 256) |
allowNetwork |
false, true, or hostname allowlist for injected fetch (JS/TS only) |
fileSystem |
'none' | 'virtual' | 'tmpdir' (JS/TS) |
envVars |
Extra environment variables (bash / python) |
tsCompilerOptions |
typescript.CompilerOptions for TS transpile |
preloadJavaScript |
Code evaluated in the isolate before user code (JS/TS) |
| Field | Description |
|---|---|
Same as SandboxOptions |
Merged over constructor defaults |
executor |
'ivm' (default) or 'docker' for JS/TS (docker needs Docker CLI) |
stdout, stderr, exitCode, durationMs, optional memoryUsedMb (reserved).
Async generator: buffered stdout / stderr chunks, then an exit chunk (not true byte streaming from the isolate yet).
| Command | Description |
|---|---|
npm run dev |
HTTP server with tsx watch + NODE_OPTIONS=--no-node-snapshot |
npm run build |
Compile to dist/ (invokes the TypeScript compiler) |
npm start |
Run node dist/index.js (build first) |
npm test |
Unit tests (isolated-vm) |
npm run test:integration |
Bash, Python, HTTP API |
npm run test:all |
Full suite (10 tests: unit + integration) |
Integration tests expect python3 and /bin/bash on PATH (typical on Linux and macOS; on Windows use WSL or skip integration tests).
docker build -t ts-agent-sandbox .
docker run --rm -p 3000:3000 ts-agent-sandboxOr docker compose up using docker-compose.yml.
ts-agent-sandbox/
├── src/
│ ├── index.ts # HTTP server
│ ├── public-api.ts # npm package entry (library exports)
│ └── sandbox/
│ ├── Sandbox.ts
│ ├── docker.ts # DockerSandbox
│ ├── types.ts
│ └── executors/
│ ├── IsolatedVmExecutor.ts
│ ├── DockerExecutor.ts
│ ├── BashExecutor.ts
│ └── PythonExecutor.ts
├── tests/
│ ├── unit/
│ └── integration/
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env.example
├── .gitignore
├── package.json
├── tsconfig.json
├── LICENSE
└── README.md
isolated-vmgreatly reduces what untrusted JS can reach, but no sandbox is perfect. Follow upstream guidance.- Python and bash run as the host user with process-level limits only; isolate untrusted workloads with OS containers or VMs if needed.
- Run Node with
--no-node-snapshotwhen using this package, as required byisolated-vmon Node 20+.
MIT — see LICENSE.