@bsull/eryx
v0.4.6
Published
A Python sandbox powered by WebAssembly, for browser and Node.js
Maintainers
Readme
@bsull/eryx
A Python sandbox powered by WebAssembly, for browser and Node.js.
Eryx runs CPython 3.14 inside a WASM sandbox with full isolation — no network, no filesystem, no system access unless you explicitly allow it. Variables persist across calls, state can be snapshotted/restored, and execution can be traced line-by-line.
Install
npm install @bsull/eryxRequirements: Chrome 133+ or Node.js 24+ (requires JSPI via --experimental-wasm-jspi in Node).
Usage
import { Sandbox } from "@bsull/eryx";
const sandbox = new Sandbox();
const result = await sandbox.execute('print("Hello from Python!")');
console.log(result.stdout); // "Hello from Python!"Persistent state
Variables, functions, and imports persist across execute() calls:
await sandbox.execute("x = 42");
await sandbox.execute("import math");
const result = await sandbox.execute("print(math.sqrt(x))");
console.log(result.stdout); // "6.48074069840786"Snapshots
Save and restore interpreter state:
await sandbox.execute("data = [1, 2, 3]");
const snapshot = await sandbox.snapshotState();
await sandbox.clearState();
await sandbox.restoreState(snapshot);
const result = await sandbox.execute("print(data)");
console.log(result.stdout); // "[1, 2, 3]"Callbacks
Let Python call back into your JavaScript:
import { Sandbox, setCallbackHandler, setCallbacks } from "@bsull/eryx";
setCallbacks([
{ name: "get_weather", description: "Get current weather for a city" },
]);
setCallbackHandler((name, argsJson) => {
const args = JSON.parse(argsJson);
return JSON.stringify({ temp: 72, unit: "F", city: args.city });
});
const sandbox = new Sandbox();
await sandbox.execute(`
import json
from eryx_callbacks import invoke
result = json.loads(invoke("get_weather", json.dumps({"city": "NYC"})))
print(f"Temperature in {result['city']}: {result['temp']}°{result['unit']}")
`);Streaming output
Get stdout/stderr in real-time instead of waiting for execution to complete:
import { Sandbox, setOutputHandler } from "@bsull/eryx";
setOutputHandler((stream, data) => {
const prefix = stream === 0 ? "stdout" : "stderr";
process.stdout.write(`[${prefix}] ${data}`);
});
const sandbox = new Sandbox();
await sandbox.execute(`
for i in range(5):
print(f"Step {i}")
`);Vite / webpack
The package works with Vite and webpack. For Vite, you'll need cross-origin isolation headers for SharedArrayBuffer:
// vite.config.ts
export default defineConfig({
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
});API
Sandbox
| Method | Returns | Description |
|--------|---------|-------------|
| execute(code) | Promise<{ stdout, stderr }> | Run Python code |
| snapshotState() | Promise<Uint8Array> | Serialize interpreter state |
| restoreState(data) | Promise<void> | Restore from snapshot |
| clearState() | Promise<void> | Reset all user state |
Callbacks
| Function | Description |
|----------|-------------|
| setCallbackHandler(fn) | Register a (name, argsJson) => resultJson handler |
| setCallbacks(infos) | Register callbacks visible to Python's list_callbacks() |
| setTraceHandler(fn) | Receive line-level execution trace events |
| setOutputHandler(fn) | Receive streaming stdout/stderr |
How it works
Eryx compiles CPython 3.14 to WebAssembly (WASI) using componentize-py, then pre-initializes the interpreter so startup is fast. The JavaScript bindings are generated by jco and use JSPI for async interop.
Links
License
MIT OR Apache-2.0
