wasi-leg: WasiLeg + WasiRuntime on wasm32-wasip2 (Section B)#2
Merged
Conversation
B1 — opens the rollout of the WasiLeg + WasiRuntime surface
(Section B of the pre-1.0 deferred-followups plan). Pulls in
`wasi = 0.14` (the WIT-bindgen-based Preview 2 binding, the
hard prerequisite for the rest of Section B) under a fresh
`wasi-leg` Cargo feature.
`core/Cargo.toml`:
- New `wasi-leg = ["std", "dep:wasi"]` feature in `[features]`.
- New `[target.'cfg(target_os = "wasi")'.dependencies]` block
with `wasi = { version = "0.14", optional = true }`. The
target gate scopes the dep to wasm32-wasi* triples
(wasm32-wasi, wasm32-wasip1, wasm32-wasip2); host builds and
wasm32-unknown-unknown never see the crate.
`core/src/lib.rs`:
- New `compile_error!` for the `wasi-leg + wasm32-unknown-unknown`
combination — that target doesn't support the wasi crate's WIT
bindings, and the project's wasm32-unknown-unknown story is
WebSocketLeg + WasmRuntime, not WasiLeg.
Verification (local toolchain stable 1.93, wasi 0.14.7+wasi-0.2.4
on crates.io):
- `cargo check --manifest-path core/Cargo.toml` — clean (no
regression on host).
- `cargo check --target wasm32-wasip2 --features wasi-leg` —
clean; pulls in `wasi 0.14.7+wasi-0.2.4`.
- `cargo check --target wasm32-unknown-unknown --features wasi-leg`
— fails with the mutual-exclusion `compile_error` pointing at
the right alternative.
The wasi-leg modules themselves (WasiRuntime, WasiLeg) land in
B2 / B3; this commit is the dep + feature scaffold only. CI
flips wasm32-wasi(p2) from `allow_failure` to a hard gate in B6.
B2 — adds the WASI Preview 2 `Runtime` impl that pairs with the
`WasiLeg` transport landing in B3.
`core/src/runtime/wasi_runtime.rs` (new module):
- `WasiRuntime` holds a private `Arc<Mutex<Vec<TaskSlot>>>` task
queue. `spawn` pushes onto the queue and returns a `SpawnHandle`
whose abort flips an `AtomicBool` that the task slot's poll loop
checks (mirrors `EmbeddedRuntime`).
- `drive()` polls every queued task exactly once with a no-op
waker, retains the ones that returned `Pending`, drops the
completed-or-aborted ones, and returns the remaining count.
- `poll_until_progress(max_wait)` blocks the host on
`wasi::io::poll::poll` for at most `max_wait`, using
`wasi::clocks::monotonic_clock::subscribe_duration` as the
bounded watchdog so the drive loop always makes eventual
progress even if a future never registers a Pollable
(defense against the "starved task" failure mode flagged in
the plan).
- `sleep` is deadline-checking via `std::time::Instant::now()`
(libstd's WASI port routes through `wasi::clocks::monotonic_clock`).
- `now_monotonic` / `now_wall_clock` delegate to
`std::time::{Instant, SystemTime}::now()` — same wrappers as
the other `Runtime` impls so the trait surface stays uniform.
`core/src/runtime/mod.rs` — `pub mod wasi_runtime` + re-export
guarded on `cfg(all(feature = "wasi-leg", target_os = "wasi"))`.
Tests (compile + run only on `wasm32-wasi*`):
- `wasi_runtime_is_object_safe` — pins the `dyn Runtime` trait
object compatibility.
- `monotonic_clock_does_not_go_backwards` — guards against the
WASI clock wrapper inverting deltas.
- `wall_clock_is_after_unix_epoch` — host-clock sanity.
- `spawn_noop_completes_on_first_drive` — task lifecycle smoke.
- `abort_short_circuits_pending_future` — confirms the abort
flag is observed across a `drive()` round.
The host integration test that actually exercises a WASI guest
under `wasmtime` lands in B5; this commit ships the runtime
surface only.
Verification:
- `cargo check --manifest-path core/Cargo.toml` — clean (no
module visible on host).
- `cargo check --target wasm32-wasip2 --features wasi-leg` —
clean; module compiles against `wasi 0.14.7+wasi-0.2.4`.
- `cargo test --lib` — 233 tests pass; no host-side regressions.
B3 — adds the WASI Preview 2 TCP transport leg that pairs with `WasiRuntime` (B2). Provides the same length-prefix-framed `SessionTransport` shape as `TcpSessionTransport` so a WASI guest running inside `wasmtime` / `wasmer` / `jco` can drive the unchanged Phantom session machinery. `core/src/transport/legs/wasi.rs` (new module): - `WasiLeg::connect(SocketAddr)` — converts `std::net::SocketAddr` into the WIT `IpSocketAddress` enum (Ipv4SocketAddress or Ipv6SocketAddress), creates a TCP socket on the default network via `wasi::sockets::tcp_create_socket::create_tcp_socket`, kicks off `start_connect`, blocks on the resulting `subscribe()` Pollable via `wasi::io::poll::poll`, then `finish_connect`s into the `(InputStream, OutputStream)` pair the leg holds. - `send_bytes` writes the 4-byte big-endian length prefix + payload via `OutputStream::blocking_write_and_flush`. Rejects oversized frames (> 16 MiB) up front — same cap as `TcpSessionTransport`. - `recv_bytes` reads the length prefix, validates against the cap, then reads `len` bytes into a persistent `BytesMut` accumulator (one allocation amortized across the steady state), and hands the caller an O(1) `split_to(len).freeze()` slice. `blocking_read` may return fewer bytes than requested; a `read_exact` helper loops until satisfied or EOF. - `ip_socket_address_from_std` is split out + unit-tested for the Ipv4/Ipv6 octet conversions. `unsafe` opt-in (`#![allow(unsafe_code)]`): - The WIT-bindgen-generated `TcpSocket` / `InputStream` / `OutputStream` resources are not auto-`Send`/`Sync` (they wrap an opaque numeric handle). WASI Preview 2 is single-task per instance, so the manual `unsafe impl Send / Sync for WasiLeg` blocks are sound — the `Mutex` wrappers inside `WasiLeg` provide the only real synchronisation. Justified inline at the module-level attribute. `core/src/transport/legs/mod.rs` — `pub mod wasi` + `pub use wasi::WasiLeg` guarded on `cfg(all(feature = "wasi-leg", target_os = "wasi"))`. Client-only for this commit. Server-side accept (`start_listen` / `finish_listen` / `accept`) is explicitly out of scope per the plan's Decision Point 3 — phantom-server-on-WASI is deferred. Tests: - `ipv4_addr_conversion_round_trips` (host-runnable) — pins the IpAddressFamily / Ipv4SocketAddress conversion. - `ipv6_addr_conversion_round_trips` (host-runnable) — same for IPv6 segments + flowinfo + scope_id. End-to-end round-trip through a real `wasmtime` host lands in B5. Verification: - `cargo check --manifest-path core/Cargo.toml` — clean (no module visible on host beyond the conversion helper). - `cargo check --target wasm32-wasip2 --features wasi-leg` — clean. - `cargo test --lib` — 233 tests pass; no host-side regressions.
B4 — pins the WASI-leg module gates and surfaces them in the public-API rustdoc, completing the visibility plumbing for the `wasi-leg` feature. `core/src/api/mod.rs` — top-of-module doc comment now points at `crate::transport::legs::wasi::WasiLeg` (paired with `crate::runtime::wasi_runtime::WasiRuntime`) as the `wasm32-wasi*` `SessionTransport`, alongside the existing `WebSocketLeg` callout for `wasm32-unknown-unknown`. The `pub mod wasi;` / `pub use wasi::WasiLeg;` re-exports under `core/src/transport/legs/mod.rs` and `core/src/runtime/mod.rs` were added in B3 / B2 respectively; both are guarded on `cfg(all(feature = "wasi-leg", target_os = "wasi"))`. The `core/src/lib.rs` `compile_error!` from B1 still rejects `--features wasi-leg` on `wasm32-unknown-unknown`. Verification: - `cargo check --manifest-path core/Cargo.toml` — clean (no module visible on host). - `cargo check --target wasm32-wasip2 --features wasi-leg` — clean (modules visible, symbols defined). - `cargo doc --target wasm32-wasip2 --features wasi-leg --no-deps` — generates `target/wasm32-wasip2/doc/phantom_core/runtime/ wasi_runtime/` and `target/wasm32-wasip2/doc/phantom_core/ transport/legs/wasi/` directories, confirming both symbols render in the public surface.
B5 — the WASI-leg surface is now exercised end-to-end. The host
test compiles `core/tests/fixtures/wasi-guest/` for
`wasm32-wasip2`, stands up a native length-prefix-aware TCP echo
server on a loopback OS-chosen port, spawns the guest under
`wasmtime` (granted the `inherit-network` socket capability with
`PHANTOM_PORT` plumbed via env), and asserts byte-equality between
the sent and echoed payload.
To make the build path work, B5 also lands the supporting Cargo
plumbing:
- New **`bindings`** Cargo feature (default-on) — pulls `dep:uniffi`
on its own rather than via the kitchen-sink `std` feature. The
WASI guest sets `--features wasi-leg` without `bindings` so
UniFFI's exported-symbol metadata (incompatible with
`wasm-component-ld`, the wasm32-wasip2 linker) stays out of the
guest's dependency graph. Every `#[uniffi::*]` attribute /
derive in `core/src/{api/*,errors,config,lib,bin/uniffi-bindgen}`
is gated via `#[cfg_attr(feature = "bindings", ...)]`; the bin
also gains a `cfg(not(feature = "bindings"))` fallback that
exits with a clear "rebuild with --features bindings" message.
- The browser-only `[target.cfg(target_arch = "wasm32")]`
dependency block in `core/Cargo.toml` narrows to
`cfg(all(target_arch = "wasm32", target_os = "unknown"))`. The
`WebSocketLeg` module + `WasmRuntime` module cfg gates also
narrow to `target_os = "unknown"`. Net effect: `wasm-bindgen`,
`web-sys`, `js-sys`, the `getrandom = { features = ["js"] }`
shim, etc. are no longer pulled into the wasm32-wasi* tree —
the WASI guest gets a clean dependency graph.
`core/tests/fixtures/wasi-guest/` (new):
- Standalone Cargo project (own `[workspace]`) depending on
`phantom_core` via path, `default-features = false`,
`features = ["std", "wasi-leg"]`.
- `src/main.rs` reads `PHANTOM_PORT` from env, opens a `WasiLeg`
TCP connection to `127.0.0.1:PHANTOM_PORT`, calls
`SessionTransport::send_bytes` then `recv_bytes` with a fixed
payload, exits 0 on byte-equal echo, 2 on mismatch.
`core/tests/wasi_integration.rs` (new):
- `wasi_guest_round_trips_payload_through_wasmtime` (`#[ignore]`).
- Gracefully skips if `wasm32-wasip2` is not installed or
`wasmtime` is not on PATH.
- Builds the guest via `cargo build --target wasm32-wasip2`,
starts the echo server on an OS-chosen loopback port, spawns
`wasmtime run -S inherit-network --env PHANTOM_PORT=…` against
the guest binary, and asserts exit code 0 + the guest's "OK:
round-tripped …" stderr marker.
Verification:
- `cargo test --lib` — 233 tests pass (no host-side regressions
from the bindings feature split).
- `cargo build --manifest-path core/tests/fixtures/wasi-guest/Cargo.toml
--target wasm32-wasip2` — clean.
- `cargo test --test wasi_integration -- --ignored` — passes
locally on `wasmtime 45.0.0` against a 31-byte payload.
Scope: this is a TCP-byte-pipe round-trip through `WasiLeg`, NOT
a full `PhantomSession` handshake. The latter needs
`connect_with_transport_with_runtime` wiring against
`WasiRuntime`'s `drive`/`poll_until_progress` loop plus
fips-disabled crypto, which is a separate follow-up. The B-section
deliverable (`wasm32-wasi` becomes a hard CI gate) is unchanged
either way.
B6 — replaces the `wasm32-wasi` `allow_failure: true` row in `.github/workflows/cross.yml` with a hard-gated `wasm32-wasip2` entry, completing the deprecation of the only `allow_failure` matrix slot in the 12-target cross-compile workflow. `wasm32-wasi` was the legacy alias for what rustup now calls `wasm32-wasip1` (Preview 1). The `wasi-leg` rollout targets Preview 2 (`wasm32-wasip2`) since `wasi:sockets/tcp` lives only there — `wasi 0.14` (the WIT-bindgen Preview 2 crate) cannot target `wasm32-wasip1` at runtime even though it links. Matrix entry: - `target: wasm32-wasip2` - `cargo_args: "--no-default-features --features std,wasi-leg"` drops the `bindings` feature so UniFFI's exported-symbol metadata stays out of the dep graph (incompatible with `wasm-component-ld`, the wasm32-wasip2 linker). New `wasi-integration` job (single, not part of the compile-check matrix): - Installs `wasmtime` via the pinned official installer script. - Runs `cargo test --test wasi_integration -- --ignored`, which builds the `phantom-wasi-guest` fixture under wasm32-wasip2, spawns it under wasmtime against a native length-prefix echo server, and asserts byte-equal round-trip. The 12-target matrix has zero `allow_failure: true` rows after this commit; every cross-compile target is a hard gate. Verification: - `cargo check --manifest-path core/Cargo.toml --lib --target wasm32-wasip2 --no-default-features --features std,wasi-leg` — clean locally (the exact command CI runs). - `cargo test --test wasi_integration -- --ignored` — passes locally on wasmtime 45.0.0.
B7 — closes Section B of the pre-1.0 deferred-followups plan. `docs/operations/wasi.md` — rewritten from the "deferred" gap analysis into a quickstart for the now-shipped surface: - TL;DR with a minimal WASI Preview 2 guest using `WasiLeg`. - Public-surface table (`WasiLeg`, `WasiRuntime`) with module paths and what each does. - "Why `--no-default-features --features std,wasi-leg`?" section explaining the `bindings` feature split (UniFFI + wasm-component- ld incompatibility). - Browser vs WASI vs native dep-graph split table. - Reproducing the host integration test locally (`rustup target add wasm32-wasip2`, `brew install wasmtime`). - Explicit "Out of scope" list (no server-side accept, no full `PhantomSession` over `WasiLeg` yet). `CHANGELOG.md` — new "Added — `wasi-leg` Cargo feature" section under `[Unreleased]` summarising the surface (`WasiLeg`, `WasiRuntime`), the `bindings` feature split, the browser-WASM- only dep block narrowing to `target_os = "unknown"`, and the CI flip (wasm32-wasi → wasm32-wasip2 hard gate + new `wasi-integration` job). The 12-target cross-compile matrix now has zero `allow_failure: true` rows. Verification: - Doc-only commit. CHANGELOG diff readable. - Cross-reviewed each surface claim against the actual code shipped in B1–B6.
Addresses the PR #2 review comments. Three blockers + five notable items + the documentation drift sweep. `docs/security/panic-sites.md` — five new rows (sites 9-13) for the private `std::sync::Mutex` `expect()` sites added by `WasiRuntime` (drive / spawn / tasks_pending) and `WasiLeg` (send_bytes / recv_bytes). Each row names the mutex, its scope, and the invariant that makes the poison case unrecoverable (mirrors the existing `EmbeddedRuntime` rows). `wasi.rs` added to the Unsafe Blocks table as the third opt-in alongside `udp_transport.rs` and `websocket.rs`. `core/src/transport/legs/wasi.rs` — rewrote the `unsafe impl Send / Sync for WasiLeg` SAFETY comment. The original argument leaned on "WASI is single-threaded today"; the new argument is that the internal `Mutex` wrappers are the only access path for the WIT resource handles after construction, so the single-accessor discipline holds even if a future `wasi-threads` proposal stabilizes. Also switched the internal import to the canonical `crate::transport::session_transport::SessionTransport` (was re-export via `crate::api::session`). `core/src/runtime/wasi_runtime.rs` — added a `WasiSleep` docstring that calls out its coupling to `WasiRuntime::drive`: the future ignores its `Context` and only progresses on the next `drive()` call, so the `poll_until_progress` watchdog timeout becomes the effective sleep granularity. Using `WasiSleep` under any other executor would deadlock. `core/tests/fixtures/wasi-guest/src/main.rs` — added a `PHANTOM_MODE=runtime` branch that drives `WasiLeg` via `WasiRuntime::spawn` + `drive` + `poll_until_progress`. Closes the review gap that the two new wasi primitives shipped without any joint integration coverage. Outcome bookkeeping via `Arc<AtomicU8>` with explicit exit codes (2 mismatch, 3 I/O error, 4 executor bug). `core/tests/wasi_integration.rs` — factored `run_guest_round_trip` helper that takes a `(mode, expected_marker)` pair; the existing B5 test calls it with `("", "OK: round-tripped")`, the new B7 `wasi_guest_round_trips_payload_via_runtime_through_wasmtime` test calls it with `("runtime", "OK: runtime-driven round-trip")`. Both now use `env!("CARGO")` so the spawned `cargo build` uses the same toolchain that compiled the test binary. `.github/workflows/cross.yml` — clarified the `wasi-integration` job header to call out that both integration tests run there. Also documented the dev-dep blocker that prevents the in-tree `#[cfg(test)]` unit tests in `wasi_runtime.rs` / `wasi.rs` from running under `cargo test --target wasm32-wasip2 --lib` (`proptest`'s `fork` feature pulls `rusty-fork` → `wait-timeout` which has no wasi `imp` module; `criterion` / `uniffi` test helpers have similar issues). Closing that gap is a separate dev- dep target-gating PR; the integration tests cover the wasi- specific behaviour today. `CHANGELOG.md` — fixed the placeholder commit-hash range (`9b31266..e41b583` → `f4828c2..307b43e`), and added an explicit "Breaking for `--no-default-features --features std` consumers" paragraph documenting the UniFFI feature split. `docs/operations/wasi.md` — same commit-hash fix as CHANGELOG. Replaced the broken `[lib] crate-type = ["cdylib"]` quickstart example (incompatible with the `fn main()` body shown below it) with a `[[bin]]` form that matches the wasi-guest fixture and actually works with `wasmtime run …wasm`. `docs/PROGRESS.md` — flipped the four "wasm32-wasi remains `allow_failure: true`" mentions to reflect the shipped state (Phase 3.5, 3.10, 3.11, Phase 3 verdict). Updated the dashboard counts at the top: unsafe opt-ins 1 → 3 (now correctly listing `udp_transport`, `websocket`, `wasi`), panic sites 6 → 13. Verification: `cargo check --lib` clean, `cargo clippy --lib` clean, `cargo test --lib` 233/233 passing, `cargo check --target wasm32-wasip2 --no-default-features --features std,wasi-leg --lib` clean, `cargo test --test wasi_integration -- --ignored` 2/2 passing (both B5 byte-pipe and the new B7 runtime+leg composition round-trips succeed under wasmtime 45.0.0).
snaart
added a commit
that referenced
this pull request
May 25, 2026
Addresses the PR #2 review comments. Three blockers + five notable items + the documentation drift sweep. `docs/security/panic-sites.md` — five new rows (sites 9-13) for the private `std::sync::Mutex` `expect()` sites added by `WasiRuntime` (drive / spawn / tasks_pending) and `WasiLeg` (send_bytes / recv_bytes). Each row names the mutex, its scope, and the invariant that makes the poison case unrecoverable (mirrors the existing `EmbeddedRuntime` rows). `wasi.rs` added to the Unsafe Blocks table as the third opt-in alongside `udp_transport.rs` and `websocket.rs`. `core/src/transport/legs/wasi.rs` — rewrote the `unsafe impl Send / Sync for WasiLeg` SAFETY comment. The original argument leaned on "WASI is single-threaded today"; the new argument is that the internal `Mutex` wrappers are the only access path for the WIT resource handles after construction, so the single-accessor discipline holds even if a future `wasi-threads` proposal stabilizes. Also switched the internal import to the canonical `crate::transport::session_transport::SessionTransport` (was re-export via `crate::api::session`). `core/src/runtime/wasi_runtime.rs` — added a `WasiSleep` docstring that calls out its coupling to `WasiRuntime::drive`: the future ignores its `Context` and only progresses on the next `drive()` call, so the `poll_until_progress` watchdog timeout becomes the effective sleep granularity. Using `WasiSleep` under any other executor would deadlock. `core/tests/fixtures/wasi-guest/src/main.rs` — added a `PHANTOM_MODE=runtime` branch that drives `WasiLeg` via `WasiRuntime::spawn` + `drive` + `poll_until_progress`. Closes the review gap that the two new wasi primitives shipped without any joint integration coverage. Outcome bookkeeping via `Arc<AtomicU8>` with explicit exit codes (2 mismatch, 3 I/O error, 4 executor bug). `core/tests/wasi_integration.rs` — factored `run_guest_round_trip` helper that takes a `(mode, expected_marker)` pair; the existing B5 test calls it with `("", "OK: round-tripped")`, the new B7 `wasi_guest_round_trips_payload_via_runtime_through_wasmtime` test calls it with `("runtime", "OK: runtime-driven round-trip")`. Both now use `env!("CARGO")` so the spawned `cargo build` uses the same toolchain that compiled the test binary. `.github/workflows/cross.yml` — clarified the `wasi-integration` job header to call out that both integration tests run there. Also documented the dev-dep blocker that prevents the in-tree `#[cfg(test)]` unit tests in `wasi_runtime.rs` / `wasi.rs` from running under `cargo test --target wasm32-wasip2 --lib` (`proptest`'s `fork` feature pulls `rusty-fork` → `wait-timeout` which has no wasi `imp` module; `criterion` / `uniffi` test helpers have similar issues). Closing that gap is a separate dev- dep target-gating PR; the integration tests cover the wasi- specific behaviour today. `CHANGELOG.md` — fixed the placeholder commit-hash range (`9b31266..e41b583` → `f4828c2..307b43e`), and added an explicit "Breaking for `--no-default-features --features std` consumers" paragraph documenting the UniFFI feature split. `docs/operations/wasi.md` — same commit-hash fix as CHANGELOG. Replaced the broken `[lib] crate-type = ["cdylib"]` quickstart example (incompatible with the `fn main()` body shown below it) with a `[[bin]]` form that matches the wasi-guest fixture and actually works with `wasmtime run …wasm`. `docs/PROGRESS.md` — flipped the four "wasm32-wasi remains `allow_failure: true`" mentions to reflect the shipped state (Phase 3.5, 3.10, 3.11, Phase 3 verdict). Updated the dashboard counts at the top: unsafe opt-ins 1 → 3 (now correctly listing `udp_transport`, `websocket`, `wasi`), panic sites 6 → 13. Verification: `cargo check --lib` clean, `cargo clippy --lib` clean, `cargo test --lib` 233/233 passing, `cargo check --target wasm32-wasip2 --no-default-features --features std,wasi-leg --lib` clean, `cargo test --test wasi_integration -- --ignored` 2/2 passing (both B5 byte-pipe and the new B7 runtime+leg composition round-trips succeed under wasmtime 45.0.0).
Resolve conflicts in CHANGELOG.md and docs/security/panic-sites.md by keeping both sides: - CHANGELOG [Unreleased]: retain both the wasi-leg "Added" block and the FIPS primitive-swap "Security" block, ahead of the Phase 8 section. - panic-sites.md: merge both new panic-site sets — FIPS sites stay 9-11, WASI runtime/leg sites renumbered 9-13 -> 12-16, with internal "Site N" cross-references fixed (9 -> 12, 12 -> 15). All non-doc files auto-merged cleanly; only the two doc files conflicted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Section B of
docs/superpowers/plans/2026-05-24-pre-1.0-deferred-followups-detailed.md—wasm32-wasip2is a hard CI gate. Adds thewasi-legCargo feature, exposingWasiLeg(TCPSessionTransportoverwasi:sockets/tcp) +WasiRuntime(single-task executor overwasi:io/poll+wasi:clocks) for embedders running Phantom Core inside a WASI Preview 2 host (Wasmtime, WasmEdge, Spin, wasmCloud, Cloudflare Workers WASI sandbox).Per-commit walkthrough (B1–B7):
build(wasi)—wasi = 0.14optional dep +wasi-legfeature + compile_error guard on wasm32-unknown-unknown.runtime(wasi)—WasiRuntimescaffold:Mutex<Vec<TaskSlot>>queue,drive()polls each task once,poll_until_progress(max_wait)blocks onwasi:io/pollwith asubscribe_durationwatchdog.transport(wasi)—WasiLeg::connect(SocketAddr)+ length-prefix-framedsend_bytes/recv_bytesviablocking_write_and_flush/blocking_read. Client-only; server-sideacceptdeferred per Decision Point 3.cfg(wasi)— re-exports + module gates + rustdoc cross-references.tests(wasi)— host integration test under wasmtime +bindingsCargo feature split (UniFFI scaffolding moved out ofstdso the WASI guest can drop it;wasm-component-ldcannot ingest UniFFI exports). Also narrowed the browser-WASM-only dep block fromcfg(target_arch = "wasm32")→cfg(all(target_arch = "wasm32", target_os = "unknown"))so WASI builds skipwasm-bindgen,web-sys,js-sys.ci(wasi)—cross.ymlflipswasm32-wasi(wasallow_failure: true) → hardwasm32-wasip2matrix entry + newwasi-integrationjob that installs wasmtime and runs the host driver. The 12-target matrix now has zeroallow_failure: truerows.docs(wasi)—wasi.mdrewritten as a quickstart;CHANGELOG.mdadds theAdded — wasi-leg Cargo featuresection.Wire-format / API surface
phantom_core::transport::legs::wasi::WasiLeg,phantom_core::runtime::wasi_runtime::WasiRuntime. Both gated oncfg(all(feature = "wasi-leg", target_os = "wasi")).bindings(default-on). Native FFI consumers (Swift / Kotlin / Python / C) unchanged. WASI guests opt out via--no-default-features --features std,wasi-leg.Test Plan
cargo test --lib— 233 tests pass (host default-features build, no regressions).cargo test --lib --no-default-features --features std— 231 tests pass (the 2 missing are uniffi-gated tests).cargo clippy --lib -- -D warnings— clean.cargo check --target wasm32-wasip2 --no-default-features --features std,wasi-leg --lib— clean.cargo build --manifest-path core/tests/fixtures/wasi-guest/Cargo.toml --target wasm32-wasip2— clean.cargo test --test wasi_integration -- --ignored— passes locally on wasmtime 45.0.0.cross.ymlwasm32-wasip2row +wasi-integrationjob) — to verify on PR.cross.ymlwasm32-unknown-unknown) — confirm browser surface unchanged.Out of scope (follow-up)
WasiLeg::accept/WasiListener(runningphantom-serveras a WASI guest — Decision Point 3 defers).PhantomSessionhandshake overWasiLeg. The B5 host test exercises the byte pipe; the handshake needsconnect_with_transport_with_runtimewiring againstWasiRuntime::drive+poll_until_progress, plus the host echo server has to become a realPhantomListener.Quickstart for embedders is in
docs/operations/wasi.md.