Cogitator
Tools

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.

FactoryTool nameParametersDescription
createCalcTool()calculateexpressionEvaluate math expressions (+, -, *, /, %, parens)
createJsonTool()process_jsonjson, query? (JSONPath)Parse JSON and query with JSONPath
createHashTool()hash_texttext, algorithm (sha256 | sha1 | md5)Cryptographic hashes
createBase64Tool()base64text, operation (encode | decode), urlSafe?Encode/decode Base64 (standard and URL-safe)
createSlugTool()slugtext, separator? (-), lowercase? (true), maxLength?Convert text to URL-safe slugs with transliteration
createValidationTool()validatevalue, type (email | url | uuid | ipv4 | ipv6)Validate common formats
createDiffTool()difforiginal, modified, format? (unified | inline | json), context? (3)Generate diffs between texts (Myers algorithm)
createRegexTool()regextext, pattern, operation (match | matchAll | test | replace | split), flags?, replacement?, limit?Regex operations with ReDoS protection
createCsvTool()csvdata (string or rows), operation (parse | stringify), delimiter?, quote?, headers?Parse/generate CSV data (RFC 4180)
createMarkdownTool()markdownmarkdown, options?: { sanitize?, gfm? }Convert Markdown to HTML (GFM subset)
createXmlTool()xmlxml, query? (XPath-like)Parse XML to JSON with XPath-like queries
createDatetimeTool()datetimeoperation (parse | format | add | subtract | diff | now), date?, format?, timezone?, amount?, unit?, endDate?Parse, format, manipulate dates (UTC + offsets)
createCompressionTool()compressiondata, operation (compress | decompress), inputEncoding?, outputEncoding?, level? (0-9, 6)Gzip compress/decompress data
createSigningTool()signingoperation (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 return found: false.
  • xml: /root/child (absolute from the root element), //name, *, name[2], /@attr, /text(). A query without matches returns type: 'empty'. Malformed XML is rejected; DTD entities are never expanded.
  • diff: unified (default) is a standard unified diff with ---/+++ headers and @@ hunks; inline and json are 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: sanitize defaults to true -- raw HTML is escaped and javascript:/data:-style URLs become #.
  • compression: gzip interoperable with zlib; decompress defaults 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 with node: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

FieldTypeDescription
namestringTool name for LLM function calling
descriptionstringWhat the tool does (shown to the LLM)
wasmModulestringPath or URL to the .wasm file
wasmFunctionstringExported function to call (default: run)
parametersZodTypeZod schema for input validation
categoryToolCategoryOptional category for grouping
tagsstring[]Optional tags for discovery
timeoutnumberExecution timeout in ms (default: 5000)
wasibooleanEnable 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(),
});
OptionDefaultDescription
debounceMs100Debounce for file change events while watching
useWasifalseInstantiate plugins with WASI
timeout30000Per-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:

  1. Loading: The .wasm binary 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
  2. Execution: Validated parameters are JSON-serialized and passed to the exported WASM function via plugin.call(functionName, input)
  3. Output: The WASM function returns a byte buffer, which is parsed as JSON (or returned as raw text if parsing fails)
  4. 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

AspectWASM (Extism)Docker
Cold start< 5 ms500 ms - 2 s
Memory overhead~2 MB per instance~50 MB per container
IsolationLinear memoryFull OS-level
Network accessNoneConfigurable
File systemNoneMountable volumes
Best forPure computationI/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.

On this page