Skip to main content
Beta Feature - Enterprise OnlyWASM plugins are currently in beta and only available in Bifrost Enterprise builds. Contact your account team for access.

Overview

WebAssembly (WASM) plugins offer a powerful alternative to native Go plugins, providing cross-platform compatibility and sandboxed execution. Unlike native .so plugins, WASM plugins:
  • Run anywhere - Single .wasm binary works on any OS/architecture
  • No version matching - No need to match Go versions or dependency versions
  • Sandboxed execution - WASM provides memory-safe, isolated execution
  • Multi-language support - Write plugins in TypeScript, Go, Rust, or any WASM-compatible language

Plugin Interface

All WASM plugins must export these functions:
ExportSignatureDescription
malloc(size: u32) -> u32Allocate memory for host to write data
free(ptr: u32) or (ptr: u32, size: u32)Free allocated memory (Rust requires size for dealloc)
get_name() -> u64Returns packed ptr+len of plugin name
init(config_ptr, config_len: u32) -> i32Initialize with config (0 = success)
http_intercept(input_ptr, input_len: u32) -> u64HTTP transport intercept
pre_hook(input_ptr, input_len: u32) -> u64Pre-request hook
post_hook(input_ptr, input_len: u32) -> u64Post-response hook
cleanup() -> i32Cleanup resources (0 = success)

Return Value Format

Functions returning data use a packed u64 format:
  • Upper 32 bits: pointer to data in WASM memory
  • Lower 32 bits: length of data

Data Exchange

All complex data is exchanged as JSON strings. The host allocates memory using malloc, writes JSON data, and passes pointers to the plugin functions.

Getting Started

Choose your preferred language:

Prerequisites

Install Node.js (v18+) for AssemblyScript compilation:macOS:
brew install node
Linux (Ubuntu/Debian):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

Project Structure

my-wasm-plugin/
├── assembly/
│   ├── index.ts       # Plugin implementation
│   ├── memory.ts      # Memory management utilities
│   ├── types.ts       # Type definitions
│   └── tsconfig.json  # AssemblyScript config
├── package.json
└── Makefile

Step 1: Initialize Project

mkdir my-wasm-plugin && cd my-wasm-plugin
npm init -y
npm install --save-dev assemblyscript json-as
npx asinit .

Step 2: Implement the Plugin

Create assembly/index.ts:
import { JSON } from 'json-as'

// Memory management (simplified)
let heap: ArrayBuffer = new ArrayBuffer(65536)
let heapOffset: u32 = 0

export function malloc(size: u32): u32 {
  const ptr = heapOffset
  heapOffset += size
  return ptr
}

export function free(ptr: u32): void {
  // Simple allocator - no-op for free
}

function readString(ptr: u32, len: u32): string {
  const bytes = new Uint8Array(len)
  for (let i: u32 = 0; i < len; i++) {
    bytes[i] = load<u8>(ptr + i)
  }
  return String.UTF8.decode(bytes.buffer)
}

function writeString(str: string): u64 {
  const encoded = String.UTF8.encode(str)
  const bytes = Uint8Array.wrap(encoded)
  const ptr = malloc(bytes.length)
  for (let i = 0; i < bytes.length; i++) {
    store<u8>(ptr + i, bytes[i])
  }
  // Pack pointer (upper 32 bits) and length (lower 32 bits)
  return (u64(ptr) << 32) | u64(bytes.length)
}

// Plugin configuration
let pluginConfig: string = ''

export function get_name(): u64 {
  return writeString('my-typescript-wasm-plugin')
}

export function init(configPtr: u32, configLen: u32): i32 {
  pluginConfig = readString(configPtr, configLen)
  return 0 // Success
}

export function http_intercept(inputPtr: u32, inputLen: u32): u64 {
  const input = readString(inputPtr, inputLen)
  
  // Parse and modify as needed
  // For pass-through, return the input with has_response: false
  const output = '{"context":{},"request":null,"response":null,"has_response":false,"error":""}'
  
  return writeString(output)
}

export function pre_hook(inputPtr: u32, inputLen: u32): u64 {
  const input = readString(inputPtr, inputLen)
  
  // Parse and modify as needed
  // For pass-through, return with has_short_circuit: false
  const output = '{"context":{},"request":null,"short_circuit":null,"has_short_circuit":false,"error":""}'
  
  return writeString(output)
}

export function post_hook(inputPtr: u32, inputLen: u32): u64 {
  const input = readString(inputPtr, inputLen)
  
  // Parse and modify as needed
  // For pass-through, return with has_error matching input
  const output = '{"context":{},"response":null,"error":null,"has_error":false,"hook_error":""}'
  
  return writeString(output)
}

export function cleanup(): i32 {
  pluginConfig = ''
  return 0 // Success
}

Step 3: Build

Add to package.json:
{
  "scripts": {
    "build": "asc assembly/index.ts -o build/plugin.wasm --runtime stub --optimize"
  }
}
Build:
npm run build
Output: build/plugin.wasm

Hook Input/Output Structures

http_intercept

Input:
{
  "context": {
    "request_id": "abc-123"
  },
  "request": {
    "method": "POST",
    "path": "/v1/chat/completions",
    "headers": { "Content-Type": "application/json" },
    "query": {},
    "body": "<base64-encoded>"
  }
}
Output:
{
  "context": { "request_id": "abc-123", "custom_key": "value" },
  "request": { ... },
  "response": null,
  "has_response": false,
  "error": ""
}
To short-circuit with a response:
{
  "context": { ... },
  "request": null,
  "response": {
    "status_code": 200,
    "headers": { "Content-Type": "application/json" },
    "body": "<base64-encoded>"
  },
  "has_response": true,
  "error": ""
}

pre_hook

Input:
{
  "context": { "request_id": "abc-123" },
  "request": {
    "provider": "openai",
    "model": "gpt-4",
    "input": [{ "role": "user", "content": "Hello" }],
    "params": { "temperature": 0.7 }
  }
}
Output:
{
  "context": { "request_id": "abc-123", "plugin_processed": true },
  "request": { ... },
  "short_circuit": null,
  "has_short_circuit": false,
  "error": ""
}
To short-circuit with a response:
{
  "context": { ... },
  "request": null,
  "short_circuit": {
    "response": {
      "chat_response": {
        "id": "mock-123",
        "model": "gpt-4",
        "choices": [{ "index": 0, "message": { "role": "assistant", "content": "Mock response" } }]
      }
    }
  },
  "has_short_circuit": true,
  "error": ""
}

post_hook

Input:
{
  "context": { "request_id": "abc-123" },
  "response": {
    "chat_response": {
      "id": "chatcmpl-123",
      "model": "gpt-4",
      "choices": [{ "index": 0, "message": { "role": "assistant", "content": "Hello!" } }],
      "usage": { "prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15 }
    }
  },
  "error": {},
  "has_error": false
}
Output:
{
  "context": { "request_id": "abc-123", "post_processed": true },
  "response": { ... },
  "error": {},
  "has_error": false,
  "hook_error": ""
}

Configuration

Configure your WASM plugin in Bifrost’s config.json:
{
  "plugins": [
    {
      "path": "/path/to/plugin.wasm",
      "name": "my-wasm-plugin",
      "enabled": true,
      "config": {
        "custom_option": "value"
      }
    }
  ]
}
You can also load plugins from URLs:
{
  "plugins": [
    {
      "path": "https://example.com/plugins/my-plugin.wasm",
      "name": "my-wasm-plugin",
      "enabled": true
    }
  ]
}

Limitations vs Native Plugins

WASM plugins have some trade-offs compared to native Go plugins:
AspectNative (.so)WASM
PerformanceFastest (in-process)JSON serialization overhead
Cross-platformBuild per platformSingle binary everywhere
Version matchingExact Go/package match requiredNo version requirements
MemoryShared process memoryLinear memory (limited)
LanguagesGo onlyTypeScript, Go, Rust, etc.
DebuggingFull Go toolingLimited debugging support
SecurityFull process accessSandboxed execution

Source Code Reference

Complete hello-world examples are available in the Bifrost repository:

Troubleshooting

Module fails to load

Error: failed to instantiate WASM module Solution: Ensure all required exports are present. Use a WASM inspection tool:
# List exports
wasm-objdump -x plugin.wasm | grep -A 20 "Export"

Memory allocation errors

Error: out of memory or invalid memory access Solution:
  • Increase heap size in your allocator
  • Ensure you’re freeing memory after use
  • Check for memory leaks in long-running plugins

JSON parsing errors

Error: failed to parse input JSON Solution:
  • Validate your JSON structures match expected schemas
  • Handle optional/nullable fields properly
  • Add error logging to identify malformed data

Build errors (TinyGo)

Error: package not supported by TinyGo Solution: TinyGo doesn’t support all Go standard library packages. Avoid:
  • reflect (limited support)
  • net/http (use raw JSON instead)
  • Complex generics

Build errors (Rust)

Error: cannot find -lc Solution: For wasm32-unknown-unknown target, don’t link to libc. Ensure your Cargo.toml doesn’t require native dependencies.

Need Help?