Run neural policies you don't fully trust — safely, and swap them live.
Kelvane is a small, honest toolkit for safety-first neural inference on
WebAssembly. It runs untrusted .wasm modules inside a sandbox that enforces a
per-invocation compute and authority budget, gives them host-owned neural
inference (CPU or CUDA), and lets you hot-swap a module at runtime — plus a
compact, reproducible multi-agent reinforcement learning reference to produce
policies to run.
Author: Muhammad Rakibul Islam. License: Apache-2.0. No networking. No security primitives. General-purpose research infrastructure built from publicly known techniques.
Running a neural policy — especially one you didn't write or can't fully audit — means running someone else's code. Kelvane's contribution is a safety-first place to run it: each decision executes in a fresh sandbox with a hard memory ceiling, a per-call CPU budget, and zero ambient authority (no filesystem, no network, no stdio), and the model itself is owned by the host — the module only gets to ask for an inference, never touches the weights or the accelerator directly. You can replace the running module mid-flight without a restart. The included RL framework is a self-contained way to train, export, and then run a policy through that boundary end to end.
| Component | What it does |
|---|---|
kelvane-runtime (Rust) |
Loads untrusted, hot-swappable WebAssembly modules; enforces a per-call memory ceiling, CPU fuel budget, and zero-ambient-authority sandbox; serves host-owned inference (CPU via tract, optional CUDA via ONNX Runtime). |
kelvane-sdk (Rust) |
The guest ABI: a length-delimited packed pointer+length return, host-driven allocation, and an export_module! macro. |
kelvane-marl (Python) |
A cooperative gridworld, a GPU-capable vectorized environment, MAPPO and QMIX trainers, and ONNX policy export. |
# 1) train a policy and export it to ONNX (GPU used automatically if present)
cd kelvane-marl
pip install numpy torch gymnasium pettingzoo onnx onnxruntime
python -m kelvane_marl.mappo --updates 120 --envs 64
python -m kelvane_marl.export_onnx
cd ..
# 2) build the example modules for the wasm32-wasip1 platform
rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1 -p policy-module -p scripted-module
# 3) run the end-to-end demo: inference in the sandbox, the memory cap, hot-swap
cargo run --release -p kelvane-demo kelvane-marl (Python) kelvane-runtime (Rust)
┌───────────────────────────┐ ┌──────────────────────────────────┐
│ gridworld → MAPPO / QMIX │ ONNX │ sandbox (fresh store per call) │
│ vectorized env (GPU) │ ───────► │ • memory ceiling │
│ export FP32 / INT8 │ model │ • CPU fuel budget │
└───────────────────────────┘ │ • zero ambient authority │
│ • hot-swap │
input bytes ──► module_alloc ──► process │ host-owned inference: │
▲ │ │ kelvane::infer ──► CPU / CUDA │
└──────── output bytes ◄───────┘ └──────────────────────────────────┘
The guest ships bytes in and gets bytes out through the kelvane-sdk ABI; when a
module needs a neural decision it calls the host's kelvane::infer, which runs
the ONNX model on the selected backend and returns the scores.
cargo run --release -p kelvane-demo shows the three core behaviors:
- (a) inference in the sandbox — the exported policy runs inside a WASM module and returns a decision;
- (b) the memory cap — a module run under a 16 KB memory ceiling is stopped before it can exceed it;
- (c) hot-swap — a scripted module is swapped for the neural policy at runtime, and the next call transparently runs the new one.
Kelvane is a thin, opinionated layer on top of Wasmtime, not a replacement for it, and not a general plugin framework.
- Wasmtime is the WebAssembly engine Kelvane is built on. Kelvane adds the opinionated policy: a per-invocation memory + CPU budget, a fresh store per call, a host-owned inference capability, and safe hot-swap.
- extism is a general plugin system for many host languages. Kelvane is narrower on purpose: it focuses on running inference policies under a per-call compute/authority budget rather than being a broad plugin ABI.
- Wassette is a WebAssembly-based runtime for agent tools. Kelvane does not aim to be an agent/tool runtime; it is the smaller building block that runs a single untrusted policy safely, with the model owned by the host.
If you need a full plugin framework or tool runtime, use those. If you want a minimal, auditable place to run a neural policy under a hard per-call budget with host-owned inference, that is what Kelvane is.
Two things are measured, because they answer different questions:
- End-to-end (
bench_e2e) — a fullModuleRuntime::invoke: fresh sandbox store, WASI + inference linker setup, instantiation, the guestmodule_alloc+ input copy, theprocessABI round trip (including the guest's call back into hostkelvane::infer), reading the packed output,module_dealloc, and store teardown. This is the latency a caller actually pays per decision. - Inference-only (
bench) — just the host-sideModel::run(one ONNX forward pass), for comparison. It is not the per-call cost.
Exported [1,4,11,11] policy, 2000 iterations after 50 warm-up iterations.
Environment. AMD Ryzen 7 3700X (8C/16T) · NVIDIA RTX 2080 SUPER (driver 610.62, CUDA 12.x) · Ubuntu 24.04.2 on WSL2 (kernel 6.18, Windows 10) · rustc 1.94.1 · wasmtime 46.0.1 · tract-onnx 0.21.12 · ort 2.0.0-rc.12 (ONNX Runtime, CUDA EP). All figures are from this one machine.
| Path | Backend | median | mean | p95 | p99 | throughput |
|---|---|---|---|---|---|---|
| End-to-end | CPU (tract) |
175 µs | 184 µs | 227 µs | ~290 µs | ~5,400 calls/s |
| End-to-end | GPU (CUDA/ORT) | 345 µs | 353 µs | 400 µs | 447 µs | ~2,830 calls/s |
| Inference-only | CPU (tract) |
16.3 µs | 17.0 µs | 21.5 µs | ~32 µs | ~58,900 inf/s |
| Inference-only | GPU (CUDA/ORT) | 158 µs | 164 µs | 192 µs | 224 µs | ~6,080 inf/s |
Two honest takeaways for this small model on this machine:
- The full sandboxed call is ~11× the inference number (175 µs vs 16.3 µs on CPU). Per-call store construction, instantiation, and the ABI round trip — not inference — dominate. (On wasmtime 46 this end-to-end cost is ~9% higher than on the old wasmtime 29 — the engine upgrade that fixed the sandbox-escape CVEs moved per-call instantiation from ~161 µs to ~175 µs; inference is unchanged.)
- The GPU path is real but slower here — about 10× slower than CPU (158 µs vs
16.3 µs inference-only). Expected for a tiny
[1,4,11,11]→7model: kernel launch and host⇄device transfer swamp the compute, andtract's optimized CPU path wins. CUDA is present for larger models, not as a speed-up for this one.
The cuda feature depends on ONNX Runtime, which ort fetches over TLS at build
time; that pulls in openssl-sys, so a CUDA build needs the OpenSSL development
headers and pkg-config (e.g. apt-get install libssl-dev pkg-config on
Debian/Ubuntu) plus, at run time, the CUDA runtime and cuDNN 9 on the library
path. Reproduce with:
cargo run --release --example bench_e2e -p kelvane-runtime # CPU, end-to-end
cargo run --release --example bench -p kelvane-runtime # CPU, inference-only
cargo run --release --features cuda --example bench_e2e -p kelvane-runtime # GPUThese are not general performance claims — measure your own model and hardware.
Kelvane is not tied to the gridworld policy it ships. The generality test
(crates/kelvane-runtime/tests/generality.rs) loads a small CNN trained on the
scikit-learn digits dataset (8×8 handwritten digits, 10 classes — a different
domain and a different input shape, [1,1,8,8], than the [1,4,11,11] policy)
and classifies held-out digits end to end through the sandbox, via the same
generic guest module. The model (~97% test accuracy) and its provenance/license
are documented in crates/kelvane-runtime/tests/models/generate_digits.py.
Current generality boundary: the input shape is fixed at load time and the batch dimension is 1; the example guest reads a flat feature vector and argmaxes up to 64 output scores. Models within those limits (fixed shape, batch-1, ≤64 outputs) run unchanged; anything needing dynamic shapes or batching does not yet.
Kelvane is pre-1.0 (0.x). "Stable" below means we will not change it
without bumping the minor version (0.x → 0.(x+1)) and noting it in the
changelog; "experimental" means it may change more freely. When in doubt we mark
things experimental — under-promising is deliberate.
kelvane-runtime
- Stable:
ModuleRuntimeand its methods (new,load_module,invoke,hot_swap,call_count,model_backend);ExecutionLimitsand its fields;Modeland its methods (run,input_len,backend). - Experimental:
ModuleRuntime::load_modeland the freeinference::load_model— the explicit fixedinput_shapeargument is likely to change once shapes can be auto-detected from the model (or made variable). Expect this signature to gain a default or move to a builder; depend on it accordingly. - Not covered by semver: the string values returned by
model_backend/Model::backend(e.g."cpu(tract)") are informational — do not parse them. Theinternalsfeature is private (fuzz-only). Thecudabackend-selection / fallback behavior may change.
kelvane-sdk — the guest ABI
- Stable: the
export_module!macro and its byte-in/byte-out handler contract;pack; the packed-i64return layout; themodule_alloc/process/module_deallocexport signatures; thekelvane::inferimport signature and its little-endian flat-f32tensor convention. Guests built against these keep working. The full contract is in the crate's rustdoc. - Experimental:
alloc_bytes,dealloc_bytes, andrunare the low-level plumbing behindexport_module!— prefer the macro.
The JSON shape of the example modules' output
({"module":...,"action":...}) is an example convention, not part of the
ABI, and is not stable.
Kelvane's threat model — what the sandbox does and does not protect against,
each claim tied to a mechanism and a test — is documented in
SECURITY.md, along with the trust boundary, the assumptions the
guarantees rest on, and how to report a vulnerability. In short: it defends the
host against an untrusted guest (memory cap, fuel/DoS bounds, zero
ambient authority, per-call isolation, host-owned model, a patched engine); it
does not protect against side channels, a malicious host, bugs in the engine
itself, or the safety of the model's own decisions.
CI builds and runs the full test suite on every cell below. WASM modules are
built and the ONNX fixtures are committed before the test step, so nothing is
skipped — a green cell means the sandbox was actually exercised on that platform
(not merely compiled). All cells build with --locked.
| OS (runner) | Rust stable | Rust 1.88.0 (MSRV) |
|---|---|---|
Linux — ubuntu-latest, x86-64 |
✅ | ✅ |
macOS — macos-latest, arm64 |
✅ | ✅ |
Windows — windows-latest, x86-64 |
✅ | ✅ |
- MSRV: 1.94.0, determined empirically — the dependency tree's floor is
wasmtime/wasmtime-wasi46 andcranelift0.133 (edition 2024, rustc 1.94.0). 1.93 and below fail to build; the MSRV CI row pins exactly 1.94.0.rust-versioninCargo.tomlmatches. (Raised from 1.88 by the wasmtime upgrade that resolved the sandbox-escape advisories.) - wasm target: the example guest modules compile to
wasm32-wasip1(rustup target add wasm32-wasip1). The guest code is gated towasm32, so a host-sidecargo build/clippy --workspaceis link-clean on every OS. - CUDA backend (
cudafeature): a separate, non-blocking, build-only Linux job (the runners have no GPU) — it compiles and lint-checks the backend but does not run it. Building it needslibssl-dev+pkg-config; running it needs the CUDA runtime + cuDNN 9 on the library path. - Reproducibility:
Cargo.lockis committed and authoritative; CI builds--lockedso an out-of-sync lock fails loudly.ortis pinned to=2.0.0-rc.12. Python deps inkelvane-marlare fully pinned. - Toolchain versions behind the benchmark numbers are listed in the Benchmarks Environment line above and match what CI uses.
- Licenses (
cargo deny): the whole dependency tree is permissive and Apache-2.0-compatible (Apache-2.0/MIT/BSD/ISC/Zlib/Unicode-3.0/Unlicense/ BlueOak). The only copyleft entries (ittapiGPL-2.0,r-efiLGPL-2.1) are the copyleft arm of dual/multi-licensed crates; the permissive arm is selected, so no copyleft obligation and no GPL/AGPL surprise. - Advisories (
cargo audit, a blocking CI job): no known vulnerabilities. The earlier 19 RustSec advisories againstwasmtime/wasmtime-wasi29 — including two critical sandbox escapes (RUSTSEC-2026-0095/0096) and a HIGH WASIpath_openbypass (RUSTSEC-2026-0149) — were resolved by upgrading towasmtime46.0.1. The only remaining finding is one unmaintained advisory forpaste(a transitive proc-macro dependency), which is not a vulnerability.
Kelvane is open source under the Apache License 2.0. You may use, modify, and
distribute it, including commercially, provided you retain the copyright notice
and NOTICE and state significant changes, per the license. Copyright 2026
Muhammad Rakibul Islam. See LICENSE and NOTICE.
- Additional inference backends behind Cargo features.
- A richer example-module gallery.
- Optional WIT / component-model interface for the guest ABI.
- More reproducible RL reference scenarios.
See CONTRIBUTING.md. All Rust checks (fmt, clippy -D warnings, build, test) and the Python pytest suite must pass.
Questions about Kelvane? rakib.islam@rutgers.edu
Author: Muhammad Rakibul Islam — https://github.com/rakib-nyc