Using Wasm Modules
Note
This tutorial shows how to run WebAssembly (Wasm) modules in Sysinspect and explains how the Wasm runtime behaves internally.
Reasoning
WebAssembly in Sysinspect is not about browsers.
Here, Wasm is used as a portable execution format, comparable to a Java .jar:
a single, architecture-independent artifact that can be distributed and executed
consistently across heterogeneous systems.
Traditional configuration management systems primarily execute scripts or, in some cases, native binaries directly on the target system. As soon as compiled binaries are involved, this approach becomes fragile:
CPU architecture differences
libc and ABI mismatches
platform-specific packaging
manual dependency logistics
inconsistent runtime behavior
Sysinspect already solves binary logistics by distributing architecture-specific packages automatically. However, Wasm enables a different and often simpler model:
one noarch artifact
sandboxed execution
explicit host interaction
predictable behavior across platforms
In Sysinspect, a Wasm module behaves operationally like a script, even though it is compiled. You build it once, publish it once, and run it everywhere.
Hint
Surely, one can compile native binaries for each architecture and develop binary modules directly.
Yes, but this quickly becomes tedious and complex, as your module must also take care of direct integration with Sysinspect itself. Wasm offers a more elegant solution and isolates all the integration bits away from the module logic, allowing you to focus on the core functionality.
Prerequisites
Before you begin, ensure you have:
Installing Wasm Runtime
Sysinspect executes Wasm modules through a runtime module. A runtime module is a standard Sysinspect module that embeds a Wasm engine and defines how Wasm code interacts with the host system.
To execute Wasm binaries, a Wasm runtime must be present in the package repository.
Build the runtime
If building Sysinspect from source:
make
The Wasm runtime binary is typically located at:
$SRC/target/release/runtime/wasm-runtime
Register the runtime
Register the runtime on the SysMaster:
sysinspect module -A \
--path /path/to/target/release/runtime/wasm-runtime \
--name "runtime.wasm" \
--descr "Wasm runtime"
Verify registration:
sysinspect module -L
Then sync the cluster:
sysinspect --sync
Installing Wasm Modules
Wasm user modules are distributed as compiled .wasm files. From Sysinspect’s
perspective, these are noarch executable artifacts handled by the Wasm runtime.
Directory layout
Wasm modules are installed as a library tree, similar to Lua scripts:
lib/
runtime/
wasm/
hellodude.wasm
caller.wasm
reader.wasm
The important rule is consistency: upload the directory tree, not individual files, so all nodes observe identical paths.
Publish the modules
Upload the directory containing the Wasm modules:
sysinspect module -A --path ./lib -l
Then sync the cluster:
sysinspect --sync
Verify:
sysinspect module -Ll
Execution Model and JIT Compilation
When a Wasm module is executed for the first time on a node, the runtime performs a one-time compilation step:
The
.wasmfile is translated into a cached native representation (.cwasminternally).The cached artifact is an ELF relocatable object, specific to the host architecture.
Subsequent executions reuse the cached artifact and are significantly faster.
This is conceptually similar to Python’s .pyc or .pyo files.
Important
Cached .cwasm artifacts are not tracked by the Sysinspect package manager, because they are:
architecture-dependent
node-local
runtime-managed
They must never be uploaded or distributed. They appear automatically and are removed automatically on the next sync. Shipping a cached artifact to a different architecture will result in undefined behavior.
Language Support
Any language capable of compiling to WASI can theoretically be used.
In practice, Sysinspect focuses on languages that produce small, fast, and predictable Wasm binaries. There are two primary recommendations:
TinyGo is the recommended choice if you want it “in 10 minutes”, Rust otherwise. TinyGo offers:
Relatively small binaries
Predictable output
Excellent WASI support
One can learn Go in a few hours
“Tons” of ready to use libraries
Important
You can also use standard Go with
GOOS=wasip1 GOARCH=wasm, but TinyGo produces much smaller and faster binaries.
Rust is your primary choice if you want maximum performance and control, but don’t want it “in 10 minutes”. Rust offers:
Smallest/fastest binaries
Excellent WASI support
Very mature toolchain
Excellent performance
Memory safety
Rich ecosystem
Note
Although Rust makes many things “right”, yet it has a way much steeper learning curve. Even if you’ve mastered it enough, the development speed is not necessarily faster than with Go.
At last, Configuration Management does not require systems programming skills and usually any CM module code is typically a “glue boilerplate”, that can be done with higher-level languages.
But it is still fun. :-)
C/C++ is also a solid choice, but you must take care of memory management and other low-level details yourself. Other languages do technically work as well (Grain, Swift etc), but they aren’t supported in SysInspect realm. If you want to try them out, you should be prepared for one or more side effects:
Significantly larger binaries
Poor(-er) performance
Unstable toolchains
Randomly missing WASI features
Other bad surprises
While experimentation is encouraged, production modules should prioritise simplicity and predictability. In any case, if you find a language that works well, please share your experience with the community.
SDKs and Helper Libraries
Language-specific helper libraries and SDKs are expected to evolve as community contributions.
At present, the Wasm runtime operates in spartan mode:
minimal host API
no language-specific abstractions
explicit behavior over convenience
This reduces maintenance cost and keeps runtime behavior transparent.
Portable Helpers
Unlike Lua and Py3, the Wasm runtime does not inject a runtime-owned high-level object directly into the guest language. Instead, SysInspect ships guest-side helper code that builds portable helper meanings on top of the generic Wasm host header API.
For Rust guests, the example helper lives under:
modules/runtime/wasm-runtime/examples/rust-sdk/host.rs
That helper exposes the same portable host meanings as the Lua and Py3 runtimes:
has(name)trait_value(name)paths()path_value(name)
These helpers are still passive views over the shared request payload. The
source of truth remains the request header, especially host.traits and
host.paths.
Logging
Wasm logging is currently lower-level than Lua or Python runtime logging, but it is already available as part of the host API.
Wasm guests can emit runtime logs through the low-level import:
api.log(level, msg_ptr, msg_len)
Guest-side helper code may wrap that into a more convenient language-level
API, but Sysinspect itself does not currently inject a high-level log.*
namespace into Wasm guests.
The runtime collects guest-emitted logs and exposes them in the standard
__sysinspect-module-logs response field. If a guest returns a raw
__module-logs field directly, the runtime remaps that field into the same
standard location.
This means:
Lua and Python expose runtime-owned
log.*helpers.Wasm exposes a lower-level
api.log(...)host import instead.the runtime normalises the final output field to
__sysinspect-module-logs.
Platform-specific helpers
The Wasm host API also exposes low-level PackageKit helper imports under the
api import module:
packagekit_available() -> i32packagekit_status(out_ptr, out_cap) -> i32packagekit_packages(out_ptr, out_cap) -> i32packagekit_history(req_ptr, req_len, out_ptr, out_cap) -> i32packagekit_install(req_ptr, req_len, out_ptr, out_cap) -> i32packagekit_remove(req_ptr, req_len, out_ptr, out_cap) -> i32packagekit_upgrade(req_ptr, req_len, out_ptr, out_cap) -> i32
These imports are intentionally low-level. They return JSON payloads through guest memory buffers, unlike the higher-level Lua and Python helper namespaces. They remain Linux-only active helpers and are not part of the portable descriptive contract.
Calling a Wasm module from a model
Runtime-bound modules are invoked through the virtual wasm.<module>
namespace. Sysinspect resolves that virtual namespace to the installed
runtime.wasm dispatcher automatically.
Example action:
call-hello:
descr: Call WASM/WASI module
module: wasm.hellodude
bind:
- wasm
state:
$:
args:
key: PRIVACY_POLICY_URL
Here:
wasm.hellodudeidentifies the Wasm runtime module.Arguments with the
rt.*prefix are reserved for runtime configuration. You can always get runtime manual with directly calling the runtime module using--manargument.Arguments without the
rt.*prefix are passed to the submodule “as is”.
Mixed Runtime Example
A single model can freely combine Wasm, Lua, and native Sysinspect modules.
Example:
entities:
- example
actions:
call-spawner:
descr: Try spawner
module: wasm.caller
bind: [example]
state:
$:
args: {}
get-os-version:
descr: Return OS version
module: lua.reader
bind: [example]
state:
$:
opts: [rt.logs]
args: {}
ping:
descr: Information module
module: sys.run
bind: [example]
state:
$:
args:
cmd: "cat /etc/machine-id"
This demonstrates that runtimes are orthogonal: each runtime handles its own
execution model, while Sysinspect orchestrates them uniformly when you call one example entity.
Troubleshooting
Confirm the runtime appears in
sysinspect module -L.Ensure the Wasm module was compiled for WASI.
Verify the library directory was uploaded and synced.
Do not distribute cached runtime artifacts.