Skip to content

0xBoji/wasm_agent_sandbox_platform

Repository files navigation

wasm_agent_sandbox_platform (wasp)

Default-deny WebAssembly sandbox execution for autonomous agents, local tools, and Rust-based orchestration systems.

License: MIT Rust crates.io CI

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.


Table of Contents


What this crate is

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 .wasm instead 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.

Why it exists

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.

Who should use it

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.


Who should not use it

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.


Status

Current implementation includes:

  • wasp run <module.wasm> execution flow,
  • default-deny filesystem and environment behavior,
  • repeatable --allow-dir support,
  • repeatable --env KEY=VALUE support,
  • 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-report sample for explicit env-path verification,
  • manifest kind: "wasm-run" support for declarative execution of existing .wasm modules,
  • manifest-level read-only / read-write directory 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.


TL;DR Quickstart

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

That gets you:

  • a Wasm execution sandbox with no ambient host env,
  • access only to /tmp/workspace,
  • a single JSON object describing the outcome.

Installation

When the crate is published, the standard Cargo install path is:

cargo install wasm_agent_sandbox_platform --bin wasp

If 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) --git

If you already have the repo checked out locally:

cargo install --path . --bin wasp

The mental model

The easiest way to reason about wasp is:

  1. Choose a Wasm module

    • wasp executes a guest .wasm module, not a shell command.
  2. Declare exactly what the guest may see

    • no directories unless you pass --allow-dir
    • no environment variables unless you pass --env
  3. Run inside WASI preview1

    • the guest starts at _start
    • stdout/stderr are captured into memory
  4. 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

CLI command reference

wasp run <module.wasm>

Executes a WebAssembly module inside the WASI sandbox.

Flags

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

Example

wasp run ./agent_task.wasm --allow-dir /tmp/workspace --env TASK_ID=abc123

wasp sample list

Lists the built-in guest tasks that ship inside the binary.

wasp sample list

wasp sample build <sample> <output.wasm>

Builds 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.wasm

wasp sample run <sample> --workspace <path>

Compiles 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 ./workspace

You can also forward explicit guest env values through the same path:

wasp sample run workspace-probe --workspace ./workspace --env TASK_ID=abc123

The built-in samples stay intentionally small, but the env plumbing is the same one used by normal wasp run.

wasp task run --manifest <task.json>

Runs a higher-level task manifest that resolves to a built-in sample execution flow.

wasp task run --manifest ./task.json

JSON output contract

wasp always writes a single JSON object to stdout.

Success example

{
  "status": "success",
  "exit_code": 0,
  "execution_time_ms": 12,
  "stdout": "task completed\n",
  "stderr": "",
  "error": null
}

Host-side failure example

{
  "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"
}

Field meanings

  • status: success or error
  • exit_code: guest exit code if available, otherwise null
  • execution_time_ms: elapsed runtime in milliseconds
  • stdout: captured guest stdout
  • stderr: captured guest stderr
  • error: host-side or structured guest failure message

Security model

wasp follows a default-deny model.

Denied by default

  • host filesystem access
  • ambient environment variables
  • terminal output passthrough

Allowed only when explicitly granted

  • preopened directories via --allow-dir
  • injected env vars via --env

Verified boundary behavior in tests

  • 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

Flywheel Step 5 sample workflow

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.

1. Discover what is built in

wasp sample list

Current samples:

  • workspace-probe
    • reads input.txt from an allowed workspace and prints its contents
  • workspace-write
    • writes agent-note.txt inside an allowed workspace and prints written
  • env-report
    • prints injected guest environment values

2. Build a sample guest explicitly

wasp sample build workspace-probe ./workspace-probe.wasm

Use this when you want a concrete .wasm artifact on disk for debugging, demos, or separate orchestration steps.

3. Run a sample end-to-end without stitching together raw shell build steps

mkdir -p ./workspace
printf 'hello from workspace\n' > ./workspace/input.txt
wasp sample run workspace-probe --workspace ./workspace

Expected 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.txt

Expected 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=abc123

Expected guest stdout: the injected env string, for example TASK_ID=abc123.

Why this matters

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.


Task manifest flow

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

Important behavior:

  • relative workspace paths 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.

Additional manifest kind: wasm-run

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-only
  • read-write

Important compatibility note:

  • manifest access modes are additive higher-level ergonomics
  • plain CLI --allow-dir still 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.

Rust guest templates and examples

For authors who want to move beyond the built-in WAT guests, the repo now includes Rust guest examples:

  • examples/rust-guests/workspace_probe
  • examples/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 --release

Then run the resulting guest with normal WASP execution:

wasp run ./target/wasm32-wasip1/release/workspace_probe_guest.wasm --allow-dir ./workspace

Use these examples when you want editable Rust guest code.
Use wasp sample ... when you want the shortest no-raw-shell Step 5 demo path.


Typical workflows

1. Run a fully isolated guest

wasp run ./task.wasm

Use this when the guest should not touch host files or see any host environment variables.

2. Give the guest access to one workspace directory

wasp run ./task.wasm --allow-dir ./workspace

Use this when the guest should be able to read or write only inside one explicit directory tree.

3. Pass explicit execution metadata

wasp run ./task.wasm --env TASK_ID=abc123 --env MODE=review

Use this when the guest needs a few specific runtime values without inheriting the entire host environment.

4. Combine both for agent runs

wasp run ./task.wasm --allow-dir ./workspace --env TASK_ID=abc123 --env AGENT_ROLE=reviewer

Testing and verification

Before claiming changes are complete in this repo, run:

cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo package --locked

Current automated coverage includes:

  • CLI parsing behavior,
  • JSON contract behavior,
  • built-in sample list/build/run behavior,
  • task manifest behavior,
  • env-report sample behavior,
  • wasm-run manifest 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.sh

Release automation

This 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
  • .github/workflows/release-plz.yml

    • opens release PRs from main
    • can publish to crates.io when CARGO_REGISTRY_TOKEN is configured

For a real release, the remaining operator steps are mostly repository settings and secrets:

  • set CARGO_REGISTRY_TOKEN in GitHub Actions secrets
  • confirm the crate name/version is ready for publication
  • review the release-plz PR before merging/publishing

Limitations and non-goals

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.


Roadmap / likely next improvements

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.

License

MIT

About

Default-deny WebAssembly sandbox execution for autonomous agents, local tools, and Rust-based orchestration systems.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors