Boxwren
Local-first sandbox for untrusted WebAssembly. Boxwren runs a WASI module inside Wasmtime with deny-by-default fuel, memory, wall-clock, filesystem, network, and environment limits, a per-run kill switch, and a stable execution report.
Boxwren contains execution. Keywarden decides whether a tool may be called. Undoom records and replays failures.
Local-first is the default. Embed the crate or CLI where the agent already runs. An optional hosted stack — boxwren-server accounts/API keys/runs, plus the Next.js dashboard under web/app/dashboard — is documented in docs/HOSTED.md and described by docs/openapi.yaml.
The guest is WebAssembly, not source. Agent frameworks compile or supply a module. A bundled Python or JavaScript interpreter is not part of this runtime.
Install
The library is the crates.io package. The boxwren binary is published as a GitHub Release.
From a checkout:
Guests need the wasm32-wasip1 and wasm32-wasip2 targets (rustup target add).
Library
Reuse one engine. Each run gets its own store, fuel budget, memory cap, and deadline.
use Duration;
use ;
let engine = new?;
let mut limits = default;
limits.wall_time = from_secs;
let report = engine.execute?;
println!;
KillHandle::kill, from another thread, stops that run. A wall-clock deadline that expires on its own is timed_out, not killed.
Defaults: 1_000_000_000 fuel (an instruction count, not milliseconds), 32 MiB of memory, 2 seconds of wall time, 1 MiB of captured stdout and of stderr, 32 MiB maximum module size, no directories, no network, and no environment variables.
CLI
--allow-dir is host:guest, host:guest:ro, or host:guest:rw. The split is the last colon. The default mode is read-only. --allow-net is an IP, ip:port, hostname, or hostname:port. Hostname grants turn on WASI name lookup for that run. --allow-env NAME copies a host variable, and NAME=VALUE sets one explicitly. A missing directory or an unknown variable fails before the guest starts.
Ctrl-C fires the kill handle. The process prints the report and exits 0 when the status is success, 1 when the guest was stopped or trapped, and 2 when the request itself was invalid.
boxwren demo runs three embedded guests: a read of /etc/passwd, a connect to 127.0.0.1:1, and a memory grow past the cap. It exits non-zero if any of those is allowed. The secret guest calls path_open directly so the report names the path. Preview 1 std::fs often returns before the host call when no preopen matches.
Report
Schema version 1, JSON. Fields: status, exit_code, return_value, duration_ms, fuel_used, peak_memory, stdout, stderr, truncation flags, events, and warnings. An event is filesystem, network, or env, plus the target and allowed or denied.
Statuses: success, fuel_exhausted, memory_limited, timed_out, killed, trapped, rejected.
A grant of / or of the home directory is allowed and named in warnings. Rewriting that grant is a policy decision and belongs to Keywarden.
Limits of the sandbox
Wasm plus these capabilities stops the guest from reading host memory, walking out of a preopen, opening the network, inheriting environment secrets, or running past the fuel, memory, and wall-clock limits when those limits and the kill switch are left on.
It does not stop a bug in Wasmtime or Boxwren, a Spectre-class side channel, a grant pointed at ~/.ssh, or anything the host process can already do outside the sandbox. Compilation of a module is not interruptible; the module size cap is the mitigation. Software fault isolation is not a virtual machine. See docs/THREAT_MODEL.md and docs/ARCHITECTURE.md.
Python and Go
A cold boxwren process is about 6 ms on the machine in docs/BENCHMARKS.md. An agent loop that starts a process per snippet should call the library in-process instead.
Python (pip install boxwren-py, then import boxwren_py):
=
=
=
From a checkout: pip install maturin && maturin develop in crates/boxwren-py, or:
PYTHONPATH=.
Go (go get github.com/rejisterjack/boxwren/bindings/go): the package wraps the C ABI in crates/boxwren-ffi, not a second sandbox. cargo build -p boxwren-ffi --release, then go test in bindings/go. Grants use the same strings as the CLI. Engine.KillHandle() and ExecuteWith(wasm, request, kill) stop one run from any goroutine.
Hosted (optional)
Run the multi-tenant HTTP API locally:
Point the dashboard at it (NEXT_PUBLIC_BOXWREN_API_URL) from web/. Full deploy notes, CORS allowlist, and Keywarden admission: docs/HOSTED.md. OpenAPI stub: docs/openapi.yaml.
Releasing
Push an annotated tag v* on main. GitHub Actions then:
- Builds multi-OS
boxwrenCLI binaries andlibboxwren_ffiartifacts and creates a GitHub Release - Runs
cargo publish -p boxwren(needs repository secretCARGO_REGISTRY_TOKEN) - Builds and publishes
boxwren-pywheels (needs repository secretPYPI_API_TOKENand apypienvironment)
Without those secrets the GitHub Release still publishes; crates.io and PyPI jobs fail until the tokens are set.
License
MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.