TinyGo + WASM¶
WASM-tier plugins are TinyGo modules executed inside Samuel by wazero. The runtime is pure Go — no host toolchain, no shared libraries, no platform-specific binaries to ship.
Toolchain¶
You need:
- TinyGo ≥ 0.31 —
brew install tinygoor download. - Go ≥ 1.22 (TinyGo's host).
- A working
samuelinstall for local validate.
Project layout¶
samuel-my-translator/
├── samuel-plugin.toml
├── go.mod
├── plugin.go # main package, host-exported functions
├── plugin_test.go
└── .github/workflows/
└── release.yml # uses samuelpkg/samuel-plugin-release@v1
Minimum module¶
package main
import (
"unsafe"
samuel "github.com/samuelpkg/samuel-wasm-sdk"
)
//export samuel_protocol_version
func samuel_protocol_version() int32 { return 1 }
//export health
func health() int32 {
return 1 // 1 = healthy, 0 = unhealthy
}
//export sync_after
func sync_after(payloadPtr, payloadLen uint32) (resultPtr uint64) {
payload := samuel.ReadBytes(payloadPtr, payloadLen)
var in samuel.SyncAfterIn
samuel.UnmarshalJSON(payload, &in)
for _, f := range in.FilesWritten {
if !samuel.HasSuffix(f, "AGENTS.md") {
continue
}
body, _ := samuel.FsRead(f)
target := samuel.ReplaceSuffix(f, "AGENTS.md", "CLAUDE.md")
samuel.FsWrite(target, body)
}
return samuel.WriteJSON(samuel.OK)
}
func main() {} // required by TinyGo, never executed
Build¶
The wasi target is required — Samuel runs modules under wazero with the WASI snapshot 1 surface plus the samuel.* host module.
Host ABI¶
Samuel exposes a host module named samuel with these calls:
| Function | Purpose | Capability |
|---|---|---|
samuel.log(level, ptr, len) | structured log line | none |
samuel.fs_read(path_ptr, path_len) -> (out_ptr, out_len, err) | read a file | filesystem.read |
samuel.fs_write(path_ptr, path_len, body_ptr, body_len) -> err | write a file | filesystem.write |
samuel.exec(argv_ptr, argv_len, env_ptr, env_len) -> (exit, out_ptr, out_len) | shell out | exec |
samuel.net_outbound(req_ptr, req_len) -> (resp_ptr, resp_len) | HTTP(S) | network.outbound |
samuel.config_get(key_ptr, key_len) -> (val_ptr, val_len) | read samuel.toml slot | samuel.api |
samuel.callback(event_ptr, event_len, payload_ptr, payload_len) -> err | fire a sub-event | varies |
The SDK at samuelpkg/samuel-wasm-sdk wraps these in Go-idiomatic helpers so plugins don't deal with raw pointers.
Restrictions¶
TinyGo's WASI target imposes:
- No goroutines (the runtime is single-threaded).
- No
reflectbeyond what TinyGo's subset allows. - No
cgo. - Limited stdlib (notably
net/httpis replaced bysamuel.net_outbound). time.Now()and friends work;time.Sleepblocks the runtime — avoid.
Stick to pure logic, file I/O via host calls, and JSON over the SDK helpers. Anything that wants threads or sockets is OCI-tier territory.
Local validate¶
samuel plugin validate samuel-plugin.toml
samuel install file://./ # installs from local checkout for testing
samuel doctor
Compile cache¶
The first install of a WASM plugin compiles the module via wazero and stores the result at ~/.samuel/cache/wasm-compiled/<sha256>/. Subsequent installs and runs hit the cache. The cache is per-process for the running framework version — bumping samuel invalidates everything.