@yarlisai/sandbox
v0.1.0-alpha.1
Published
Pluggable sandbox runner for executing untrusted user code (worker_threads pool + in-memory fallback).
Maintainers
Readme
@yarlisai/sandbox
Pluggable sandbox runner for executing untrusted user JavaScript. Built on a Port/Adapter pattern (ADR 0007). Swap between a worker_threads pool, an in-process eval, or any custom transport by changing one line.
Port signature
export interface SandboxRunner {
readonly name: string
execute(args: SandboxExecuteArgs): Promise<SandboxExecuteResult>
dispose(): Promise<void>
}
export interface SandboxClientConfig {
adapter: SandboxRunner
}execute() runs ONE piece of user code and resolves with its return value. dispose() tears the runner down (drain pool / close handles).
Install
bun add @yarlisai/sandbox@alphaThe default node-worker adapter relies on Node's built-in worker_threads and vm — no extra runtime deps.
Architecture
Your app ──► SandboxClient ──► SandboxRunner (port) ──► [node-worker | memory | custom] (adapter)SandboxRunner— the port:{ name, execute(args), dispose() }createSandboxClient({ adapter })— thin façade so callers don't reach into the adapter directly- Adapters — each implements
SandboxRunner
Usage
import { createSandboxClient, nodeWorkerAdapter } from '@yarlisai/sandbox'
// Production: pool of worker_threads, vm.Script + wall-clock kill switch.
const sandbox = createSandboxClient({
adapter: nodeWorkerAdapter({ maxWorkers: 4, workerMemoryMb: 256 }),
})
const { result } = await sandbox.execute({
code: 'return params.a + params.b',
executionParams: { a: 1, b: 2 },
envVars: {},
contextVariables: {},
isCustomTool: false,
timeout: 5_000,
secureFetchImpl: (url, init) => fetch(url, init as RequestInit),
consoleSink: {
log: (m) => console.log(m),
info: (m) => console.info(m),
warn: (m) => console.warn(m),
error: (m) => console.error(m),
},
})
// Graceful shutdown.
await sandbox.dispose()Message protocol
The node-worker adapter speaks the following protocol over postMessage:
main → worker
{ type: 'execute', execId, code, contextVariables, executionParams,
envVars, isCustomTool, syncTimeout }
{ type: 'fetch_response', id, ok, status, statusText, headers, body, error? }
worker → main
{ type: 'fetch', id, url, init } // proxied back to secureFetchImpl
{ type: 'console', level, message } // forwarded to consoleSink
{ type: 'result', execId, value }
{ type: 'error', execId, error: { name, message, stack } }Both message types are exported as SandboxMainToWorkerMessage and SandboxWorkerToMainMessage for adapter authors who want to reuse the same worker source. The raw worker source is also exported as WORKER_SOURCE.
Security caveats
node-workeris isolation, not sandboxing: aworker_threadsWorker shares the host filesystem, native crypto, and (withoutresourceLimits) memory. Code that bypassesvm.runInContext(e.g. by triggering an unhandled rejection deeper than the script) can in principle reach those surfaces. Combine with OS-level sandboxing if you accept code from anonymous users.- All
fetch()calls inside the sandbox proxy back tosecureFetchImplon the main thread. You must SSRF-validate every URL there — the worker has no network restriction of its own. - The wall-clock timeout is enforced by
worker.terminate()on the main thread. Thevm.Scripttimeoutoption is a second kill switch; do not rely on it alone (microtask-starvation loops can outlast it). - The
memoryadapter has no isolation. It runs user code in the same process vianew Function()for unit tests / dev loops only. Never accept untrusted input through it.
Writing a custom adapter
import type { SandboxRunner, SandboxExecuteArgs } from '@yarlisai/sandbox'
export function myAdapter(): SandboxRunner {
return {
name: 'my-adapter',
async execute(args: SandboxExecuteArgs) {
// call your runtime (firecracker, gvisor, e2b, ...)
return { result: someValue }
},
async dispose() {
// optional teardown
},
}
}Drop it into createSandboxClient({ adapter: myAdapter() }) — done.
Built-in adapters
| Adapter | Factory | Transport | Use for |
|---|---|---|---|
| Node Worker | nodeWorkerAdapter | worker_threads pool + vm.Script | production |
| Memory | memoryAdapter | new Function(code) in same process | unit tests, dev |
Build
bun install
cd packages/sandbox
bun run buildRelated
@yarlisai/core— logger / env helpers used by sibling packages.- ADR 0007 — Port/Adapter protocol — the rules every service package follows.
License
MIT
