Default-deny WebAssembly sandbox execution for autonomous agents, local tools, and Rust-based orchestration systems.
Think of
wasm_agent_sandbox_platform(wasp) as a strict, local execution firewall for agent-generated code: instead of giving agents raw shell access, you hand them a small Wasm runtime with an explicit allowlist for directories and environment variables, then receive one predictable JSON object back.
- What this crate is
- Why it exists
- Who should use it
- Who should not use it
- Status
- TL;DR Quickstart
- Installation
- The mental model
- CLI command reference
- JSON output contract
- Security model
- Flywheel Step 5 sample workflow
- Task manifest flow
- Rust guest templates and examples
- Typical workflows
- Testing and verification
- Release automation
- Limitations and non-goals
- Roadmap / likely next improvements
- License
wasp is a Rust command-line tool for executing WebAssembly modules inside a default-deny WASI sandbox.
It is designed for local agent systems that need a safer alternative to direct shell execution. At the current MVP level, wasp helps enforce a clear boundary between:
- the host machine,
- the permissions explicitly granted to a guest module,
- and the machine-readable output returned to higher-level tools.
In practice, that means:
- guest code runs as
.wasminstead of arbitrary shell commands, - filesystem access is blocked unless granted with
--allow-dir, - environment variables are blocked unless granted with
--env KEY=VALUE, - stdout/stderr are captured instead of leaking into the host terminal,
- the caller always receives one JSON object summarizing the run.
Autonomous coding agents are useful, but raw shell access is a terrible default trust boundary.
If an agent can execute arbitrary host commands directly, even a minor prompt failure can lead to:
- accidental file deletion,
- credential leakage from inherited environment variables,
- running untrusted dependency installers on the host,
- hard-to-parse mixed stdout/stderr output,
- orchestration failures caused by human-oriented terminal logs.
wasp exists to make that execution layer:
- default-deny instead of ambient-permission,
- JSON-native instead of terminal-native,
- explicitly allowlisted instead of “whatever the current shell can see”,
- agent-friendly without pretending the host is a safe sandbox.
This crate is a good fit if you are building:
- local autonomous coding agents,
- Rust-based agent runtimes and orchestrators,
- shell-to-Wasm execution wrappers,
- multi-tool systems that need predictable machine-readable execution results,
- local-first sandboxes for generated code and tests.
It is especially useful when you want the execution boundary itself to be part of the product contract, not an implementation detail hidden behind shell scripting.
This is not the right tool if you need:
- full Linux container isolation,
- a general-purpose VM manager,
- transparent execution of arbitrary native binaries,
- network sandboxing beyond what your Wasm/WASI runtime exposes,
- a cloud multi-tenant security boundary by itself.
wasp is a strong local execution boundary for Wasm modules, but it is not a replacement for containers, VMs, or hardened OS-level sandboxing in hostile environments.
Current implementation includes:
wasp run <module.wasm>execution flow,- default-deny filesystem and environment behavior,
- repeatable
--allow-dirsupport, - repeatable
--env KEY=VALUEsupport, - captured guest stdout/stderr,
- stable JSON output for both success and host-side failure cases,
- a shell-first installer at
scripts/install.sh, - crates.io-ready package metadata in
Cargo.toml, - GitHub Actions CI and release-plz workflow scaffolding,
- built-in
wasp sample ...tooling for Step 5 demos without manual raw shell build steps, - built-in
env-reportsample for explicit env-path verification, - manifest
kind: "wasm-run"support for declarative execution of existing.wasmmodules, - manifest-level
read-only/read-writedirectory access modes, - integration tests covering env leakage, file read/write gating, nested path behavior, symlink escape prevention, and rename behavior inside preopened directories.
This crate is intended for local execution only.
If you just want the shortest path to using wasp:
# run a Wasm guest with one allowed directory and one injected env var
wasp run ./agent_task.wasm --allow-dir /tmp/workspace --env TASK_ID=abc123That gets you:
- a Wasm execution sandbox with no ambient host env,
- access only to
/tmp/workspace, - a single JSON object describing the outcome.
When the crate is published, the standard Cargo install path is:
cargo install wasm_agent_sandbox_platform --bin waspIf you prefer a curl/bash installer:
bash <(curl -fsSL https://raw.githubusercontent.com/0xBoji/wasm_agent_sandbox_platform/main/scripts/install.sh)If you want to force installation directly from GitHub instead of crates.io:
bash <(curl -fsSL https://raw.githubusercontent.com/0xBoji/wasm_agent_sandbox_platform/main/scripts/install.sh) --gitIf you already have the repo checked out locally:
cargo install --path . --bin waspThe easiest way to reason about wasp is:
-
Choose a Wasm module
waspexecutes a guest.wasmmodule, not a shell command.
-
Declare exactly what the guest may see
- no directories unless you pass
--allow-dir - no environment variables unless you pass
--env
- no directories unless you pass
-
Run inside WASI preview1
- the guest starts at
_start - stdout/stderr are captured into memory
- the guest starts at
-
Receive one JSON envelope back
- callers do not need to scrape terminal noise
- host-side failures and guest-side output stay in the same stable shape
Executes a WebAssembly module inside the WASI sandbox.
-
--allow-dir <path>- repeatable
- preopens a host directory for guest filesystem access
- no other host directories are implicitly exposed
-
--env <KEY=VALUE>- repeatable
- injects a single environment variable into the guest
- host environment variables are otherwise not inherited
wasp run ./agent_task.wasm --allow-dir /tmp/workspace --env TASK_ID=abc123Lists the built-in guest tasks that ship inside the binary.
wasp sample listBuilds a built-in sample guest into a standalone .wasm file without making the caller manually assemble a guest build flow.
wasp sample build workspace-probe ./workspace-probe.wasmCompiles a built-in sample guest and immediately runs it through the same sandbox path as wasp run.
wasp sample run workspace-probe --workspace ./workspace
wasp sample run workspace-write --workspace ./workspaceYou can also forward explicit guest env values through the same path:
wasp sample run workspace-probe --workspace ./workspace --env TASK_ID=abc123The built-in samples stay intentionally small, but the env plumbing is the same one used by normal wasp run.
Runs a higher-level task manifest that resolves to a built-in sample execution flow.
wasp task run --manifest ./task.jsonwasp always writes a single JSON object to stdout.
{
"status": "success",
"exit_code": 0,
"execution_time_ms": 12,
"stdout": "task completed\n",
"stderr": "",
"error": null
}{
"status": "error",
"exit_code": null,
"execution_time_ms": 1,
"stdout": "",
"stderr": "",
"error": "failed to load wasm module `./missing-module.wasm`: failed to read input file"
}status:successorerrorexit_code: guest exit code if available, otherwisenullexecution_time_ms: elapsed runtime in millisecondsstdout: captured guest stdoutstderr: captured guest stderrerror: host-side or structured guest failure message
wasp follows a default-deny model.
- host filesystem access
- ambient environment variables
- terminal output passthrough
- preopened directories via
--allow-dir - injected env vars via
--env
- host env does not leak without
--env - guest can read/write only inside an allowed directory
- nested paths inside an allowed directory work
..traversal does not escape one preopen into a sibling directory- symlinks pointing outside a preopen do not bypass the sandbox boundary
- rename operations inside a preopen update real host paths
The whole point of this section is to keep agents from falling back to ad-hoc raw shell just to demonstrate a Wasm task flow.
wasp sample listCurrent samples:
workspace-probe- reads
input.txtfrom an allowed workspace and prints its contents
- reads
workspace-write- writes
agent-note.txtinside an allowed workspace and printswritten
- writes
env-report- prints injected guest environment values
wasp sample build workspace-probe ./workspace-probe.wasmUse this when you want a concrete .wasm artifact on disk for debugging, demos, or separate orchestration steps.
mkdir -p ./workspace
printf 'hello from workspace\n' > ./workspace/input.txt
wasp sample run workspace-probe --workspace ./workspaceExpected outer result: the normal WASP JSON envelope.
Expected guest stdout: the contents of ./workspace/input.txt.
For a write-oriented sample:
mkdir -p ./workspace
wasp sample run workspace-write --workspace ./workspace
cat ./workspace/agent-note.txtExpected guest side effect: agent-note.txt appears inside the allowed workspace only.
For an env-focused sample:
wasp sample run env-report --workspace . --env TASK_ID=abc123Expected guest stdout: the injected env string, for example TASK_ID=abc123.
This gives an agent a direct tool path for Step 5:
- pick a sample,
- build it if needed,
- run it through the same sandbox engine,
- inspect the JSON result,
- inspect only the explicitly allowed workspace side effects.
That is a much tighter and more reviewable execution loop than hand-assembling raw shell commands every time.
When you want a slightly higher-level task entrypoint than wasp sample run, use a JSON manifest:
{
"kind": "sample",
"sample": "workspace-probe",
"workspace": "workspace",
"env": {
"TASK_ID": "abc123"
}
}Run it with:
wasp task run --manifest ./task.jsonImportant behavior:
- relative
workspacepaths are resolved relative to the manifest file location - the manifest still routes through the same sandbox boundary as
wasp run - the final stdout remains the standard WASP JSON contract
This makes it easier for agents or orchestrators to store a reusable execution plan on disk instead of rebuilding raw command lines every time.
When you already have a .wasm guest artifact and want a declarative wrapper around wasp run, use:
{
"kind": "wasm-run",
"module": "workspace-probe.wasm",
"allow_dirs": [
{
"path": "workspace",
"access": "read-only"
}
],
"env": {
"TASK_ID": "abc123"
}
}Supported directory access modes:
read-onlyread-write
Important compatibility note:
- manifest access modes are additive higher-level ergonomics
- plain CLI
--allow-dirstill behaves exactly as before and means read-write
This gives orchestrators a declarative way to say:
- what module to run,
- what directories to expose,
- whether a directory should be read-only or read-write,
- which env values to inject.
For authors who want to move beyond the built-in WAT guests, the repo now includes Rust guest examples:
examples/rust-guests/workspace_probeexamples/rust-guests/workspace_write
These are intentionally small starter crates showing how to write a guest that:
- reads a file from an allowed workspace
- writes a file into an allowed workspace
Example build flow from inside one of those guest directories:
cargo build --target wasm32-wasip1 --releaseThen run the resulting guest with normal WASP execution:
wasp run ./target/wasm32-wasip1/release/workspace_probe_guest.wasm --allow-dir ./workspaceUse these examples when you want editable Rust guest code.
Use wasp sample ... when you want the shortest no-raw-shell Step 5 demo path.
wasp run ./task.wasmUse this when the guest should not touch host files or see any host environment variables.
wasp run ./task.wasm --allow-dir ./workspaceUse this when the guest should be able to read or write only inside one explicit directory tree.
wasp run ./task.wasm --env TASK_ID=abc123 --env MODE=reviewUse this when the guest needs a few specific runtime values without inheriting the entire host environment.
wasp run ./task.wasm --allow-dir ./workspace --env TASK_ID=abc123 --env AGENT_ROLE=reviewerBefore claiming changes are complete in this repo, run:
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo package --lockedCurrent automated coverage includes:
- CLI parsing behavior,
- JSON contract behavior,
- built-in sample list/build/run behavior,
- task manifest behavior,
- env-report sample behavior,
wasm-runmanifest behavior,- manifest read-only enforcement,
- env injection and env default-deny behavior,
- file read/write behavior under
--allow-dir, - nested path access,
- sibling-directory escape prevention,
- symlink escape prevention,
- rename behavior within a preopened directory.
You can also syntax-check the installer script directly:
bash -n scripts/install.shThis repo now includes the baseline release-readiness scaffolding expected for a publishable Rust CLI:
-
.github/workflows/ci.yml- runs
cargo fmt --check - runs
cargo clippy --all-targets --all-features -- -D warnings - runs
cargo test --all-targets --all-features - runs
cargo package --locked
- runs
-
.github/workflows/release-plz.yml- opens release PRs from
main - can publish to crates.io when
CARGO_REGISTRY_TOKENis configured
- opens release PRs from
For a real release, the remaining operator steps are mostly repository settings and secrets:
- set
CARGO_REGISTRY_TOKENin GitHub Actions secrets - confirm the crate name/version is ready for publication
- review the release-plz PR before merging/publishing
Current non-goals include:
- native binary execution,
- non-Wasm guest workflows,
- full container orchestration,
- WAN or distributed trust guarantees,
- proving security against hostile kernel- or hypervisor-level attackers.
This tool is meant to be one execution-security pillar inside a larger local agent infrastructure.
Likely next improvements:
- publish the crate to crates.io,
- add release automation for installable binaries,
- broaden guest fixtures around cross-directory rename/link semantics,
- add larger-file and partial-write coverage,
- improve user-facing docs around building guest Wasm modules.
- add richer manifest kinds beyond built-in sample execution.
MIT