slopjail is a sandbox for running untrusted JavaScript in the browser. It can be used to safely implement a code execution tool for browser-based AI agents, among other use cases.
Status: Alpha β‘
- Tiny (~3 KB gzipped)
- Simple API:
createSandbox()βrun()βdispose(). - Expose variables and functions for untrusted code to access.
- Pretty good security:
- Code runs in a Web Worker on an opaque origin: no access to the host page's storage, cookies, or DOM.
- Network access is blocked by default using a strict Content-Security-Policy.
- Disables APIs like
navigatorto resist device fingerprinting.
Install:
npm install slopjail
Create a sandbox, run some code, and clean up:
import { createSandbox } from "slopjail";
const sandbox = await createSandbox({
globals: {
twenty: 20,
add: (a: number, b: number) => a + b,
},
});
try {
await sandbox.run("console.log(await add(twenty, 5))"); // 25
} finally {
sandbox.dispose();
}slopjail creates a hidden <iframe> with an opaque origin and a restrictive CSP. A small relay script inside the iframe spawns a Web Worker and bridges a MessagePort back to the host for RPC. Untrusted code runs in the Worker, completely isolated from the main page. Any functions you provide as globals are replaced with RPC proxies β when the sandbox calls them, they execute in the host context and the result is sent back.
Host (main thread)
ββ iframe (sandbox="allow-scripts", opaque origin, strict CSP)
ββ Worker (runs untrusted code)
ββ RPC proxy functions β call back to host via MessagePort
Objects are traversed recursively, so nested functions work the same way:
const sandbox = await createSandbox({
globals: {
math: {
add: (a: number, b: number) => a + b,
multiply: (a: number, b: number) => a * b,
},
version: "1.0.0",
},
});
try {
await sandbox.run(`
const sum = await math.add(2, 3)
const product = await math.multiply(sum, 4)
console.log(version, product) // "1.0.0" 20
`);
} finally {
sandbox.dispose();
}run() enforces a 3-second execution timeout by default. If the code doesn't finish in time, the returned promise rejects with an error. You can override it per call:
await sandbox.run(code, { timeout: 10_000 }); // 10 secondsBy default, the sandbox blocks all network access.
Use the contentSecurityPolicy option to relax specific CSP directives:
const sandbox = await createSandbox({
contentSecurityPolicy: {
// Allow access to the GitHub API
connectSrc: ["https://api.github.com"],
},
});
await sandbox.run(`
const res = await fetch('https://api.github.com/zen')
console.log(await res.text())
`);You can allow ESM import statements by using scriptSrc:
const sandbox = await createSandbox({
contentSecurityPolicy: {
// Allow importing ES modules from esm.sh
scriptSrc: ["https://esm.sh"],
},
});
await sandbox.run(`
import _ from 'https://esm.sh/underscore'
console.log(_.uniq([1, 2, 1, 4, 1, 3])) // [1, 2, 4, 3]
`);Give a sandbox a name for easier debugging:
const sandbox = await createSandbox({
name: `ai-code-tool-${Date.now()}`,
});Sandbox implements Symbol.dispose, so you can use using to automatically clean up when leaving scope:
using sandbox = await createSandbox();
await sandbox.run(code);Which is equivalent to:
const sandbox = await createSandbox();
try {
await sandbox.run(code);
} finally {
sandbox.dispose();
}Simply expose your own console global to capture them:
const sandbox = await createSandbox({
globals: {
console: {
log: (...args: unknown[]) => {
document.getElementById("output")!.textContent += args.join(" ") + "\n";
},
},
},
});
try {
await sandbox.run('console.log("hello from the sandbox!")');
} finally {
sandbox.dispose();
}You can either expose a global callback for sandboxed code to call, or use evaluate() to return the value of a JS expression:
const sandbox = await createSandbox({
// Globals are copied into the sandbox, references are not shared
globals: { fruit: ["apple", "banana"] },
});
try {
await sandbox.run('fruit.push("cherry")');
const updatedFruit = await sandbox.evaluate("fruit");
console.log(updatedFruit); // ['apple', 'banana', 'cherry']
} finally {
sandbox.dispose();
}Create a new sandboxed execution environment.
Creation options:
| Option | Type | Description |
|---|---|---|
globals |
Record<string, unknown> |
Variables and functions to expose inside the sandbox. |
contentSecurityPolicy |
object |
Additional CSP directives appended to the default policy. |
name |
string |
Name for debugging. |
| Method | Description |
|---|---|
run(code: string, options?): Promise<void> |
Execute JavaScript inside the sandbox. |
evaluate(expr: string, options?): Promise<unknown> |
Evaluate a single JavaScript expression inside the sandbox and return its value. |
dispose(): void |
Terminate the worker and clean up all resources. |
Execution options:
| Option | Type | Description |
|---|---|---|
timeout |
number |
Maximum time in milliseconds to wait before rejecting with a timeout error. Defaults to 3000. |