Skip to main content

jsbox

A small JavaScript sandbox for Python

jsbox embeds QuickJS (via rquickjs and PyO3) in Python. Run untrusted, LLM-generated JavaScript with hard limits and nothing but the functions you hand it.

CI PyPI License

What this is

Agent frameworks increasingly need to run JavaScript the model itself wrote: a data transform, a "compute the answer" snippet, a batch of tool calls. That code is untrusted by construction. jsbox runs it in an in-process QuickJS runtime that has no filesystem, no network, no require, no timers and no Node.js APIs. The guest can reach exactly what the host bound, and it is stopped by a wall-clock timeout, a heap limit and a stack limit.

It is small (a ~0.7 MB wheel, ~150 µs to create a runtime, no Rust toolchain needed to install) and deliberately boring: an interpreter, not a JIT. If you need raw JavaScript speed, use a V8 based engine; if you need a tiny, fast-starting, hard-to-misuse sandbox, this is that.

from jsbox import Runtime

rt = Runtime()
rt.bind_function("add", lambda a, b: a + b)
print(rt.eval("add(2, 3)"))  # 5

Get started

pip install jsbox  # or: uv pip install jsbox

Python 3.10+ on Linux, macOS and Windows (one abi3 wheel per platform).

Tool calling with limits

from jsbox import JsTimeoutError, Runtime

rt = Runtime(memory_limit=16 * 1024 * 1024)  # 16 MB heap


def get_weather(city: str) -> str:
    return f"72F and sunny in {city}"


rt.bind_function("getWeather", get_weather)

# Model-generated JS calling back into your tool:
print(rt.eval("getWeather('Zurich')"))  # 72F and sunny in Zurich

# Code that might not terminate is stopped, and the runtime stays usable:
try:
    rt.eval("while (true) {}", timeout=2.0)
except JsTimeoutError:
    print("runaway script killed after 2s")
print(rt.eval("1 + 1"))  # 2

Data in, data out

Values are copied across the boundary; the guest never holds a reference to a Python object.

JavaScript Python
null, undefined None
boolean, string bool, str
integral number, BigInt int (BigInt never wraps)
other number float
Array list (a tuple goes in as an array)
plain Object dict (own enumerable keys, functions skipped)
Uint8Array, ArrayBuffer bytes (bytes/bytearray go in as Uint8Array)
rt.bind_object("config", {"debug": True, "tags": ["a", "b"], "blob": b"\x00\x01"})
print(rt.eval("config.tags.length"))  # 2
print(rt.eval("({ok: true, rows: [1, 2, 3]})"))  # {'ok': True, 'rows': [1, 2, 3]}

Anything else (functions, symbols, Date, non-str dict keys, cycles) raises instead of being silently mangled.

Syntax check

rt.check_syntax(code) (or module=True) parses without running and raises JavaScriptError (SyntaxError) if invalid.

Promises

A script that returns a promise is settled by draining microtasks, so async/await works. A promise that can never settle (there are no timers or I/O) is an error, not a hang.

print(rt.eval("(async () => { await null; return 7 })()"))  # 7

ES modules

rt.add_static_module("math", "export const answer = 42;")
rt.add_static_module("app", "import {answer} from 'math'; export const x = answer + 1;")
print(rt.eval_module("app"))  # {'x': 43}

Only modules you registered can be imported, by import or import(). There is no file or URL loading.

Async host functions

Bound functions may be async def. Under eval_async they reach JS as Promises, run on your event loop, and can be in flight concurrently (Promise.all). timeout= covers the whole call, including time spent waiting on the host.

import asyncio
from jsbox import Runtime


async def fetch(x):
    await asyncio.sleep(0.01)
    return x * 2


async def main():
    rt = Runtime()
    rt.bind_function("fetch", fetch)
    print(await rt.eval_async("Promise.all([1, 2, 3].map(x => fetch(x)))"))  # [2, 4, 6]


asyncio.run(main())

A plain eval() that calls an async function raises TypeError: use eval_async.

console and unconvertible results

rt = Runtime(on_console=lambda level, args: print(level, args), lossy=True)
rt.eval("console.log('hi', [1, 2]); console.error('oops')")  # log ['hi', [1, 2]] / error ['oops']
rt.eval("({ok: true, run() {}})")  # {'ok': True, 'run': '[function]'}

Runtime(buffer=True) installs a small Node-style Buffer (a non-enumerable Uint8Array subclass, no I/O). JSZip and pptxgenjs detect it and UTF-8-encode through it, which makes a pptxgenjs write() about 20% faster; it is off by default so guests still see a non-Node environment.

console.* is a no-op without on_console. With lossy=True, functions, symbols and cycles in a result become "[function]", "[symbol]", "[Circular]" instead of raising.

Vendored npm libraries

Browser bundles such as pptxgenjs, three, vega, d3, turf, dagre and bpmn-moddle run unmodified: the host prepends the bundle (and a few byte-plumbing shims) as trusted source, the guest calls the library, and bytes come back as bytes. The test suite runs these exact files (tests/test_onehub_compat.py).

Errors

Situation Python exception
JS throws, or a syntax error JavaScriptError (.name, .message, .stack)
timeout= exceeded JsTimeoutError
A bound Python function raises the original exception, re-raised unchanged
Value cannot be converted TypeError / ValueError

JS that catches a host error sees only host function failed: <ExceptionClass> [#n]. The message of your Python exception (paths, secrets, internals) never reaches the guest.

Security model

There is no permission system to configure. A fresh Runtime can compute and nothing else; what it can do is exactly what you bound.

  • No ambient authority. No require, process, fetch, setTimeout, file or network access.
  • Heap, stack and copy limits with safe defaults. memory_limit (64 MB), max_stack_size (1 MB), max_value_bytes (16 MB per conversion). Pass 0 to memory_limit or max_stack_size to disable them.
  • Timeouts you cannot swallow. timeout= interrupts the interpreter loop; try/catch in the guest cannot catch it, and it also covers property getters hit while converting the result. It cannot interrupt a single long native operation: a catastrophic regular expression (/^(a+)+$/.test('a'.repeat(40) + 'b')) runs to completion, and some builtins (huge BigInt powers) overshoot the deadline. See below.
  • Host errors are not guest-readable, and a swallowed host error is never blamed on a later, unrelated JS error.
  • Isolated runtimes. Two Runtimes share nothing; a Runtime is bound to its thread.

What this is not: QuickJS is a C engine in your process. A memory-safety bug in it is a bug in your process, so for hostile multi-tenant workloads also use OS-level isolation (a container or a separate process). Bound host functions are privileged code you wrote, so keep them narrow. A timeout cannot interrupt a Python callback that is itself stuck (it fires as soon as that callback returns), nor a runaway regular expression: for untrusted code, run the Runtime in a process you can kill (or reject patterns up front). A built-in hard kill is the open item.

Limitations

  • TextEncoder/TextDecoder (UTF-8) and btoa/atob are built in and native.
  • Runtime(buffer=True) adds a small Node-style Buffer (UTF-8, latin1, base64, hex). It is off by default because it is ambient API surface; turn it on for JSZip-based libraries such as pptxgenjs, which then take their native UTF-8 path (about 20% faster on a 20-slide deck).
  • No timers (setTimeout), fetch, or Date/Map/Set conversion yet. Libraries that only use timers to yield work with a microtask shim.
  • Interpreter speed: it is not a JIT. CPU-bound JavaScript runs roughly 4-8x slower than V8 (a 20-slide pptxgenjs deck ≈ 0.45 s vs 0.1 s; fib(30) ≈ 70 ms vs 10 ms). Startup and boundary crossings are where it wins: creating a runtime ≈ 0.2 ms, a complete host call (JS to Python and back) ≈ 0.5 µs, eval('1+1') ≈ 2 µs.
  • Python ints outside i64 cannot be sent in.

Development

python -m venv .venv && .venv/bin/pip install maturin pytest pytest-asyncio hypothesis
.venv/bin/maturin develop --release
.venv/bin/pytest

Metadata

Release files for jsbox 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jsbox 0.1.0
File Size Uploaded Provenance Actions
jsbox-0.1.0.tar.gz 42.7 kB With provenance Details for jsbox-0.1.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for jsbox 0.1.0
File Actions
jsbox-0.1.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details for jsbox-0.1.0-cp310-abi3-win_amd64.whl
jsbox-0.1.0-cp310-abi3-musllinux_1_2_x86_64.whl CPython 3.10 abi3 Linux musl 1.2+ x86-64 Details for jsbox-0.1.0-cp310-abi3-musllinux_1_2_x86_64.whl
jsbox-0.1.0-cp310-abi3-musllinux_1_2_aarch64.whl CPython 3.10 abi3 Linux musl 1.2+ ARM64 Details for jsbox-0.1.0-cp310-abi3-musllinux_1_2_aarch64.whl
jsbox-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details for jsbox-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
jsbox-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details for jsbox-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
jsbox-0.1.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details for jsbox-0.1.0-cp310-abi3-macosx_11_0_arm64.whl

Total release size: 5.5 MB

Release files / jsbox-0.1.0.tar.gz

Download URL jsbox-0.1.0.tar.gz
Size 42.7 kB
Tags Source
SHA-256 checksum
How to use checksums
cb908a21dc2ec82f2e8c7a04da0fc41b5b610526938d87a55c6d8c7f8ebb9ac8
BLAKE2b-256 checksum
How to use checksums
e6b73848bcf6c75f51ebe555d54bb68db26142f97c38fd73157d61835b52b3c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.0-cp310-abi3-win_amd64.whl

Download URL jsbox-0.1.0-cp310-abi3-win_amd64.whl
Size 787.6 kB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
6d273d4a214a7b7fd42d1b97adc0ceeda725f7d635635cd37d501f1552fd5b3c
BLAKE2b-256 checksum
How to use checksums
4becea3b9eb42c9971be8429e42ad7a6aea50f3827816555c2f252eeb72cb5e6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.0-cp310-abi3-musllinux_1_2_x86_64.whl

Download URL jsbox-0.1.0-cp310-abi3-musllinux_1_2_x86_64.whl
Size 1.1 MB
Tags CPython 3.10 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
e4e997ac30a92a78b9ea124c7078ae29b9a8959e7e2a8a6383ec480c46f98a64
BLAKE2b-256 checksum
How to use checksums
f29823449f549307cf2de201a2e6d49891b667ee888151c495e4af3b12e8a490
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.0-cp310-abi3-musllinux_1_2_aarch64.whl

Download URL jsbox-0.1.0-cp310-abi3-musllinux_1_2_aarch64.whl
Size 1.0 MB
Tags CPython 3.10 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
8b46ad5443329d60e01558a9068e3f1f0de4dbe40b16517f1a50ce9498edcf0e
BLAKE2b-256 checksum
How to use checksums
7eeb210f14c45b3b5405e3bfab7314dc9cc6d165d6e57b13475e252224003647
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL jsbox-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 873.7 kB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
d8aae83bfb7e9c4830795d0ea64020cc6e973396db5d7f61fdb8faded76ba9ca
BLAKE2b-256 checksum
How to use checksums
da2ff6f6664c791610805978eb60b4aa18e3f686cc6bcc2681b87487874e28f5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL jsbox-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 845.3 kB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
cabf386369eb5f1421771b25fa2c507e3ce470235e173130113ef3820411bc66
BLAKE2b-256 checksum
How to use checksums
202b1739ede3251fc7287a9fd917ddb1fca9fb9c9ab184b95e89042c3e59f06e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / jsbox-0.1.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL jsbox-0.1.0-cp310-abi3-macosx_11_0_arm64.whl
Size 794.7 kB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
b3b54b61ee750db299dc8358272cebb78bc66572b4fd6ee792965bce921da620
BLAKE2b-256 checksum
How to use checksums
af929c88e929293c6e28d6be877774eb936a668463e1fbd6d0d3f4ccd00a1582
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Security for jsbox 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 7 files in this release have been uploaded with provenance metadata.

Select from 7 files

Attestation for jsbox-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page