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
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