MCPcopy Index your code
hub / github.com/eryx-org/eryx

github.com/eryx-org/eryx @eryx-macros-v0.5.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release eryx-macros-v0.5.0 ↗ · + Follow
2,715 symbols 8,202 edges 155 files 901 documented · 33% updated 8d agoeryx-v0.5.0 · 2026-06-16★ 618 open issues
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Eryx

CI crates.io PyPI npm docs

A WebAssembly sandbox for running untrusted Python safely — memory and CPU limits, no filesystem or network access by default, and async host callbacks for the access you do want to allow.

Embed it from Python, JavaScript, or Rust. Runs full CPython 3.14 (not a limited subset), so the real standard library and most pure-Python packages just work. Try the demo or read the docs.

Used in production at Grafana.

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

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()
  • Session state persistence — Variables, functions, and classes persist between executions for REPL-style usage
  • State snapshots — Capture and restore Python state with pickle-based serialization
  • Execution tracing — Line-level progress reporting via sys.settrace
  • Stderr capture — Separate stdout and stderr streams with optional streaming handlers
  • Execution cancellation — Cancel long-running executions via ExecutionHandle
  • TCP/TLS networking — Host-controlled network access with configurable policies
  • Introspection — Python can discover available callbacks at runtime
  • Composable runtime libraries — Pre-built APIs with Python wrappers and type stubs
  • Pre-compiled Wasm — 41x faster sandbox creation with ahead-of-time compilation
  • Sandbox pooling — Managed pool of warm sandbox instances for high-throughput scenarios

Python Version

Eryx embeds CPython 3.14 compiled to WebAssembly (WASI). The WASI-compiled CPython and standard library come from the componentize-py project by the Bytecode Alliance.

Quick Start

Python

pip install pyeryx

The package is published as pyeryx but imported as eryx.

import eryx

# Zero-config: ships with a pre-initialized CPython runtime, so this is fast (~1-5ms)
sandbox = eryx.Sandbox()

result = sandbox.execute('''
print("Hello from the sandbox!")
print(f"2 + 2 = {2 + 2}")
''')

print(result.stdout)

See the PyPI package and crates/eryx-python for the full Python API (sessions, callbacks, package loading, resource limits).

JavaScript

npm install @bsull/eryx

Rust

cargo add eryx --features embedded
use eryx::Sandbox;

#[tokio::main]
async fn main() -> Result<(), eryx::Error> {
    // Sandbox::embedded() provides zero-config setup (requires `embedded` feature)
    let sandbox = Sandbox::embedded().build()?;

    let result = sandbox.execute(r#"
        print("Hello from Python!")
        import sys
        print("This goes to stderr", file=sys.stderr)
    "#).await?;

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

With Callbacks

Use the #[callback] macro for strongly-typed callbacks with automatic 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::embedded()
        .with_callback(get_time)
        .with_callback(echo)
        .build()?;

    let result = sandbox.execute(r#"
# Callbacks are available as direct async functions
timestamp = await get_time()
print(f"Current time: {timestamp}")

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

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

For runtime-defined callbacks (plugin systems, dynamic APIs), implement the Callback trait directly. See the runtime_callbacks example.

Sandbox Pooling

For high-throughput scenarios where you need to execute many Python scripts concurrently, use SandboxPool to maintain a pool of warm sandbox instances:

use eryx::{Sandbox, SandboxPool, PoolConfig};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), eryx::Error> {
    // Create a pool with custom configuration
    let config = PoolConfig {
        max_size: 10,              // Maximum concurrent sandboxes
        min_idle: 2,               // Pre-warm 2 instances
        idle_timeout: Duration::from_secs(300),  // Evict after 5 min idle
        acquire_timeout: Duration::from_secs(30), // Wait up to 30s for sandbox
        ..Default::default()
    };

    let pool = SandboxPool::new(Sandbox::embedded(), config).await?;

    // Acquire a sandbox from the pool
    let sandbox = pool.acquire().await?;

    // Use the sandbox normally
    let result = sandbox.execute("print('Hello from pool!')").await?;
    println!("{}", result.stdout);

    // Sandbox automatically returns to pool when dropped
    drop(sandbox);

    // Check pool statistics
    let stats = pool.stats();
    println!("Acquisitions: {}, Creations: {}",
             stats.total_acquisitions, stats.total_creations);

    Ok(())
}

For custom sandbox configurations with callbacks:

use eryx::{Sandbox, SandboxPool, PoolConfig};

let pool = SandboxPool::with_builder(
    || {
        Sandbox::embedded()
            .with_callback(MyCallback)
            .build()
    },
    PoolConfig::default(),
).await?;

Key features:

  • Pre-warming: Pool creates min_idle sandboxes upfront for immediate availability
  • Bounded concurrency: max_size limits concurrent sandbox usage via semaphore
  • Statistics tracking: Monitor acquisitions, creations, and wait times
  • Idle eviction: Sandboxes idle longer than idle_timeout are automatically evicted
  • Non-blocking acquire: Use try_acquire() to get a sandbox without waiting

Session State Persistence

For REPL-style usage where state persists between executions:

use eryx::{Sandbox, session::InProcessSession};

#[tokio::main]
async fn main() -> Result<(), eryx::Error> {
    let sandbox = Sandbox::embedded().build()?;
    let mut session = InProcessSession::new(&sandbox).await?;

    // First execution defines a variable
    session.execute("x = 42").await?;

    // Second execution can access it
    let result = session.execute("print(x * 2)").await?;
    println!("{}", result.stdout); // "84"

    // Snapshot and restore state
    let snapshot = session.snapshot_state().await?;
    session.clear_state().await?;
    session.restore_state(&snapshot).await?;

    Ok(())
}

Execution Cancellation

Cancel long-running or infinite executions using ExecutionHandle:

use std::time::Duration;
use eryx::{Sandbox, Error};

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

    // Start a cancellable execution
    let handle = sandbox.execute_cancellable("while True: pass");

    // Cancel from another task after a delay
    let token = handle.cancellation_token();
    tokio::spawn(async move {
        tokio::time::sleep(Duration::from_secs(1)).await;
        token.cancel();
    });

    // Wait for result
    match handle.wait().await {
        Ok(result) => println!("Completed: {}", result.stdout),
        Err(Error::Cancelled) => println!("Execution was cancelled"),
        Err(e) => println!("Error: {e}"),
    }

    Ok(())
}

The cancellation uses Wasmtime's epoch-based interruption for prompt termination.

Networking

Enable TCP and TLS networking with host-controlled policies using NetConfig:

use std::time::Duration;
use eryx::{Sandbox, NetConfig};

#[tokio::main]
async fn main() -> Result<(), eryx::Error> {
    let net_config = NetConfig::default()
        .allow_host("api.example.com")
        .allow_host("*.trusted.org")
        .with_connect_timeout(Duration::from_secs(10))
        .with_max_connections(5);

    let sandbox = Sandbox::embedded()
        .with_network(net_config)
        .build()?;

    let result = sandbox.execute(r#"
import urllib.request
response = urllib.request.urlopen("https://api.example.com/data")
print(response.read().decode())
    "#).await?;

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

By default, networking is disabled. When enabled via with_network():

  • Localhost and private networks are blocked by default
  • Use allowed_hosts patterns with wildcards (e.g., *.example.com)
  • The host handles DNS resolution and connection management
  • TLS connections use the system certificate store (with optional custom certs)

Feature Flags

Feature Description Trade-offs
embedded Zero-config sandboxes: embeds pre-compiled Wasm runtime + Python stdlib +32MB binary size; enables unsafe code paths
preinit Pre-initialization support for ~25x faster sandbox creation Adds eryx-runtime dep; requires build step
native-extensions Native Python extension support (e.g., numpy) via late-linking Implies preinit; experimental

Package support (with_package() for .whl and .tar.gz files) is always available — no feature flag required.

Pre-initialization

The preinit feature provides ~25x faster sandbox creation by capturing Python's initialized memory state at build time. This works with or without native extensions — you can pre-import stdlib modules like json, asyncio, re, etc.

Metric Without Pre-init With Pre-init Speedup
Sandbox creation ~450ms ~18ms 25x faster

Recommended Configurations

// Fastest startup, zero configuration (recommended for most users)
// Features: embedded
let sandbox = Sandbox::embedded().build()?;

// With pre-initialization for faster sandbox creation
// Features: embedded, preinit
// Pre-import common stdlib modules during build for ~25x speedup
let preinit_bytes = eryx::preinit::pre_initialize(
    &stdlib_path, None, &["json", "asyncio", "re"], &[]
).await?;

// With package support for third-party libraries
// Features: embedded (packages always available)
let sandbox = Sandbox::embedded()
    .with_package("requests-2.31.0-py3-none-any.whl")?
    .build()?;

// With native extensions (numpy, etc.)
// Features: embedded, native-extensions
let sandbox = Sandbox::embedded()
    .with_package("numpy-wasi.tar.gz")?
    .build()?;

Performance

Metric Normal Wasm Pre-compiled Speedup
Sandbox creation ~650ms ~16ms 41x faster
Per-execution overhead ~1.8ms ~1.6ms 14% faster
Session (5 executions) ~70ms ~3ms 23x faster

Development

This project uses mise for tooling and task management.

Setup

mise install
mise run setup  # Build Wasm + precompile (one-time)

Tasks

# Development
mise run check          # Run cargo check
mise run build          # Build all crates
mise run test           # Run tests with embedded Wasm
mise run test-all       # Run tests with all features
mise run lint           # Run clippy lints
mise run fmt            # Format code
mise run fmt-check      # Check code formatting

# Wasm
mise run build-eryx-runtime  # Build the Python Wasm component
mise run build-all      # Build Wasm + Rust crates
mise run precompile-eryx-runtime # Pre-compile to native code

# CI & Quality
mise run ci             # Run all CI checks (fmt-check, lint, test)
mise run msrv           # Check compilation on minimum supported Rust version

# Documentation
mise run doc            # Generate documentation
mise run doc-open       # Generate and open documentation

# Benchmarks
mise run bench          # Run benchmarks
mise run bench-save     # Run benchmarks and save baseline

# Examples
mise run examples       # Run all examples

Manual Commands

cargo nextest run --workspace                    # Run tests
cargo nextest run --workspace --features embedded     # Fast tests
cargo clippy --workspace --all-targets --all-features # Run lints
cargo fmt --all                                  # Format code
cargo doc --workspace --no-deps --open           # Generate docs
cargo bench --package eryx                       # Run benchmarks

Troubleshooting: Stale Builds

This project has multiple cache layers (cargo, mise, embedded runtime, WASM artifacts). If you experience unexpected behavior after changing code:

```bash

Diagnose which cache layer is stale

Extension points exported contracts — how you extend this code

Core symbols most depended-on inside this repo

Shape

Method 1,268
Function 1,069
Class 319
Enum 41
Interface 18

Languages

Rust77%
Python20%
TypeScript3%

Modules by API surface

crates/eryx-python/tests/test_callbacks.py135 symbols
crates/eryx/src/sandbox.rs118 symbols
crates/wit-dylib-ffi/src/types.rs106 symbols
crates/eryx-wasm-runtime/src/lib.rs104 symbols
crates/eryx/src/wasm.rs80 symbols
crates/eryx-wasm-runtime/src/python.rs79 symbols
crates/eryx-python/tests/test_sandbox.py76 symbols
crates/wit-dylib-ffi/src/lib.rs70 symbols
crates/eryx/src/net.rs68 symbols
crates/eryx/tests/sandbox_callbacks.rs57 symbols
crates/eryx/src/session/executor.rs57 symbols
crates/eryx/src/callback.rs57 symbols

For agents

$ claude mcp add eryx \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page