WASM Tools
Run tools in isolated WebAssembly sandboxes with Extism for security, speed, and portability.
Overview
The @cogitator-ai/wasm-tools package provides tools that execute inside WebAssembly sandboxes powered by Extism. Compared to Docker-based sandboxing, WASM tools offer:
- 100-500x faster cold start than Docker containers
- ~20x lower memory footprint per tool instance
- Memory-safe execution in an isolated linear memory space
- No network or filesystem access -- no hosts are allowed and no directories are pre-opened
import { Agent } from '@cogitator-ai/core';
import { createCalcTool, createHashTool, createCsvTool } from '@cogitator-ai/wasm-tools';
const agent = new Agent({
name: 'data-processor',
model: 'openai/gpt-6.1-sol',
instructions: 'You process and verify data.',
tools: [createCalcTool(), createHashTool(), createCsvTool()],
});Pre-built WASM Tools
The package ships with 14 ready-to-use WASM tool factories. Each returns a Tool instance backed by a compiled .wasm module.
| Factory | Tool name | Parameters | Description |
|---|---|---|---|
createCalcTool() | calculate | expression | Evaluate math expressions (+, -, *, /, %, parens) |
createJsonTool() | process_json | json, query? (JSONPath) | Parse JSON and query with JSONPath |
createHashTool() | hash_text | text, algorithm (sha256 | sha1 | md5) | Cryptographic hashes |
createBase64Tool() | base64 | text, operation (encode | decode), urlSafe? | Encode/decode Base64 (standard and URL-safe) |
createSlugTool() | slug | text, separator? (-), lowercase? (true), maxLength? | Convert text to URL-safe slugs with transliteration |
createValidationTool() | validate | value, type (email | url | uuid | ipv4 | ipv6) | Validate common formats |
createDiffTool() | diff | original, modified, format? (unified | inline | json), context? (3) | Generate diffs between texts (Myers algorithm) |
createRegexTool() | regex | text, pattern, operation (match | matchAll | test | replace | split), flags?, replacement?, limit? | Regex operations with ReDoS protection |
createCsvTool() | csv | data (string or rows), operation (parse | stringify), delimiter?, quote?, headers? | Parse/generate CSV data (RFC 4180) |
createMarkdownTool() | markdown | markdown, options?: { sanitize?, gfm? } | Convert Markdown to HTML (GFM subset) |
createXmlTool() | xml | xml, query? (XPath-like) | Parse XML to JSON with XPath-like queries |
createDatetimeTool() | datetime | operation (parse | format | add | subtract | diff | now), date?, format?, timezone?, amount?, unit?, endDate? | Parse, format, manipulate dates (UTC + offsets) |
createCompressionTool() | compression | data, operation (compress | decompress), inputEncoding?, outputEncoding?, level? (0-9, 6) | Gzip compress/decompress data |
createSigningTool() | signing | operation (generateKeypair | sign | verify), algorithm (ed25519), message?, privateKey?, publicKey?, signature?, encoding?, seed? | Ed25519 keypair generation, sign, and verify |
Every factory accepts an optional { timeout } in milliseconds — 5000 by default, 10000 for compression and signing:
const calc = createCalcTool({ timeout: 10000 });The Zod schemas are exported too (calcToolSchema, jsonToolSchema, …, signingToolSchema, with matching CalcToolInput-style types), as is getWasmPath(name), the absolute path of a bundled module (getWasmPath('calc')). With @cogitator-ai/sandbox the package path @cogitator-ai/wasm-tools/wasm/calc.wasm works as well: the executor resolves it from your application. calcToolConfig, jsonToolConfig, hashToolConfig and base64ToolConfig are ready-made SandboxConfigs for those four modules.
Executing Tools
Inside Cogitator, tools run through the @cogitator-ai/sandbox WASM executor; only when that package is not installed or cannot start does the runtime call the tool's own execute(). A WASM call never falls back to Docker or the host. You can also call them directly -- parameters are validated with the Zod schema, the module runs in an Extism worker thread, the timeout terminates the worker, and context.signal aborts the call:
const result = await createCalcTool().execute(
{ expression: '(2 + 3) * 4' },
{ agentId: 'a', runId: 'r', signal: AbortSignal.timeout(5000) }
);
// { result: 20, expression: '(2 + 3) * 4' }Plugin-level errors come back as data ({ ..., error }) so the agent can recover. A few behaviours worth knowing:
- json: JSONPath subset --
.key,['key'],[n](negative allowed), slices,*,..key. Multi-value paths return arrays; missing paths returnfound: false. - xml:
/root/child(absolute from the root element),//name,*,name[2],/@attr,/text(). A query without matches returnstype: 'empty'. Malformed XML is rejected; DTD entities are never expanded. - diff:
unified(default) is a standard unified diff with---/+++headers and@@hunks;inlineandjsonare also available. - validation: IPv4 with leading zeros is rejected, IPv6 is normalized to the RFC 5952 canonical form, and only the email domain is lower-cased.
- markdown:
sanitizedefaults totrue-- raw HTML is escaped andjavascript:/data:-style URLs become#. - compression: gzip interoperable with zlib;
decompressdefaults to base64 in / UTF-8 out and verifies CRC32. - signing: WASM has no secure RNG, so a 32-byte seed is generated on the host for
generateKeypair(the seed is the private key). Signatures interoperate withnode:crypto. - datetime: month arithmetic clamps to month end (
Jan 31 + 1 month = Feb 29), impossible dates are rejected. - regex: nested unbounded quantifiers are rejected; any other catastrophic pattern is stopped by the timeout.
Defining Custom WASM Tools
Use defineWasmTool() to wrap any .wasm module as a Cogitator tool:
import { defineWasmTool } from '@cogitator-ai/wasm-tools';
import { z } from 'zod';
const imageProcessor = defineWasmTool({
name: 'process_image',
description: 'Resize and crop images in a WASM sandbox',
wasmModule: './plugins/image-proc.wasm',
wasmFunction: 'process',
parameters: z.object({
imageData: z.string().describe('Base64-encoded image'),
operation: z.enum(['resize', 'crop', 'rotate']),
width: z.number().optional(),
height: z.number().optional(),
}),
category: 'utility',
tags: ['image', 'processing'],
timeout: 10000,
wasi: true,
});Modules compiled with extism-js (like the pre-built ones) embed QuickJS and need wasi: true. Extism >= 2.0.0-rc13 is required for their host functions.
WasmToolConfig
| Field | Type | Description |
|---|---|---|
name | string | Tool name for LLM function calling |
description | string | What the tool does (shown to the LLM) |
wasmModule | string | Path or URL to the .wasm file |
wasmFunction | string | Exported function to call (default: run) |
parameters | ZodType | Zod schema for input validation |
category | ToolCategory | Optional category for grouping |
tags | string[] | Optional tags for discovery |
timeout | number | Execution timeout in ms (default: 5000) |
wasi | boolean | Enable WASI (needed for extism-js modules) |
WasmToolManager
For dynamic loading, hot-reloading, and managing multiple WASM modules at runtime, use WasmToolManager:
import { WasmToolManager } from '@cogitator-ai/wasm-tools';
const manager = new WasmToolManager({ debounceMs: 100, useWasi: true, timeout: 30000 });
const tool = await manager.load('./plugins/calc.wasm');
const agent = new Agent({
name: 'dynamic-agent',
model: 'openai/gpt-6.1-sol',
instructions: 'Use the loaded plugins.',
tools: manager.getTools(),
});| Option | Default | Description |
|---|---|---|
debounceMs | 100 | Debounce for file change events while watching |
useWasi | false | Instantiate plugins with WASI |
timeout | 30000 | Per-call timeout in ms |
A manager tool is named after the file (calc.wasm → calc), described as WASM tool: calc, and takes a free-form object: whatever the model passes is JSON-serialized and handed to the module's run export, and the output is parsed as JSON when possible. Loading a second file with the same base name replaces the first. Use defineWasmTool() when the model needs a real schema.
File Watching
The manager can watch a glob pattern for .wasm files and automatically load, reload, or unload modules as they change on disk:
await manager.watch('./plugins/*.wasm', {
onLoad: (name, path) => console.log(`Loaded: ${name}`),
onReload: (name, path) => console.log(`Reloaded: ${name}`),
onUnload: (name, path) => console.log(`Unloaded: ${name}`),
onError: (name, path, error) => console.error(`Error: ${name}`, error),
});When a .wasm file is modified, the manager closes the old plugin instance and loads the new one; deleting the file unloads it. Agents keep the same tool object, so a reload is picked up on the next call. A manager watches one pattern at a time — watch() throws if it is already watching. WasmLoader and FileWatcher, the building blocks of the manager, are exported as well.
Plugins run in worker threads: when a call exceeds timeout (default 30 s) or its context.signal aborts, the call rejects and the plugin is replaced with a fresh instance.
Manager API
const tool = await manager.load('./path/to/module.wasm');
const allTools = manager.getTools();
const specific = manager.getTool('calc');
const mod = manager.getModule('calc');
// { name, path, plugin, tool, loadedAt }
const modules = manager.getModules();
await manager.close();How WASM Sandboxing Works
Each WASM tool runs inside an Extism plugin -- a lightweight WebAssembly runtime with strict isolation:
- Loading: The
.wasmbinary is read from disk (compiled once and cached until the file changes) or fetched from a URL, and instantiated via the Extism SDK in a worker thread - Execution: Validated parameters are JSON-serialized and passed to the exported WASM function via
plugin.call(functionName, input) - Output: The WASM function returns a byte buffer, which is parsed as JSON (or returned as raw text if parsing fails)
- Isolation: Each plugin has its own linear memory space -- it cannot access host memory, and with no pre-opened directories or allowed hosts it has no file system or network access even with WASI enabled
┌─────────────────────────────────────┐
│ Host Process (Node.js) │
│ │
│ params ──► JSON.stringify ──────┐ │
│ │ │
│ ┌───────────────────────────────▼─┐ │
│ │ Extism WASM Sandbox │ │
│ │ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Own linear memory │ │ │
│ │ │ No host access │ │ │
│ │ │ No network │ │ │
│ │ │ No filesystem │ │ │
│ │ └─────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────┬─┘ │
│ │ │
│ result ◄── JSON.parse ◄────────┘ │
└─────────────────────────────────────┘Security Benefits
- No access to the host process -- the module only sees the JSON input it is given
- Memory bounds checking -- buffer overflows in WASM trap instead of corrupting host memory
- No ambient authority -- no file system, network, or secure randomness inside the sandbox (randomness, when needed, is supplied by the host)
- Timeout enforcement -- the worker thread running a long computation is terminated after the configured timeout
- ReDoS containment -- catastrophic regexes run in the worker, never on the host event loop; the timeout kills them
WASM vs Docker Sandboxing
| Aspect | WASM (Extism) | Docker |
|---|---|---|
| Cold start | < 5 ms | 500 ms - 2 s |
| Memory overhead | ~2 MB per instance | ~50 MB per container |
| Isolation | Linear memory | Full OS-level |
| Network access | None | Configurable |
| File system | None | Mountable volumes |
| Best for | Pure computation | I/O-heavy, multi-process |
Use WASM tools when you need fast, lightweight sandboxing for pure computation (hashing, parsing, math, text processing). Use Docker sandboxing when tools need file system access, network calls, or a full OS environment.