Skip to content

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.

Terminal window
omnifs init my-provider
cd my-provider

omnifs 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 = true

omnifs-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 entire wire protocol is three read operations, named to match the WIT package:

OperationFUSE triggerWhat it returns
lookup-childlookup on a directory entrynode-kind: file or directory
list-childrenreaddira list of child names
read-fileread on a fileraw bytes

Every other FUSE operation (stat, getattr, open, release) is handled by the host using the results of these three. You implement nothing else.

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.txt
Terminal window
ls /omnifs/my-provider/
# hello.txt
cat /omnifs/my-provider/hello.txt
# hello from omnifs
Terminal window
omnifs build --target wasm32-wasip2

This 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:

Terminal window
cargo build --release --target wasm32-wasip2
wasm-tools component embed wit/ \
--world omnifs:provider/provider \
target/wasm32-wasip2/release/my_provider.wasm \
-o my_provider.component.wasm

Either path produces a valid component.

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:

Terminal window
omnifs restart

The mount point appears immediately. The host does not need to be recompiled; it resolves the WIT exports at load time.

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.

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.