Your first provider
A provider is a wasm32-wasip2 component that implements the omnifs:provider@1.0.0 WIT interface. The host runtime loads it at startup and routes FUSE operations to it. You ship a new provider by dropping a .wasm file; nothing in the host changes.
This page walks from omnifs init to a mounted path. The WIT reference and testing guide cover the next steps.
Scaffold the project
Section titled “Scaffold the project”omnifs init my-providercd my-provideromnifs init writes a Cargo workspace configured for wasm32-wasip2, a wit/ directory with the current omnifs:provider WIT package, and a src/lib.rs with the three required exports stubbed out.
The generated Cargo.toml pulls in the SDK crate:
[package]name = "my-provider"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib"]
[dependencies]omnifs-provider = "1.0.0"
[profile.release]opt-level = "s"strip = trueomnifs-provider re-exports the WIT bindings generated by wit-bindgen and the host-provided capability types. You do not need to run wit-bindgen yourself.
The three operations
Section titled “The three operations”The entire wire protocol is three read operations, named to match the WIT package:
| Operation | FUSE trigger | What it returns |
|---|---|---|
lookup-child | lookup on a directory entry | node-kind: file or directory |
list-children | readdir | a list of child names |
read-file | read on a file | raw bytes |
Every other FUSE operation (stat, getattr, open, release) is handled by the host using the results of these three. You implement nothing else.
Minimal skeleton
Section titled “Minimal skeleton”Below is a complete implementation that projects a single directory with one file. It compiles, mounts, and responds to ls and cat.
use omnifs_provider::exports::omnifs::provider::filesystem::{ Guest, LookupResult, NodeKind,};
struct MyProvider;
impl Guest for MyProvider { /// Return the kind of `name` inside `parent`, or None if it does not exist. fn lookup_child(parent: u64, name: String) -> Option<LookupResult> { match (parent, name.as_str()) { // root directory contains one child: "hello.txt" (0, "hello.txt") => Some(LookupResult { inode: 1, kind: NodeKind::File, }), _ => None, } }
/// Return the names of all children of `inode`. fn list_children(inode: u64) -> Vec<String> { match inode { 0 => vec!["hello.txt".to_string()], _ => vec![], } }
/// Return the full content of the file at `inode`. fn read_file(inode: u64) -> Option<Vec<u8>> { match inode { 1 => Some(b"hello from omnifs\n".to_vec()), _ => None, } }}
omnifs_provider::export!(MyProvider with_types_in omnifs_provider);Inode 0 is always the provider root. Inodes you assign to children must be stable: the same logical object gets the same inode across calls, even after upstream renames. The host caches by inode and builds directory entries from your list-children results; mismatches cause stale entries.
The tree this stub projects:
/omnifs/my-provider/└── hello.txtls /omnifs/my-provider/# hello.txt
cat /omnifs/my-provider/hello.txt# hello from omnifsomnifs build --target wasm32-wasip2This runs cargo build --release --target wasm32-wasip2 and runs the output through wasm-tools component embed to attach the WIT metadata the host expects. The final artifact lands at target/wasm32-wasip2/release/my_provider.wasm.
If you prefer to drive the build yourself:
cargo build --release --target wasm32-wasip2wasm-tools component embed wit/ \ --world omnifs:provider/provider \ target/wasm32-wasip2/release/my_provider.wasm \ -o my_provider.component.wasmEither path produces a valid component.
Load the provider
Section titled “Load the provider”Point the host at the component in ~/.config/omnifs/providers.toml:
[[provider]]name = "my-provider"path = "/absolute/path/to/my_provider.wasm"mount = "/omnifs/my-provider"Restart the host:
omnifs restartThe mount point appears immediately. The host does not need to be recompiled; it resolves the WIT exports at load time.
Capabilities and credentials
Section titled “Capabilities and credentials”The provider never opens a socket. If your implementation needs to call an external API, it does so through a capability the host grants at load time. Credentials live in the host configuration; the provider receives an opaque handle.
[[provider]]name = "my-provider"path = "/absolute/path/to/my_provider.wasm"mount = "/omnifs/my-provider"
[provider.capabilities]http = { allow = ["api.example.com"] }secrets = { MY_API_KEY = { env = "MY_API_KEY" } }Inside the component, you call the host-provided HTTP capability rather than opening a TCP connection directly. The host enforces the allowlist. See Capabilities and security for the full capability model.
Next steps
Section titled “Next steps”Real providers read from upstream APIs inside list-children and read-file, map upstream object IDs to stable inodes, and surface errors as empty results or sentinel files rather than panics. The WIT reference documents every type in the interface. The testing guide shows how to drive a mounted provider with ls, cat, find, and rg without writing test harness code.