/extism-wasm-plugins
When to Use
- Building a plugin system where third-party code extends your agent
- Each plugin must be isolated (WASM sandbox) but able to call host functions
- Multi-language plugins: Rust, Go, JavaScript, Python, C all compile to the same interface
- Agent tool registry backed by hot-loadable WASM plugins
Do NOT use for
- Single-language native addons (use [[napi-rs-native-addons]])
- Sandboxing untrusted code with filesystem access (use [[wasmtime-wasi-sandbox]])
- Browser-side WASM (use [[wasm-bindgen-js-interop]])
Architecture
Host (Node.js / Rust / Go / Python)
├─ Plugin manager
│ ├─ plugin-a.wasm (Rust PDK)
│ ├─ plugin-b.wasm (Go PDK)
│ └─ plugin-c.wasm (JS PDK)
└─ Host functions exposed to all plugins
├─ kv_get / kv_set — shared KV store
├─ http_request — proxied HTTP (host controls allowed URLs)
└─ log — host-side logging
Shared memory protocol:
Host writes input → plugin_input_length / plugin_input_load
Plugin reads input → plugin_get_input
Plugin writes output → plugin_set_output
Host reads output → plugin_output_length / plugin_output_load
Node.js host (calling plugins)
import { Plugin, ExtismContext } from '@extism/extism';
const ctx = new ExtismContext();
// Load a plugin (WASM bytes or file path)
const plugin = await ctx.plugin('./plugins/summarizer.wasm', {
// Host functions the plugin can call
functions: {
'extism:host/user': {
kv_read: (plugin, offset) => {
const key = plugin.readString(offset);
const val = kvStore.get(key) ?? '';
return plugin.writeString(val);
},
kv_write: (plugin, keyOffset, valOffset) => {
const key = plugin.readString(keyOffset);
const val = plugin.readString(valOffset);
kvStore.set(key, val);
},
}
},
// Resource limits
memory: { max: 5 }, // 5 × 64KB = 320KB
});
// Call an exported function
const input = JSON.stringify({ text: "Long article..." });
const output = await plugin.call('summarize', input);
console.log(output.string()); // "Summary: ..."
plugin.free();
ctx.free();
Rust plugin (PDK)
// Cargo.toml: extism-pdk = "1"
use extism_pdk::*;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Input { text: String }
#[derive(Serialize)]
struct Output { summary: String, word_count: usize }
#[plugin_fn]
pub fn summarize(input: Json<Input>) -> FnResult<Json<Output>> {
let words: Vec<&str> = input.text.split_whitespace().collect();
let summary = words.iter().take(20).cloned().collect::<Vec<_>>().join(" ");
Ok(Json(Output {
summary: format!("{}...", summary),
word_count: words.len(),
}))
}
// Call a host function from the plugin
#[host_fn]
extern "ExtismHost" {
fn kv_read(key: &str) -> String;
fn kv_write(key: &str, value: &str);
}
#[plugin_fn]
pub fn cached_summarize(input: Json<Input>) -> FnResult<Json<Output>> {
let cache_key = format!("summary:{}", &input.text[..20]);
unsafe {
if let Ok(cached) = kv_read(&cache_key) {
if !cached.is_empty() {
return Ok(Json(Output { summary: cached, word_count: 0 }));
}
}
}
// ... compute and cache ...
Ok(Json(Output { summary: "...".to_string(), word_count: 0 }))
}
Go plugin (PDK)
package main
import (
"encoding/json"
extism "github.com/extism/go-pdk"
)
//export summarize
func summarize() int32 {
input := extism.InputString()
var data map[string]string
json.Unmarshal([]byte(input), &data)
result := map[string]string{
"summary": data["text"][:min(len(data["text"]), 100)] + "...",
}
out, _ := json.Marshal(result)
extism.OutputBytes(out)
return 0
}
func main() {}
Plugin registry (hot-reload)
class PluginRegistry {
private ctx = new ExtismContext();
private plugins = new Map<string, Plugin>();
async load(name: string, wasmPath: string): Promise<void> {
if (this.plugins.has(name)) this.plugins.get(name)!.free();
const plugin = await this.ctx.plugin(wasmPath, { wasi: true });
this.plugins.set(name, plugin);
}
async call(name: string, fn: string, input: string): Promise<string> {
const plugin = this.plugins.get(name);
if (!plugin) throw new Error(`Plugin '${name}' not loaded`);
const output = await plugin.call(fn, input);
return output.string();
}
unload(name: string): void {
this.plugins.get(name)?.free();
this.plugins.delete(name);
}
}
Anti-Fake-Pass Checklist
❌ Not calling plugin.free() → WASM heap and host context leak
❌ Exposing unrestricted http_request host function → plugins can reach internal APIs
❌ No memory limit → malicious plugin allocates until host OOM
❌ Sharing Plugin instance across concurrent calls → Extism plugins are not thread-safe; one per goroutine/thread
❌ Passing secrets via env vars → plugins can read env if WASI is enabled; pass only via host functions
❌ Hot-reload without freeing old plugin → old WASM instance stays in memory