Skip to content

feat(leaf): pin the WASI build of the leaf and ship a host shim example - #487

Merged
bahdotsh merged 4 commits into
mainfrom
ci/headless-leaf-wasi
Sep 30, 2026
Merged

bahdotsh merged 4 commits into
mainfrom
ci/headless-leaf-wasi

Conversation

@bahdotsh

@bahdotsh bahdotsh commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Summary

The leaf node now builds for a WebAssembly host, on the WASI target, and CI gates it. A host shim example shows the shape a runtime drives: the time and the frames come in from the host, the frames the device owes go back out.

  • CI pins wasm32-wasip1 in the embedded-core job, in both halves of the crate. The no_std half builds and lints with no bare-metal-rng: on WASI getrandom's backend is chosen by the target ahead of any feature and reads the runtime's random_get, so the host's entropy is what the MLS library draws and there is no symbol for the host to register. The std half builds through the new example, so the example cannot rot into a file nobody compiles.
  • examples/wasi_host_shim.rs opens a device in a MemoryStore, mints the key package frame for a peer at a host-supplied now_unix_secs, hands every JSON frame on standard input to LeafDevice::handle at that time, prints the frames it owes and the events it raised, and at end of input seals one message to the peer. With no Welcome among the input, that seal is refused with NoSession and the shim prints the refusal: a sealed frame exists only for a session that exists, and the example does not fake a peer to produce one. The header gives the exact build and run lines, a "what this deliberately does not show" list (a pairing completing, durable storage, a clock, authorization), and the entropy warning: on WASI every key is exactly as strong as the runtime's random_get.
  • wasm32-unknown-unknown is deliberately not claimed, and the CI comment, the leaf README, CLAUDE.md and ADR 0021's consequences all say why: on that target the pinned mls-rs enables getrandom's js feature, and getrandom 0.2 selects js ahead of custom, so a host that registered its own entropy backend would supply a symbol nothing calls and the module would import browser glue. A green build there proves nothing about a non-browser host. The tree carried no WASM claim before this PR; the one that existed lived in a session note and was wrong for exactly this reason.

What the module imports

The release build of the example for wasm32-wasip1 is 1.2 MiB and its import section names one module, wasi_snapshot_preview1, with eight functions: args_get, args_sizes_get, environ_get, environ_sizes_get, fd_read, fd_write, proc_exit and random_get. No clock is imported, which is the crate's time obligation made visible: the leaf never reads one. No browser glue is imported. The wasm-bindgen crate is compiled in, because mls-rs depends on it unconditionally on every wasm32 target, and it leaves three unused export symbols (__wbindgen_malloc, __wbindgen_free, __wbindgen_exn_store) in the module; nothing imports through it.

The phone side is absent on WebAssembly

Building an example builds the crate's dev-dependencies, and OpenMLS 0.7.4 does not compile for wasm32-wasip1: its key package lifetime reads fluvio_wasm_timer, which only its js feature supplies. The offline-protocol-mls dev-dependency is now declared under cfg(not(target_arch = "wasm32")), and tests/phone_interop.rs, its only user, gates itself off the same targets. On every host the interop tests run exactly as before.

Validation

  • cargo fmt --all -- --check, cargo clippy --workspace -- -D warnings.
  • The three new CI steps, run verbatim: build and clippy of the leaf with --no-default-features --locked --target wasm32-wasip1 -- -D warnings, and the example build for the target.
  • The existing bare-metal gates still pass with the example declared: the leaf on thumbv8m.main-none-eabihf (clippy, with bare-metal-rng) and on thumbv6m-none-eabi (build), which is where a required-features mistake would have failed.
  • cargo build -p offline-protocol-leaf --locked, cargo test -p offline-protocol-leaf (the interop tests and the manifest guard, and the example compiled on the host), cargo clippy -p offline-protocol-leaf --examples -- -D warnings, rustdoc for the leaf under -D warnings.
  • scripts/check-crate-readmes.sh and scripts/tests/test-generate-bindings.sh (54 guards), the two scripts that read files this PR touches.
  • The example was run natively on macOS with an empty input and with one plaintext frame addressed to another node: it prints the device address, the key package frame, Ignored { reason: "frame is addressed to another node" } for the frame, and the NoSession refusal. The import section was read from the release .wasm with a short parser of the binary format; no wasm tooling is installed on this machine.

Not in this PR

  • A run under a WebAssembly runtime. Neither wasmtime nor wasmer is installed here and nothing was installed for this PR. The module imports only wasi_snapshot_preview1, and the same source runs natively, but "runs under a runtime" is not claimed.
  • A pairing on WASI. That needs a phone; the two ends meet in phone_interop and tools/mls-interop, on the host.
  • A footprint number. tools/embedded-footprint is Cortex-M only and cannot measure a .wasm.
  • An upstream ask to make mls-rs's wasm32 getrandom features opt-in. Not needed for the claim this PR makes; it would only widen it to the browser target.

Notes for reviewers

  • The example uses the crate's std feature and required-features = ["std"], so the --no-default-features builds skip it instead of failing on it. CI builds it in its own step for that reason.
  • The WASI steps do not pass --features bare-metal-rng, and passing it would change nothing: getrandom 0.2 tries the target's own backend (wasi) before js and before custom, and mls-rs already enables custom on every wasm32 target, so the example links on WASI with the feature on and nothing registered. The feature is left off because it names an obligation the firmware takes on, and on WASI there is none: the entropy is the runtime's. The manifest comment on the feature now says this; it previously claimed the feature would replace an operating system's entropy, which is wrong wherever custom is the last fallback.
  • No UDL change, no binding regeneration, no behaviour change in the crate: the two manifest edits are a dev-dependency moved under a target table and an example declaration.

CI builds and lints `offline-protocol-leaf` for `wasm32-wasip1` in both
halves: without default features, where getrandom reads the runtime's
`random_get` and `bare-metal-rng` is not used, and with `std` through the
new `wasi_host_shim` example, which drives a device from a runtime over its
standard streams with the time and the frames supplied by the host.

`wasm32-unknown-unknown` is not claimed. There the pinned mls-rs enables
getrandom's `js` feature, which getrandom 0.2 selects ahead of `custom`, so
a host-registered entropy backend would be a symbol nothing calls and the
module would import browser glue. A green build there proves nothing about
a non-browser host, and the CI comment, the leaf README, CLAUDE.md and ADR
0021 say so.

An example is built with the crate's dev-dependencies, and OpenMLS does not
compile for WebAssembly, so the phone-side dev-dependency moves under
`cfg(not(target_arch = "wasm32"))` and the interop test that needs it gates
itself off the same targets. Nothing changes on a host.
@bahdotsh
bahdotsh force-pushed the ci/headless-leaf-wasi branch from 1f8e9c3 to 0f34585 Compare September 30, 2026 15:56
@bahdotsh
bahdotsh merged commit e01af54 into main Sep 30, 2026
22 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant