Writing WASM Plugins
Overview
Section titled “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
.wasmbinary 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
Section titled “Plugin Interface”All WASM plugins must export these functions:
| Export | Signature | Description |
|---|---|---|
malloc | (size: u32) -> u32 | Allocate memory for host to write data |
free | (ptr: u32) or (ptr: u32, size: u32) | Free allocated memory (Rust requires size for dealloc) |
get_name | () -> u64 | Returns packed ptr+len of plugin name |
init | (config_ptr, config_len: u32) -> i32 | Initialize with config (0 = success) |
http_pre_hook | (input_ptr, input_len: u32) -> u64 | HTTP transport pre-hook (request interception) |
http_post_hook | (input_ptr, input_len: u32) -> u64 | HTTP transport post-hook (non-streaming response interception) |
http_stream_chunk_hook | (input_ptr, input_len: u32) -> u64 | HTTP streaming chunk hook (per-chunk interception for streaming responses) |
pre_hook | (input_ptr, input_len: u32) -> u64 | Pre-request hook |
post_hook | (input_ptr, input_len: u32) -> u64 | Post-response hook |
cleanup | () -> i32 | Cleanup resources (0 = success) |
Return Value Format
Section titled “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
Section titled “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
Section titled “Getting Started”Choose your preferred language:
Prerequisites
Section titled “Prerequisites”Install Node.js (v18+) for AssemblyScript compilation:
macOS:
brew install nodeLinux (Ubuntu/Debian):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -sudo apt install -y nodejsProject Structure
Section titled “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└── MakefileStep 1: Initialize Project
Section titled “Step 1: Initialize Project”mkdir my-wasm-plugin && cd my-wasm-pluginnpm init -ynpm install --save-dev assemblyscript json-asnpx asinit .Step 2: Implement the Plugin
Section titled “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 configurationlet 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_pre_hook(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 http_post_hook(inputPtr: u32, inputLen: u32): u64 { const input = readString(inputPtr, inputLen)
// Parse input which includes both request and response // For pass-through, just return context and empty error const output = '{"context":{},"error":""}'
return writeString(output)}
// Input structure for http_stream_chunk_hook@jsonclass StreamChunkInput { context: JSON.Obj = new JSON.Obj() request: JSON.Raw = new JSON.Raw('null') chunk: JSON.Raw = new JSON.Raw('null') // DeepIntShieldStreamChunk as JSON (see below)}
// Output structure for http_stream_chunk_hook@jsonclass StreamChunkOutput { context: JSON.Obj = new JSON.Obj() chunk: JSON.Raw = new JSON.Raw('null') // DeepIntShieldStreamChunk as JSON, or null to skip has_chunk: bool = false skip: bool = false error: string = ''}
// DeepIntShieldStreamChunk is one of: DeepIntShieldChatResponse, DeepIntShieldTextCompletionResponse,// DeepIntShieldResponsesStreamResponse, DeepIntShieldSpeechStreamResponse, DeepIntShieldTranscriptionStreamResponse,// DeepIntShieldImageGenerationStreamResponse, or DeepIntShieldError.// For chat completions, the chunk JSON looks like:// {// "id": "chatcmpl-xxx",// "object": "chat.completion.chunk",// "created": 1234567890,// "model": "gpt-4",// "choices": [{"index": 0, "delta": {"content": "Hello"}, "finish_reason": null}],// ...// }
export function http_stream_chunk_hook(inputPtr: u32, inputLen: u32): u64 { const inputJson = readString(inputPtr, inputLen) const input = JSON.parse<StreamChunkInput>(inputJson)
// For pass-through, return chunk unchanged with skip: false // To skip a chunk, set skip: true and chunk: null const output = new StreamChunkOutput() output.context = input.context output.chunk = input.chunk output.has_chunk = true output.skip = false output.error = ''
return writeString(JSON.stringify(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
Section titled “Step 3: Build”Add to package.json:
{ "scripts": { "build": "asc assembly/index.ts -o build/plugin.wasm --runtime stub --optimize" }}Build:
npm run buildOutput: build/plugin.wasm
Prerequisites
Section titled “Prerequisites”Install TinyGo for WASM compilation:
macOS:
brew install tinygoLinux (Ubuntu/Debian):
wget https://github.com/tinygo-org/tinygo/releases/download/v0.32.0/tinygo_0.32.0_amd64.debsudo dpkg -i tinygo_0.32.0_amd64.debProject Structure
Section titled “Project Structure”my-wasm-plugin/├── main.go # Plugin implementation├── memory.go # Memory management utilities├── types.go # Type definitions├── go.mod└── MakefileStep 1: Initialize Project
Section titled “Step 1: Initialize Project”mkdir my-wasm-plugin && cd my-wasm-plugingo mod init github.com/yourusername/my-wasm-pluginStep 2: Implement Memory Management
Section titled “Step 2: Implement Memory Management”Create memory.go:
package main
import "unsafe"
var heap = make([]byte, 1024*1024) // 1MB heapvar heapOffset uint32 = 0
//export plugin_mallocfunc plugin_malloc(size uint32) uint32 { ptr := heapOffset heapOffset += size return ptr}
//export plugin_freefunc plugin_free(ptr uint32) { // Simple allocator - no-op}
func readInput(ptr, length uint32) []byte { if length == 0 { return nil } data := make([]byte, length) for i := uint32(0); i < length; i++ { data[i] = *(*byte)(unsafe.Pointer(uintptr(ptr + i))) } return data}
func writeBytes(data []byte) uint64 { ptr := plugin_malloc(uint32(len(data))) for i, b := range data { *(*byte)(unsafe.Pointer(uintptr(ptr + uint32(i)))) = b } // Pack pointer (upper 32 bits) and length (lower 32 bits) return (uint64(ptr) << 32) | uint64(len(data))}Step 3: Implement the Plugin
Section titled “Step 3: Implement the Plugin”Create main.go:
package main
import ( "encoding/json")
//export get_namefunc get_name() uint64 { return writeBytes([]byte("my-go-wasm-plugin"))}
//export initfunc init_plugin(configPtr, configLen uint32) int32 { if configLen > 0 { configData := readInput(configPtr, configLen) // Parse and store config as needed _ = configData } return 0 // Success}
// HTTPPreHookInput represents the input to http_pre_hooktype HTTPPreHookInput struct { Context map[string]interface{} `json:"context"` Request json.RawMessage `json:"request"`}
// HTTPPreHookOutput represents the output from http_pre_hooktype HTTPPreHookOutput struct { Context map[string]interface{} `json:"context"` Request json.RawMessage `json:"request,omitempty"` Response json.RawMessage `json:"response,omitempty"` HasResponse bool `json:"has_response"` Error string `json:"error"`}
//export http_pre_hookfunc http_pre_hook(inputPtr, inputLen uint32) uint64 { inputData := readInput(inputPtr, inputLen)
var input HTTPPreHookInput if err := json.Unmarshal(inputData, &input); err != nil { output := HTTPPreHookOutput{Error: err.Error()} data, _ := json.Marshal(output) return writeBytes(data) }
// Add custom context value input.Context["from-http-pre"] = "wasm-plugin"
// Pass through output := HTTPPreHookOutput{ Context: input.Context, Request: input.Request, HasResponse: false, }
data, _ := json.Marshal(output) return writeBytes(data)}
// HTTPPostHookInput represents the input to http_post_hooktype HTTPPostHookInput struct { Context map[string]interface{} `json:"context"` Request json.RawMessage `json:"request"` Response json.RawMessage `json:"response"`}
// HTTPPostHookOutput represents the output from http_post_hooktype HTTPPostHookOutput struct { Context map[string]interface{} `json:"context"` Error string `json:"error"`}
//export http_post_hookfunc http_post_hook(inputPtr, inputLen uint32) uint64 { inputData := readInput(inputPtr, inputLen)
var input HTTPPostHookInput if err := json.Unmarshal(inputData, &input); err != nil { output := HTTPPostHookOutput{Error: err.Error()} data, _ := json.Marshal(output) return writeBytes(data) }
// Add custom context value input.Context["from-http-post"] = "wasm-plugin"
// Pass through output := HTTPPostHookOutput{ Context: input.Context, }
data, _ := json.Marshal(output) return writeBytes(data)}
// HTTPStreamChunkHookInput represents the input to http_stream_chunk_hooktype HTTPStreamChunkHookInput struct { Context map[string]interface{} `json:"context"` Request json.RawMessage `json:"request"` Chunk json.RawMessage `json:"chunk"` // DeepIntShieldStreamChunk JSON}
// HTTPStreamChunkHookOutput represents the output from http_stream_chunk_hooktype HTTPStreamChunkHookOutput struct { Context map[string]interface{} `json:"context"` Chunk json.RawMessage `json:"chunk,omitempty"` // DeepIntShieldStreamChunk JSON, nil to skip HasChunk bool `json:"has_chunk"` Skip bool `json:"skip"` Error string `json:"error"`}
//export http_stream_chunk_hookfunc http_stream_chunk_hook(inputPtr, inputLen uint32) uint64 { inputData := readInput(inputPtr, inputLen)
var input HTTPStreamChunkHookInput if err := json.Unmarshal(inputData, &input); err != nil { output := HTTPStreamChunkHookOutput{Error: err.Error()} data, _ := json.Marshal(output) return writeBytes(data) }
// Pass through chunk unchanged output := HTTPStreamChunkHookOutput{ Context: input.Context, Chunk: input.Chunk, HasChunk: true, Skip: false, }
data, _ := json.Marshal(output) return writeBytes(data)}
// PreHookInput represents the input to pre_hooktype PreHookInput struct { Context map[string]interface{} `json:"context"` Request json.RawMessage `json:"request"`}
// PreHookOutput represents the output from pre_hooktype PreHookOutput struct { Context map[string]interface{} `json:"context"` Request json.RawMessage `json:"request,omitempty"` ShortCircuit json.RawMessage `json:"short_circuit,omitempty"` HasShortCircuit bool `json:"has_short_circuit"` Error string `json:"error"`}
//export pre_hookfunc pre_hook(inputPtr, inputLen uint32) uint64 { inputData := readInput(inputPtr, inputLen)
var input PreHookInput if err := json.Unmarshal(inputData, &input); err != nil { output := PreHookOutput{Error: err.Error()} data, _ := json.Marshal(output) return writeBytes(data) }
// Add custom context value input.Context["from-pre-hook"] = "wasm-plugin"
// Pass through output := PreHookOutput{ Context: input.Context, Request: input.Request, HasShortCircuit: false, }
data, _ := json.Marshal(output) return writeBytes(data)}
// PostHookInput represents the input to post_hooktype PostHookInput struct { Context map[string]interface{} `json:"context"` Response json.RawMessage `json:"response"` Error json.RawMessage `json:"error"` HasError bool `json:"has_error"`}
// PostHookOutput represents the output from post_hooktype PostHookOutput struct { Context map[string]interface{} `json:"context"` Response json.RawMessage `json:"response,omitempty"` Error json.RawMessage `json:"error,omitempty"` HasError bool `json:"has_error"` HookError string `json:"hook_error"`}
//export post_hookfunc post_hook(inputPtr, inputLen uint32) uint64 { inputData := readInput(inputPtr, inputLen)
var input PostHookInput if err := json.Unmarshal(inputData, &input); err != nil { output := PostHookOutput{HookError: err.Error()} data, _ := json.Marshal(output) return writeBytes(data) }
// Add custom context value input.Context["from-post-hook"] = "wasm-plugin"
// Pass through output := PostHookOutput{ Context: input.Context, Response: input.Response, Error: input.Error, HasError: input.HasError, }
data, _ := json.Marshal(output) return writeBytes(data)}
//export cleanupfunc cleanup() int32 { return 0 // Success}
func main() {}Step 4: Build
Section titled “Step 4: Build”tinygo build -o build/plugin.wasm -target=wasi -scheduler=none .Or create a Makefile:
build: @mkdir -p build GOWORK=off tinygo build -o build/plugin.wasm -target=wasi -scheduler=none .
clean: @rm -rf buildOutput: build/plugin.wasm
Prerequisites
Section titled “Prerequisites”Install Rust and add the WASM target:
# Install Rust (if not already installed)curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add WASM targetrustup target add wasm32-unknown-unknownOptional - Install wasm-opt for smaller binaries:
# macOSbrew install binaryen
# Linuxapt install binaryenProject Structure
Section titled “Project Structure”my-wasm-plugin/├── src/│ ├── lib.rs # Plugin implementation│ ├── memory.rs # Memory management│ └── types.rs # Type definitions├── Cargo.toml└── MakefileStep 1: Initialize Project
Section titled “Step 1: Initialize Project”cargo new --lib my-wasm-plugincd my-wasm-pluginUpdate Cargo.toml:
[package]name = "my-wasm-plugin"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib"]
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"
[profile.release]opt-level = "s"lto = trueStep 2: Implement Memory Management
Section titled “Step 2: Implement Memory Management”Create src/memory.rs:
use std::alloc::{alloc, dealloc, Layout};
#[no_mangle]pub extern "C" fn malloc(size: u32) -> u32 { let layout = Layout::from_size_align(size as usize, 1).unwrap(); unsafe { alloc(layout) as u32 }}
#[no_mangle]pub extern "C" fn free(ptr: u32, size: u32) { let layout = Layout::from_size_align(size as usize, 1).unwrap(); unsafe { dealloc(ptr as *mut u8, layout) }}
pub fn read_string(ptr: u32, len: u32) -> String { let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) }; String::from_utf8_lossy(slice).to_string()}
pub fn write_string(s: &str) -> u64 { let bytes = s.as_bytes(); let ptr = malloc(bytes.len() as u32); unsafe { std::ptr::copy_nonoverlapping( bytes.as_ptr(), ptr as *mut u8, bytes.len() ); } // Pack pointer (upper 32 bits) and length (lower 32 bits) ((ptr as u64) << 32) | (bytes.len() as u64)}Step 3: Implement the Plugin
Section titled “Step 3: Implement the Plugin”Create src/lib.rs:
mod memory;
use memory::{read_string, write_string};use serde::{Deserialize, Serialize};use std::collections::HashMap;
// Plugin configuration storagestatic mut CONFIG: Option<String> = None;
#[no_mangle]pub extern "C" fn get_name() -> u64 { write_string("my-rust-wasm-plugin")}
#[no_mangle]pub extern "C" fn init(config_ptr: u32, config_len: u32) -> i32 { let config = read_string(config_ptr, config_len); unsafe { CONFIG = Some(config); } 0 // Success}
#[derive(Deserialize)]struct HTTPPreHookInput { context: HashMap<String, serde_json::Value>, request: serde_json::Value,}
#[derive(Serialize, Default)]struct HTTPPreHookOutput { context: HashMap<String, serde_json::Value>, #[serde(skip_serializing_if = "Option::is_none")] request: Option<serde_json::Value>, #[serde(skip_serializing_if = "Option::is_none")] response: Option<serde_json::Value>, has_response: bool, error: String,}
#[no_mangle]pub extern "C" fn http_pre_hook(input_ptr: u32, input_len: u32) -> u64 { let input_str = read_string(input_ptr, input_len);
let input: HTTPPreHookInput = match serde_json::from_str(&input_str) { Ok(i) => i, Err(e) => { let output = HTTPPreHookOutput { error: format!("Parse error: {}", e), ..Default::default() }; return write_string(&serde_json::to_string(&output).unwrap()); } };
let mut context = input.context; context.insert("from-http-pre".to_string(), serde_json::json!("rust-wasm"));
let output = HTTPPreHookOutput { context, request: Some(input.request), has_response: false, ..Default::default() };
write_string(&serde_json::to_string(&output).unwrap())}
#[derive(Deserialize)]struct HTTPPostHookInput { context: HashMap<String, serde_json::Value>, request: serde_json::Value, response: serde_json::Value,}
#[derive(Serialize, Default)]struct HTTPPostHookOutput { context: HashMap<String, serde_json::Value>, error: String,}
#[no_mangle]pub extern "C" fn http_post_hook(input_ptr: u32, input_len: u32) -> u64 { let input_str = read_string(input_ptr, input_len);
let input: HTTPPostHookInput = match serde_json::from_str(&input_str) { Ok(i) => i, Err(e) => { let output = HTTPPostHookOutput { error: format!("Parse error: {}", e), ..Default::default() }; return write_string(&serde_json::to_string(&output).unwrap()); } };
let mut context = input.context; context.insert("from-http-post".to_string(), serde_json::json!("rust-wasm"));
let output = HTTPPostHookOutput { context, error: String::new(), };
write_string(&serde_json::to_string(&output).unwrap())}
#[derive(Deserialize)]struct HTTPStreamChunkHookInput { context: HashMap<String, serde_json::Value>, request: serde_json::Value, chunk: String, // base64-encoded chunk}
#[derive(Serialize, Default)]struct HTTPStreamChunkHookOutput { context: HashMap<String, serde_json::Value>, #[serde(skip_serializing_if = "Option::is_none")] chunk: Option<String>, // base64-encoded chunk, None to skip has_chunk: bool, skip: bool, error: String,}
#[no_mangle]pub extern "C" fn http_stream_chunk_hook(input_ptr: u32, input_len: u32) -> u64 { let input_str = read_string(input_ptr, input_len);
let input: HTTPStreamChunkHookInput = match serde_json::from_str(&input_str) { Ok(i) => i, Err(e) => { let output = HTTPStreamChunkHookOutput { error: format!("Parse error: {}", e), ..Default::default() }; return write_string(&serde_json::to_string(&output).unwrap()); } };
// Pass through chunk unchanged let output = HTTPStreamChunkHookOutput { context: input.context, chunk: Some(input.chunk), has_chunk: true, skip: false, error: String::new(), };
write_string(&serde_json::to_string(&output).unwrap())}
#[derive(Deserialize)]struct PreHookInput { context: HashMap<String, serde_json::Value>, request: serde_json::Value,}
#[derive(Serialize, Default)]struct PreHookOutput { context: HashMap<String, serde_json::Value>, #[serde(skip_serializing_if = "Option::is_none")] request: Option<serde_json::Value>, #[serde(skip_serializing_if = "Option::is_none")] short_circuit: Option<serde_json::Value>, has_short_circuit: bool, error: String,}
#[no_mangle]pub extern "C" fn pre_hook(input_ptr: u32, input_len: u32) -> u64 { let input_str = read_string(input_ptr, input_len);
let input: PreHookInput = match serde_json::from_str(&input_str) { Ok(i) => i, Err(e) => { let output = PreHookOutput { error: format!("Parse error: {}", e), ..Default::default() }; return write_string(&serde_json::to_string(&output).unwrap()); } };
let mut context = input.context; context.insert("from-pre-hook".to_string(), serde_json::json!("rust-wasm"));
let output = PreHookOutput { context, request: Some(input.request), has_short_circuit: false, ..Default::default() };
write_string(&serde_json::to_string(&output).unwrap())}
#[derive(Deserialize)]struct PostHookInput { context: HashMap<String, serde_json::Value>, response: serde_json::Value, error: serde_json::Value, has_error: bool,}
#[derive(Serialize, Default)]struct PostHookOutput { context: HashMap<String, serde_json::Value>, #[serde(skip_serializing_if = "Option::is_none")] response: Option<serde_json::Value>, #[serde(skip_serializing_if = "Option::is_none")] error: Option<serde_json::Value>, has_error: bool, hook_error: String,}
#[no_mangle]pub extern "C" fn post_hook(input_ptr: u32, input_len: u32) -> u64 { let input_str = read_string(input_ptr, input_len);
let input: PostHookInput = match serde_json::from_str(&input_str) { Ok(i) => i, Err(e) => { let output = PostHookOutput { hook_error: format!("Parse error: {}", e), ..Default::default() }; return write_string(&serde_json::to_string(&output).unwrap()); } };
let mut context = input.context; context.insert("from-post-hook".to_string(), serde_json::json!("rust-wasm"));
let output = PostHookOutput { context, response: Some(input.response), error: Some(input.error), has_error: input.has_error, hook_error: String::new(), };
write_string(&serde_json::to_string(&output).unwrap())}
#[no_mangle]pub extern "C" fn cleanup() -> i32 { unsafe { CONFIG = None; } 0 // Success}Step 4: Build
Section titled “Step 4: Build”cargo build --release --target wasm32-unknown-unknowncp target/wasm32-unknown-unknown/release/my_wasm_plugin.wasm build/plugin.wasmOptional - Optimize with wasm-opt:
wasm-opt -Os -o build/plugin.wasm build/plugin.wasmOutput: build/plugin.wasm
Hook Input/Output Structures
Section titled “Hook Input/Output Structures”http_pre_hook
Section titled “http_pre_hook”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": ""}http_post_hook
Section titled “http_post_hook”Called after the response is received from the LLM provider. Receives both the original request and the response.
Input:
{ "context": { "request_id": "abc-123", "custom_key": "value" }, "request": { "method": "POST", "path": "/v1/chat/completions", "headers": { "content-type": "application/json" }, "query": {}, "body": "<base64-encoded>" }, "response": { "status_code": 200, "headers": { "content-type": "application/json" }, "body": "<base64-encoded>" }}Output:
{ "context": { "request_id": "abc-123", "custom_key": "value", "post_processed": true }, "error": ""}http_stream_chunk_hook
Section titled “http_stream_chunk_hook”Called for each chunk during streaming responses, BEFORE the chunk is written to the client. This hook allows plugins to modify or filter streaming chunks in real-time.
Input:
{ "context": { "request_id": "abc-123", "custom_key": "value" }, "request": { "method": "POST", "path": "/v1/chat/completions", "headers": { "content-type": "application/json" }, "query": {}, "body": "<base64-encoded>" }, "chunk": { "id": "chatcmpl-xxx", "object": "chat.completion.chunk", "created": 1234567890, "model": "gpt-4", "choices": [{"index": 0, "delta": {"content": "Hello"}, "finish_reason": null}] }}The chunk field contains a DeepIntShieldStreamChunk struct serialized as JSON. It will contain the data from whichever response type is active:
- Chat completion streaming:
{"id":"...","object":"chat.completion.chunk","choices":[...],"model":"..."} - Text completion streaming:
{"id":"...","choices":[...]} - Responses API streaming:
{"type":"...","item":...} - Speech/Transcription/Image streaming: respective response fields
- Error:
{"error":{"type":"...","message":"..."}}
It does NOT include SSE framing (no data: prefix or \n\n suffix).
Output (pass through unchanged):
{ "context": { "request_id": "abc-123", "custom_key": "value" }, "chunk": { "id": "chatcmpl-xxx", "object": "chat.completion.chunk", "created": 1234567890, "model": "gpt-4", "choices": [{"index": 0, "delta": {"content": "Hello"}, "finish_reason": null}] }, "has_chunk": true, "skip": false, "error": ""}Output (skip/filter chunk):
{ "context": { "request_id": "abc-123" }, "chunk": null, "has_chunk": false, "skip": true, "error": ""}Output (modify chunk):
{ "context": { "request_id": "abc-123" }, "chunk": { "id": "chatcmpl-xxx", "object": "chat.completion.chunk", "created": 1234567890, "model": "gpt-4", "choices": [{"index": 0, "delta": {"content": "Modified!"}, "finish_reason": null}] }, "has_chunk": true, "skip": false, "error": ""}pre_hook
Section titled “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
Section titled “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
Section titled “Configuration”Configure your WASM plugin in DeepIntShield’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
Section titled “Limitations vs Native Plugins”WASM plugins have some trade-offs compared to native Go plugins:
| Aspect | Native (.so) | WASM |
|---|---|---|
| Performance | Fastest (in-process) | JSON serialization overhead |
| Cross-platform | Build per platform | Single binary everywhere |
| Version matching | Exact Go/package match required | No version requirements |
| Memory | Shared process memory | Linear memory (limited) |
| Languages | Go only | TypeScript, Go, Rust, etc. |
| Debugging | Full Go tooling | Limited debugging support |
| Security | Full process access | Sandboxed execution |
Source Code Reference
Section titled “Source Code Reference”Complete hello-world examples are available in the DeepIntShield repository:
- TypeScript: examples/plugins/hello-world-wasm-typescript
- Go (TinyGo): examples/plugins/hello-world-wasm-go
- Rust: examples/plugins/hello-world-wasm-rust
Troubleshooting
Section titled “Troubleshooting”Module fails to load
Section titled “Module fails to load”Error: failed to instantiate WASM module
Solution: Ensure all required exports are present. Use a WASM inspection tool:
# List exportswasm-objdump -x plugin.wasm | grep -A 20 "Export"Memory allocation errors
Section titled “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
Section titled “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)
Section titled “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)
Section titled “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?
Section titled “Need Help?”- Discord Community: Join our Discord
- GitHub Issues: Report bugs or request features
- Documentation: Browse all docs