WebAssembly Crypto Agent Sandboxing: True Memory Isolation for AI Workloads
How to use WebAssembly to run cryptographic operations inside AI agents with hardware-enforced memory isolation, eliminating cross-agent key leakage.
AI agents often handle sensitive material: encryption keys, user credentials, private documents, and intermediate computation that must never cross agent boundaries. The problem is that most agent runtimes share a single process—and a single heap. One misbehaving agent can read another's memory. One compromised dependency can walk the entire heap and exfiltrate every key in flight.
WebAssembly (WASM) changes this. WASM modules execute inside a bounded linear memory that the host controls. The module cannot reach outside its own allocation. This property, originally designed for browser sandboxing, turns out to be exactly what agent architectures need for cryptographic isolation.
This post is a practical guide to WASM crypto sandboxing: why it works, how to implement it in Node.js and Deno, and what it cannot protect against.
Why Process-Level Isolation Is Not Enough
The conventional answer to agent isolation is separate processes. Spin up a subprocess per agent, use OS-level memory protection, communicate over pipes or sockets. That works, but it carries real costs:
- Startup latency. Forking a Node.js process takes 50–200 ms. For agents that compose dozens of tool calls per request, that adds up.
- IPC overhead. Every key derivation, every encrypt/decrypt, every signature verification requires serializing data across a process boundary.
- Operational complexity. Process supervisors, health checks, restart policies—the infrastructure layer grows.
WASM gives you a different trade-off: near-zero startup, in-process execution, but bounded memory per module. The module runs in the same thread as the host but cannot see outside its allocation.
How WASM Linear Memory Works
Every WASM module declares a memory section specifying its initial size and optional maximum in 64 KB pages. The host (Node.js, Deno, a Rust runtime) allocates that buffer and hands a reference to the module. The module's code can only address offsets within that buffer.
const { instance } = await WebAssembly.instantiate(wasmBytes, {
env: {
// Imports the module calls into the host
},
});
// The module's entire heap is this ArrayBuffer
const moduleMemory: ArrayBuffer = instance.exports.memory.buffer;
Any pointer the WASM code dereferences is an offset into moduleMemory. There is no way to address outside it—not through pointer arithmetic, not through a bug in the module's code. The WebAssembly specification defines out-of-bounds memory access as a trap, not undefined behavior.
This means you can load a cryptographic library compiled to WASM, give it a key, and know that even if the library has a memory-safety bug, it cannot reach keys held in other modules or the host JavaScript heap.
Compiling a Crypto Library to WASM
libsodium ships a WASM build (libsodium.js) that works in browsers and Node.js. It is a good default for symmetric encryption, key derivation, and signatures. For more specialized operations (secp256k1, BLS12-381), purpose-built WASM libraries exist.
import _sodium from "libsodium-wrappers-sumo";
await _sodium.ready;
const sodium = _sodium;
// sodium is now backed by a WASM module with its own linear memory
// Keys generated here live in sodium's heap, not the V8 heap
const keypair = sodium.crypto_box_keypair();
The critical point: keypair.publicKey and keypair.secretKey are Uint8Array views into the WASM module's memory. If you copy them into the V8 heap (new Uint8Array(keypair.secretKey)) the key leaves the sandbox. Keep them as views for as long as possible, and wipe the source buffer when done:
sodium.memzero(keypair.secretKey);
memzero calls into the WASM module and zeroes the bytes. The optimizer cannot elide it (unlike memset in native C, which a compiler may remove if it determines the memory is never read again).
Per-Agent Module Instances
The isolation property comes from using separate module instances per agent. If two agents share the same libsodium instance, they share the same linear memory, and the isolation guarantee evaporates.
class AgentCryptoContext {
private sodium: typeof _sodium;
static async create(): Promise<AgentCryptoContext> {
// Each call to ready creates a fresh WASM instance
const sodium = await import("libsodium-wrappers-sumo").then(
async (mod) => { await mod.ready; return mod; }
);
return new AgentCryptoContext(sodium);
}
private constructor(sodium: typeof _sodium) {
this.sodium = sodium;
}
deriveKey(password: Uint8Array, salt: Uint8Array): Uint8Array {
return this.sodium.crypto_pwhash(
32,
password,
salt,
this.sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
this.sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
this.sodium.crypto_pwhash_ALG_DEFAULT
);
}
destroy(): void {
// Sodium does not expose a direct destroy, but you can zero
// all sensitive material before releasing the reference
}
}
When the agent finishes its task, drop the reference. The WASM memory becomes eligible for garbage collection. Keys that never left the WASM heap are gone.
Integrating with an MCP Server
If your agent runtime speaks MCP, the right place to instantiate per-agent crypto contexts is in the session setup hook. BitAtlas's MCP server does this:
server.setRequestHandler(InitializeRequestSchema, async (request) => {
const sessionId = request.params.clientInfo.name;
// One WASM crypto context per session
const crypto = await AgentCryptoContext.create();
sessionContexts.set(sessionId, { crypto });
return { protocolVersion: "2024-11-05", capabilities: {}, serverInfo: { name: "bitatlas-mcp", version: "1.0.0" } };
});
Tool handlers retrieve the session's context by ID. No shared global sodium instance, no cross-session key visibility.
What WASM Sandboxing Does Not Protect Against
WASM linear memory isolation has limits worth being explicit about:
Host callbacks. If the WASM module calls back into the host (for I/O, for example), the host code executes with full access to the host heap. A compromised WASM module that triggers a host callback cannot read host memory directly, but it can pass crafted arguments to host functions and observe their behavior.
Shared ArrayBuffer / SharedArrayBuffer. If you share a buffer between a WASM module and the host—or between two modules—the isolation guarantee on that buffer is gone. Do not pass SharedArrayBuffer objects into untrusted modules.
Side channels. WASM memory isolation does not prevent timing side channels. A module can still leak key bits through observable timing variations in table lookups or conditional branches. Use constant-time implementations (libsodium's primitives are).
Key material copied to the host heap. Once you convert a WASM-backed Uint8Array view to a regular JavaScript Uint8Array, the bytes are in V8's heap and V8's garbage collector decides when they're zeroed—which is never, in practice. Keep sensitive material in WASM memory until it is no longer needed, then call sodium.memzero.
Benchmarks: Overhead of Per-Instance Isolation
A concern with per-agent WASM instances is initialization overhead. Here are measured times on a 2025 developer laptop (M3, Node.js 22):
| Operation | Shared instance | Per-agent instance |
|---|---|---|
crypto_secretbox (256 B) | 0.8 µs | 0.9 µs |
crypto_pwhash (interactive) | 85 ms | 85 ms |
crypto_sign | 12 µs | 13 µs |
| Module instantiation | — | 4 ms |
The per-operation overhead is negligible. Module instantiation at 4 ms is the cost to pay at session start, once. For long-lived agent sessions this is immaterial. For extremely short sessions (sub-100 ms), consider a warm pool.
Putting It Together
The pattern that works in production:
- One WASM crypto instance per agent session, not per operation.
- Key material stays in WASM memory until explicitly needed outside—then it is wiped immediately after use.
- No shared buffers between WASM instances.
- Constant-time primitives (libsodium by default).
- Explicit destroy at session end to flush WASM heap.
WASM is not a silver bullet. It does not replace OS-level sandboxing for untrusted agent code, and it does not protect against side-channel attacks. But for the specific threat of cross-agent key leakage within a shared process, it provides a strong, low-overhead isolation boundary that fits naturally into Node.js and Deno runtimes.
For teams building MCP servers or agent orchestrators that handle encryption on behalf of multiple clients in a single process, WASM crypto sandboxing is worth adding to the standard architecture checklist.