Skip to main content

Execute JavaScript and WebAssembly in a Deno sandbox from Python

Project description

Denobox

PyPI Tests Changelog License

A Python library for executing JavaScript and WebAssembly in a Deno sandbox.

Overview

denobox provides a simple interface to run JavaScript code and WebAssembly modules from Python using Deno as the runtime. It communicates with a Deno subprocess using a newline-delimited JSON (NDJSON) protocol over stdin/stdout.

Features

  • Execute JavaScript code in a fully sandboxed Deno environment
  • Load and call WebAssembly modules
  • Both synchronous and async APIs
  • JSON-based data exchange between Python and JavaScript
  • Promise resolution handled automatically
  • Thread-safe (sync) and concurrent (async) execution support

Security

Deno runs with zero permissions - maximum sandboxing:

  • No file system access - WASM files are read by Python and sent as base64
  • No network access - cannot make HTTP requests or open sockets
  • No subprocess spawning - cannot execute shell commands
  • No environment access - cannot read environment variables

The sandbox is enforced by Deno's permission system. Any attempt to access restricted resources will raise an error.

Warning: The sandbox is only as secure as Deno itself. See Deno's documentation on executing untrusted code for important security considerations.

Installation

pip install denobox

Or with uv:

uv add denobox

Requirements

  • Python 3.10+
  • The deno PyPI package (automatically installed)

Usage

JavaScript Execution

Synchronous API

from denobox import Denobox

with Denobox() as box:
    # Simple expressions
    result = box.eval("1 + 1")
    print(result)  # 2

    # Strings
    result = box.eval("'hello' + ' ' + 'world'")
    print(result)  # "hello world"

    # Arrays and objects
    result = box.eval("[1, 2, 3].map(x => x * 2)")
    print(result)  # [2, 4, 6]

    result = box.eval("({name: 'test', value: 42})")
    print(result)  # {'name': 'test', 'value': 42}

    # State persists between evals
    box.eval("var x = 10")
    box.eval("var y = 20")
    result = box.eval("x + y")
    print(result)  # 30

    # Promises are automatically resolved
    result = box.eval("Promise.resolve(42)")
    print(result)  # 42

    # Async functions work too
    result = box.eval("(async () => { return 'async result'; })()")
    print(result)  # "async result"

    # If you return only the function it will be automatically invoked
    result = box.eval("async () => { return 'async result'; }")
    print(result)  # "async result"

Asynchronous API

import asyncio
from denobox import AsyncDenobox

async def main():
    async with AsyncDenobox() as box:
        result = await box.eval("1 + 1")
        print(result)  # 2

        # Concurrent execution
        results = await asyncio.gather(
            box.eval("1 + 1"),
            box.eval("2 + 2"),
            box.eval("3 + 3"),
        )
        print(results)  # [2, 4, 6]

asyncio.run(main())

WebAssembly

Synchronous API

from denobox import Denobox

with Denobox() as box:
    # Load a WASM module from a file (Python reads and sends to Deno)
    module = box.load_wasm("path/to/module.wasm")

    # Or load from raw bytes
    wasm_bytes = open("path/to/module.wasm", "rb").read()
    module = box.load_wasm(wasm_bytes=wasm_bytes)

    # Check available exports
    print(module.exports)  # {'add': 'function', 'multiply': 'function'}

    # Call exported functions
    result = module.call("add", 3, 4)
    print(result)  # 7

    result = module.call("multiply", 5, 6)
    print(result)  # 30

    # Unload when done (optional, cleaned up on box close)
    module.unload()

Asynchronous API

import asyncio
from denobox import AsyncDenobox

async def main():
    async with AsyncDenobox() as box:
        module = await box.load_wasm("path/to/module.wasm")

        # Concurrent calls
        results = await asyncio.gather(
            module.call("add", 1, 2),
            module.call("add", 3, 4),
            module.call("multiply", 5, 6),
        )
        print(results)  # [3, 7, 30]

        await module.unload()

asyncio.run(main())

Error Handling

from denobox import Denobox, DenoboxError

with Denobox() as box:
    try:
        box.eval("throw new Error('Something went wrong')")
    except DenoboxError as e:
        print(f"JavaScript error: {e}")

    try:
        box.eval("invalid javascript {{{")
    except DenoboxError as e:
        print(f"Syntax error: {e}")

Architecture

NDJSON Protocol

Communication between Python and the Deno subprocess uses newline-delimited JSON:

Requests:

{"id": 1, "type": "eval", "code": "1 + 1"}
{"id": 2, "type": "load_wasm", "bytes": "<base64-encoded-wasm>"}
{"id": 3, "type": "call_wasm", "moduleId": "wasm_0", "func": "add", "args": [1, 2]}
{"id": 4, "type": "unload_wasm", "moduleId": "wasm_0"}
{"id": 5, "type": "shutdown"}

Note: WASM modules are sent as base64-encoded bytes. Python reads the file and encodes it, so Deno doesn't need file system access.

Responses:

{"id": 1, "result": 2}
{"id": 2, "result": {"moduleId": "wasm_0", "exports": {"add": "function"}}}
{"id": 3, "result": 3}
{"id": 4, "result": true}
{"id": 5, "result": true, "shutdown": true}

Errors:

{"id": 1, "error": "ReferenceError: x is not defined", "stack": "..."}

Components

  1. Denobox - Synchronous wrapper using subprocess.Popen with thread-safe locking
  2. AsyncDenobox - Asynchronous wrapper using asyncio.create_subprocess_exec with a background reader task
  3. WasmModule / AsyncWasmModule - Wrappers for loaded WebAssembly modules
  4. worker.js - Deno script that handles the NDJSON protocol

Development

# Clone and setup
git clone <repo>
cd denobox

# Run tests
uv run pytest

# Run tests with verbose output
uv run pytest -v

License

Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

denobox-0.1a2.tar.gz (16.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

denobox-0.1a2-py3-none-any.whl (14.0 kB view details)

Uploaded Python 3

File details

Details for the file denobox-0.1a2.tar.gz.

File metadata

  • Download URL: denobox-0.1a2.tar.gz
  • Upload date:
  • Size: 16.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for denobox-0.1a2.tar.gz
Algorithm Hash digest
SHA256 97b0dc2d467be0f74d23acf91c49d2e1cec6a42232b339cb3c0937b69f50b76d
MD5 8c689f61c315e19c010970f8d95c6815
BLAKE2b-256 b31cd800d31a37c02d6ec6a62637fe48f77a8e606cc94b77b3762e0bb69152da

See more details on using hashes here.

Provenance

The following attestation bundles were made for denobox-0.1a2.tar.gz:

Publisher: publish.yml on simonw/denobox

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file denobox-0.1a2-py3-none-any.whl.

File metadata

  • Download URL: denobox-0.1a2-py3-none-any.whl
  • Upload date:
  • Size: 14.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for denobox-0.1a2-py3-none-any.whl
Algorithm Hash digest
SHA256 198f1e35f459be674b1d4d8226a2aecb866d9fced2dbc01949299393e5060781
MD5 35d174fcfd5b01924d905a824dcdda59
BLAKE2b-256 e020b613612bc637ad9454ddd4ff17881a1896496f88dccf96dde4607e4b80f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for denobox-0.1a2-py3-none-any.whl:

Publisher: publish.yml on simonw/denobox

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page