#wasm-sandbox #python #async #sandbox

eryx

A Python sandbox with async callbacks powered by WebAssembly

13 releases

Uses new Rust 2024

0.5.0 Jun 16, 2026
0.4.10 Jun 3, 2026
0.4.9 Apr 16, 2026
0.4.8 Mar 27, 2026
0.1.0 Dec 9, 2025

#897 in WebAssembly

24 downloads per month
Used in eryx-precompile

MIT/Apache

9MB
11K SLoC

Eryx

eryx (noun): A genus of sand boas (Erycinae) - non-venomous snakes that live in sand. Perfect for "Python running inside a sandbox."

A Python sandbox with async callbacks powered by WebAssembly.

Features

  • Async callback mechanism - Callbacks are exposed as direct async functions (e.g., await get_time())
  • Parallel execution - Multiple callbacks can run concurrently via asyncio.gather()
  • Execution tracing - Line-level progress reporting via sys.settrace
  • Introspection - Python can discover available callbacks at runtime
  • Composable runtime libraries - Pre-built APIs with Python wrappers and type stubs
  • LLM-friendly - Type stubs (.pyi) for including in context windows

Quick Start

use eryx::Sandbox;

#[tokio::main]
async fn main() -> Result<(), eryx::Error> {
    let sandbox = Sandbox::builder().build()?;

    let result = sandbox.execute(r#"
        print("Hello from Python!")
    "#).await?;

    println!("Output: {}", result.stdout);
    Ok(())
}

Returning a structured result

Assign a variable named result in the script and it is JSON-serialized and returned as ExecuteResult::result — a structured channel separate from stdout:

use eryx::Sandbox;

#[tokio::main]
async fn main() -> Result<(), eryx::Error> {
    let sandbox = Sandbox::builder().build()?;

    let result = sandbox
        .execute(r#"result = {"answer": 42, "items": [1, 2, 3]}"#)
        .await?;

    // `result.result` is the JSON string `{"answer": 42, "items": [1, 2, 3]}`.
    assert_eq!(result.result.as_deref(), Some(r#"{"answer": 42, "items": [1, 2, 3]}"#));
    Ok(())
}

If the value is not JSON-serializable, result.result is None and result.result_error explains why (execution still succeeds). Use SandboxBuilder::with_result_variable to capture a different variable name.

With Callbacks

Use the #[callback] macro for strongly-typed callbacks with automatic JSON Schema generation:

use eryx::{callback, CallbackError, Sandbox};
use serde_json::{json, Value};

/// Returns the current Unix timestamp
#[callback]
async fn get_time() -> Result<Value, CallbackError> {
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    Ok(json!(now))
}

/// Echoes back the message
#[callback]
async fn echo(message: String) -> Result<Value, CallbackError> {
    Ok(json!({ "echoed": message }))
}

#[tokio::main]
async fn main() -> Result<(), eryx::Error> {
    let sandbox = Sandbox::builder()
        .with_callback(get_time)
        .with_callback(echo)
        .build()?;

    let result = sandbox.execute(r#"
timestamp = await get_time()
print(f"Current time: {timestamp}")

response = await echo(message="Hello!")
print(f"Echo: {response}")
    "#).await?;

    println!("{}", result.stdout);
    Ok(())
}

Dynamic Callbacks

For runtime-defined callbacks (e.g., from configuration or plugins):

use eryx::{DynamicCallback, Sandbox, CallbackError};
use serde_json::json;

let greet = DynamicCallback::builder("greet", "Greets a person", |args| {
        Box::pin(async move {
            let name = args["name"].as_str().unwrap_or("stranger");
            Ok(json!({ "greeting": format!("Hello, {}!", name) }))
        })
    })
    .param("name", "string", "The person's name", true)
    .build();

let sandbox = Sandbox::builder()
    .with_callback(greet)
    .build()?;

With Runtime Libraries

Runtime libraries bundle callbacks with Python wrappers and type stubs:

use eryx::{RuntimeLibrary, Sandbox};

let library = RuntimeLibrary::new()
    .with_callback(MyCallback)
    .with_preamble(include_str!("preamble.py"))
    .with_stubs(include_str!("stubs.pyi"));

let sandbox = Sandbox::builder()
    .with_library(library)
    .build()?;

License

MIT OR Apache-2.0

Dependencies

~58–78MB
~1.5M SLoC