WASM sandbox: getting started¶
This walkthrough takes one small tool from source to a capability-limited run. By the end you will have compiled a tool to WebAssembly, written a deny-by-default permission manifest, pre-flighted the tool without running it, and run it under the manifest with an audit receipt — the full operator path the sandboxed tools design describes.
The sandbox is the only place untrusted tool code runs in Synapse: a tool gets no ambient authority — no filesystem, no network, no clock — beyond what its manifest grants, and the manifest is bound to one exact module by content digest.
Prerequisites¶
- The WASM runtime is a heavy dependency, so it ships behind an optional extra:
pip install 'synapse-channel[wasm]'
The validate verb works without it; test and run report this install hint when it is
absent rather than failing obscurely.
- A way to produce a
.wasmmodule. This guide compiles a Rust tool to thewasm32-unknown-unknowntarget; any language that emits a core WebAssembly module with an exported function works (C viaclang --target=wasm32, TinyGo, AssemblyScript, or a hand-written.watassembled withwasm-tools).
rustup target add wasm32-unknown-unknown
1. Write and compile a tool¶
A sandboxed tool is a plain WebAssembly module that exports an entrypoint. The default
entrypoint name is run. Create greet.rs:
// A minimal sandboxed tool: exports `run`, takes no host authority, returns a value.
#[no_mangle]
pub extern "C" fn run() -> i32 {
42
}
Compile it to a small module:
rustc --target wasm32-unknown-unknown --crate-type=cdylib \
-C opt-level=z -C strip=symbols -C panic=abort \
greet.rs -o greet.wasm
That produces a ~100-byte greet.wasm exporting a single function, run.
2. Write the permission manifest¶
The manifest binds the tool to one exact module by its sha256: content digest and declares
the capabilities it may use — everything is deny-by-default, so an empty filesystem/network
means none. First compute the module's digest:
python -c "import hashlib,pathlib; print('sha256:'+hashlib.sha256(pathlib.Path('greet.wasm').read_bytes()).hexdigest())"
Paste that digest into greet.manifest.json (yours will differ — the digest is unique to your
exact build):
{
"tool_id": "greet",
"content_digest": "sha256:<paste-your-digest-here>",
"resources": { "memory_bytes": 1048576, "fuel": 1000000, "wall_clock_ms": 2000 }
}
resources caps what the tool may consume: a memory ceiling, a fuel (instruction) budget, and
a wall-clock backstop in milliseconds. Omit resources to take a conservative default budget.
3. Validate the manifest¶
validate is a dry check of the policy alone — no tool is involved yet:
$ synapse sandbox validate greet.manifest.json
manifest for 'greet' is valid: 0 filesystem, 0 network grant(s), fuel 1000000, memory 1048576 bytes
If the manifest is malformed (not JSON, a missing tool_id, or a digest that is not a
sha256: digest), it reports the fault and exits non-zero.
4. Pre-flight the tool with test¶
test answers "would this tool run?" without running it. It compiles the module
(validating its structure), confirms the entrypoint is an exported function, and confirms the
module matches its manifest digest — spending no fuel and executing none of the tool's code:
$ synapse sandbox test greet.wasm --manifest greet.manifest.json
preflight for 'greet': ready to run
module valid: yes
entrypoint 'run' exported: yes
digest matches manifest: yes
exported functions: run
Because the pre-flight never executes the tool, even a tool whose entrypoint loops forever pre-flights instantly. Ask for an entrypoint the module does not export and the pre-flight tells you so — and exits non-zero:
$ synapse sandbox test greet.wasm --manifest greet.manifest.json --entrypoint main
preflight for 'greet': NOT ready
module valid: yes
entrypoint 'main' exported: no
digest matches manifest: yes
exported functions: run
reason: entrypoint 'main' is not an exported function
test exits 0 when the tool is ready, 1 when the pre-flight ran but the tool is not ready
(an invalid module, a missing entrypoint, or a digest that does not match its manifest), and
2 when it could not pre-flight at all (unreadable files, or the missing [wasm] extra). That
makes it a safe gate in a script:
synapse sandbox test greet.wasm --manifest greet.manifest.json \
&& synapse sandbox run greet.wasm --manifest greet.manifest.json --approve
Add --json to either verb to get the machine-readable report instead of the summary.
5. Run the tool under its manifest¶
run executes the tool under the manifest's grants. It re-checks the module against the
manifest digest (a swapped module is refused) and requires an explicit --approve, so a
capability-bearing run is always an operator decision:
$ synapse sandbox run greet.wasm --manifest greet.manifest.json --approve
ran 'greet' — exit ok, fuel used 2, output sha256:73475cb…3a8049
granted: resource:mem=1048576,fuel=1000000,wall=2000ms
Without --approve the run stops and tells you to confirm; with a module that does not match
the manifest digest it refuses with digest_mismatch. The printed line is a bounded run
receipt — the tool id, how it exited, the fuel it burned, the digest of its output, and the
capabilities it was granted — the same kind of audit evidence a release receipt carries. Add
--json for the full receipt.
Granting access¶
The tool above needed nothing from the host. To let a tool read or write a directory, add a
filesystem grant — the tool sees only the virtual guest_path, never the host root:
{
"tool_id": "formatter",
"content_digest": "sha256:<your-digest>",
"filesystem": [
{ "host_path": "/srv/in", "guest_path": "/in", "write": false },
{ "host_path": "/srv/out", "guest_path": "/out", "write": true }
],
"resources": { "memory_bytes": 1048576, "fuel": 1000000, "wall_clock_ms": 2000 }
}
Network is denied by construction — WASI preview1 exposes no sockets — so a network grant is
recorded as policy but a tool reaches the network only through a host import that is never
linked. Each grant is evaluated by the same deny-by-default ACL model as every other access in
Synapse, never a parallel one.
Where to go next¶
- The sandboxed tools and marketplace design explains the trust chain end to end and the marketplace boundary it builds toward.
- Signed capability cards would give a distributed tool verifiable provenance — a marketplace precondition.
- Identity and ACL is the deny-by-default model the sandbox reuses.