How to write a custom tool as a WebAssembly module
This guide shows how to give the agent a tool that runs as a WebAssembly module inside mm, in a sandbox: the module sees no file, no environment variable and no network, except the project folders you open for it. Use it instead of a command tool when the tool should not have your rights — it reads one folder, or computes something from its arguments only. It assumes TinyGo is installed and that you have an agent YAML file.
Steps
-
Write the tool as an ordinary Go program. It reads the call's arguments as one JSON object on stdin, and what it prints on stdout or stderr is what the model gets back; exiting with a non-zero code marks the call as failed:
// tools-src/search_notes/main.go package main import ( "encoding/json" "fmt" "os" "path" "strings" ) func main() { var in struct { Word string `json:"word"` } if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil { fmt.Fprintln(os.Stderr, "bad input:", err) os.Exit(2) } entries, _ := os.ReadDir("notes") for _, e := range entries { data, _ := os.ReadFile(path.Join("notes", e.Name())) for i, line := range strings.Split(string(data), "\n") { if strings.Contains(strings.ToLower(line), strings.ToLower(in.Word)) { fmt.Printf("%s:%d: %s\n", e.Name(), i+1, line) } } } } -
Build it as a WASI module:
tinygo build -o tools/search_notes.wasm -target=wasip1 -no-debug ./tools-src/search_notes -
Declare it in the agent file with
wasm:in place ofcommand:, and open the folders it needs —read:for read-only,write:for read-write:tools: - name: search_notes description: Find the lines of the notes that mention a word. parameters: word: { type: string, description: "One word, e.g. wazero" } wasm: ./tools/search_notes.wasm read: [notes]The module path is relative to the agent file; the folders are relative to the working directory, and the module opens them by the same name (
notes/…). -
Ask for it in your own words, from the project directory:
mm -p "Which notes mention TinyGo?"The terminal shows the module and the JSON it received, then its output:
🧰 search_notes: search_notes.wasm {"word":"TinyGo"} │ tinygo.md:1: # TinyGo │ wazero.md:4: It runs WASI modules, such as the ones TinyGo builds with -target=wasip1.
Variants
- A tool that writes.
write: [out]lets it create and change files inout/only. A folder it may only read isread:; a folder in neither list does not exist for it. - A pure computation. Leave out
readandwrite: the module sees no file at all. - Paths inside the module. Clean them before opening: TinyGo's WASI layer refuses
./notes/a.mdand..—path.Join("notes", name)givesnotes/a.md. A name like../agent.yamlthen becomesagent.yaml, which is not mounted: the sandbox keeps it out. - Typed parameters, defaults. The JSON holds each value in its declared type (
"top": 5,"exact": true), thedefaultof an optional parameter the call left out, and no key for an optional parameter with no default. - A long computation. Add
timeout: 2m; past it the module is stopped. Without it,bashTimeoutapplies. - Ask before each call.
yolo: falseasks for a wasm tool as for a command tool. - Another language. Any WASI preview 1 command module should run the same way; only TinyGo modules were tried.
See also
- A complete example with four modules, a read folder and a write folder:
demos/wasm-tools/ - Every key and error: configuration reference and declared tools
- What the sandbox allows and what it costs: Declared tools
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
|