diff --git a/.editorconfig b/.editorconfig index ba5b634..c02692b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,3 +17,16 @@ max_line_length = off [*.dart] max_line_length = off + +[*.wit] +max_line_length = 300 + +# Binary file formats that should be ignored +[*.onnx] +charset = unset +end_of_line = unset +insert_final_newline = unset +indent_size = unset +indent_style = unset +trim_trailing_whitespace = unset +max_line_length = unset diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4360478..c0d7859 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install Mesa Vulkan drivers (lavapipe) + run: | + sudo apt-get update + sudo apt-get install -y mesa-vulkan-drivers + - name: Install mise uses: taiki-e/install-action@v2 with: @@ -32,6 +37,9 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} + - name: Fetch MNIST model + run: mise run fetch-mnist-rclone + - name: Build WASM modules run: mise run build-modules diff --git a/.gitignore b/.gitignore index 327c2cb..d0928c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.wasm +*.onnx target/ .DS_Store services/ws-wasm-agent/pkg/ diff --git a/.mise.toml b/.mise.toml index 0d10500..6b7f24d 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,5 +1,6 @@ [tools] action-validator = "latest" +"cargo:ast-grep" = "latest" cargo-binstall = "latest" "cargo:aube" = "latest" "cargo:taplo-cli" = "latest" @@ -21,22 +22,25 @@ gemini-cli = "latest" java = "latest" maven = "latest" mprocs = "latest" +node = "22" "npm:onnxruntime-web" = "latest" "npm:pyodide" = "0.29.3" ollama = "latest" osv-scanner = "latest" pipx = "latest" "pipx:cmake" = "latest" +"pipx:componentize-py" = "latest" "pipx:pytest" = "latest" protoc = "latest" rclone = "latest" ruff = "latest" rust = [ - { version = "latest", components = "clippy", targets = "wasm32-unknown-unknown" }, - { version = "nightly", components = "rust-src,rustfmt", targets = "wasm32-unknown-unknown" }, + { version = "latest", components = "clippy,rust-analyzer", targets = "wasm32-unknown-unknown,wasm32-wasip2" }, + { version = "nightly", components = "rust-src,rustfmt", targets = "wasm32-unknown-unknown,wasm32-wasip2" }, ] typos = "latest" uv = "0.11.8" +wasm-tools = "latest" yq = "latest" zig = "latest" @@ -77,6 +81,7 @@ run = "dart format services/ws-modules/dart-comm1/" [tasks.check] depends = [ + "ast-grep-check", "cargo-check", "cargo-clippy", "cargo-fmt-check", @@ -92,6 +97,10 @@ depends = [ ] description = "Run repository checks" +[tasks.ast-grep-check] +description = "Run ast-grep structural-search rules" +run = "ast-grep scan --error -c config/ast-grep/sgconfig.yml" + [tasks.cargo-check] run = "cargo check --workspace" @@ -102,10 +111,10 @@ run = "cargo +nightly fmt" run = "cargo +nightly fmt -- --check" [tasks.cargo-clippy] -run = "cargo clippy --workspace" +run = "cargo clippy --workspace --tests" [tasks.cargo-clippy-fix] -run = "cargo clippy --fix --allow-dirty --allow-staged --workspace" +run = "cargo clippy --fix --allow-dirty --allow-staged --workspace --tests" [tasks.taplo-fmt] run = "taplo format" @@ -139,6 +148,17 @@ run = "cargo run" OTLP_AUTH_PASSWORD = "1234" OTLP_AUTH_USERNAME = "root@example.com" +[tasks.ws-wasi-runner] +description = "Run the WebSocket WASI runner (set RUNNER_MODULE to a WASI module, e.g. wasi-graphics-info; set RUNNER_FEATURES=cuda on CUDA hosts)" +dir = "services/ws-wasi-runner" +run = """ +if [ -n "${RUNNER_FEATURES:-}" ]; then + cargo run --features "$RUNNER_FEATURES" +else + cargo run +fi +""" + [tasks.build-ws-wasm-agent] description = "Build the WebSocket WASM client" dir = "services/ws-wasm-agent" @@ -230,6 +250,48 @@ PUBLISH=bin/Release/net10.0/publish/wwwroot/_framework cp "$PUBLISH"/*.js "$PUBLISH"/*.wasm "$PUBLISH"/*.dat pkg/ ''' +[tasks.build-ws-wasi-graphics-info-module] +description = "Build the WASI graphics-info Python module as a WASI Preview 2 component" +dir = "services/ws-modules/wasi-graphics-info" +# WIT lives once in services/ws-wasi-runner/wit/ - the runner's `runner` +# world is what its bindgen consumes; the `module` world (which `include`s +# `runner` and adds wasi-nn) is what guest modules target. `mnist-12.onnx` +# is fetched into `pkg/` by `mise run download-models` (see +# `fetch-mnist-rclone`); componentize-py 0.23 doesn't bundle arbitrary +# data files inside the component image, so the guest fetches it via the +# `wasi:keyvalue/store` host import from the ws-server at runtime. +# componentize-py 0.23's `bindings` subcommand refuses to overwrite an +# existing output dir, so we wipe it first. +run = """ +rm -rf wit_world +componentize-py -d ../../ws-wasi-runner/wit -w module bindings . +componentize-py -d ../../ws-wasi-runner/wit -w module componentize wasi_graphics_info -o pkg/et_ws_wasi_graphics_info.wasm +cargo run -p et-cli -- module-package-json +""" + +[tasks.build-ws-wasi-data1-module] +description = "Build the Rust WASI data1 module as a WASI Preview 2 component" +# The crate is a regular workspace member but its lib body is gated on +# `#![cfg(target_os = "wasi")]`, so cargo check from the host target gets +# an empty cdylib while this task explicitly requests wasm32-wasip2. The +# .wasm artifact lands in the workspace-shared `target/` and is then +# copied into the module's pkg/ for et-modules-service to serve. +run = """ +cargo build --release -p et-ws-wasi-data1 --target wasm32-wasip2 +mkdir -p services/ws-modules/wasi-data1/pkg +cp target/wasm32-wasip2/release/et_ws_wasi_data1.wasm services/ws-modules/wasi-data1/pkg/et_ws_wasi_data1.wasm +cargo run -p et-cli -- module-package-json --module-dir services/ws-modules/wasi-data1 +""" + +[tasks.build-ws-wasi-comm1-module] +description = "Build the Rust WASI comm1 module as a WASI Preview 2 component" +run = """ +cargo build --release -p et-ws-wasi-comm1 --target wasm32-wasip2 +mkdir -p services/ws-modules/wasi-comm1/pkg +cp target/wasm32-wasip2/release/et_ws_wasi_comm1.wasm services/ws-modules/wasi-comm1/pkg/et_ws_wasi_comm1.wasm +cargo run -p et-cli -- module-package-json --module-dir services/ws-modules/wasi-comm1 +""" + [tasks.build-ws-pydata1-module] description = "Build the pydata1 Python workflow module" dir = "services/ws-modules/pydata1" @@ -273,6 +335,9 @@ depends = [ "build-ws-sensor1-module", "build-ws-speech-recognition-module", "build-ws-video1-module", + "build-ws-wasi-comm1-module", + "build-ws-wasi-data1-module", + "build-ws-wasi-graphics-info-module", "build-ws-wasm-agent", "build-ws-zig-data1-module", ] @@ -319,8 +384,20 @@ run = """ data/model-modules/model-har-motion1/pkg/har-motion1.onnx --http-url https://github.com --progress """ +[tasks.fetch-mnist-rclone] +# MNIST classifier used by services/ws-modules/wasi-graphics-info to exercise +# wasi-nn end-to-end. Sourced from the canonical ONNX Model Zoo; size 26143 +# bytes. Lands under the module's gitignored pkg/ so et-modules-service can +# serve it statically alongside the .wasm. +run = """ + rclone copyto \ + :http:onnx/models/raw/main/validated/vision/classification/mnist/model/mnist-12.onnx \ + services/ws-modules/wasi-graphics-info/pkg/mnist-12.onnx \ + --http-url https://github.com --progress +""" + [tasks.download-models] -depends = ["fetch-face1-rclone", "fetch-har-motion1-rclone"] +depends = ["fetch-face1-rclone", "fetch-har-motion1-rclone", "fetch-mnist-rclone"] [tasks.cargo-test] run = "cargo test --workspace" diff --git a/CLAUDE.md b/CLAUDE.md index 8505c38..16376d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,12 +67,60 @@ Languages: - **C# (.NET WASM)**: dotnet-data1 - **Java (TeaVM → JS)**: java-data1 - **Zig → WASM**: zig-data1 +- **Python (componentize-py → WASI Preview 2 component)**: wasi-graphics-info — runs in + `et-ws-wasi-runner` rather than the browser. The WIT world the component implements is at + `services/ws-wasi-runner/wit/world.wit` and is mirrored under the module's own `wit/`. + Drives two standardised WASI interfaces end-to-end: (1) `wasi:webgpu/webgpu` (trimmed subset + of WebAssembly/wasi-gfx) for a real 4x4 compute matmul through a host wgpu device, and + (2) `wasi:nn/{graph, tensor, inference}` for MNIST inference. Bundles `mnist-12.onnx` (served + from `pkg/` as a static asset; the guest fetches it via the `storage` host import because + componentize-py 0.23 doesn't bundle non-Python data files), then runs inference through ONNX + Runtime via `wasi:nn/graph.load` + `inference.compute` and verifies the predicted class. ### Libraries (`libs/`) - **edge-toolkit** — Common utilities, config, serialization (shared across services) - **web** — WASM web helpers (Canvas, MediaStream, WebSocket bindings for browser modules) +### WASI Runner (`services/ws-wasi-runner/`) + +`et-ws-wasi-runner` — runs ws-modules compiled to **WASI Preview 2 components** (rather than +browser WASM modules). It fetches the module's `pkg/package.json` from the ws-server, downloads +the `.wasm` named by the `wasi-main` field, instantiates it under `wasmtime` with async support, +and calls the exported `entry.run` function. + +Host imports (defined in `wit/world.wit`, package `et:ws-wasi@0.1.0`): + +- `log` — `log` and `set-status` for guest output +- `clock` — `sleep-ms`, `now-ms` +- `storage` — `put-file`/`get-file` proxied to the ws-server's storage service via reqwest +- `ws` — websocket client backed by `tokio-tungstenite`; mirrors the wire format of + `et-ws-wasm-agent` so events look the same on the server + +Plus, attached to the same Linker but defined by external WIT packages: + +- `wasi:webgpu/webgpu@0.0.1` — trimmed subset of WebAssembly/wasi-gfx, vendored under + `wit/deps/wasi-webgpu/`. Compute-only (render pipelines, textures, samplers, canvas/surface, + query sets, async pipeline creation are stripped; the trimmed surface is just what's needed + to run a compute pipeline through to a mappable readback buffer). The host impl in + `src/host/wasi_webgpu.rs` is wgpu-backed (Metal / Vulkan / DX12) for the matmul path; + every other kept method traps with `unimplemented!`. We carry this divergence from upstream + because wasi-gfx isn't published to crates.io — replace this whole tree with the upstream + WIT plus its matching host crate once it ships. +- `wasi:nn/{tensor, graph, inference, errors}` — standardised ML inference. The host wires + `wasmtime-wasi-nn` with the ONNX Runtime backend (`ort` 2.0.0-rc.10, pinned because rc.11+ + moved API surface that wasmtime-wasi-nn 44 still uses). Guests load model bytes via + `graph.load`, build `Tensor`s, and call `compute` — the same shape of calls Spin / wasmCloud + / Fermyon production wasi-nn workloads use. CUDA dispatch is opt-in via the runner's + `cuda` cargo feature (`cargo build -p et-ws-wasi-runner --features cuda` or + `RUNNER_FEATURES=cuda mise run ws-wasi-runner`); the default build is CPU-only because + Pyke's `ort` download-binaries CUDA prebuilt only exists for some triples (notably + Linux x86_64). CoreML-on-macOS would need a wasmtime-wasi-nn patch (its `onnx.rs` only + knows the CUDA provider for `ExecutionTarget::Gpu`). + +`RUNNER_MODULE` selects the module (e.g. `wasi-graphics-info`). `WS_SERVER_URL` defaults to +`ws://localhost:8080/ws`. + ### Utilities (`utilities/`) - **cli** (`et-cli`) — Scenario and module tooling. Reads scenario YAML and outputs `mise.toml` or `compose.yaml`; @@ -92,6 +140,10 @@ and must stay in sync — `mise run check` will fail if they drift. Regenerate w - WASM agent (nightly, MVP target): uses `RUSTFLAGS="-C target-cpu=mvp ..."` and `RUSTUP_TOOLCHAIN=nightly` - `har1` and `face-detection`: after wasm-pack, merge extra `package.json` fields with `yq` - Python modules: `uv build --wheel` then `cargo run -p et-cli -- module-package-json` +- WASI Python modules (`wasi-graphics-info`): `componentize-py -d wit -w module bindings .` then + `componentize-py -d wit -w module componentize -o pkg/.wasm` then + `cargo run -p et-cli -- module-package-json`. The `[tool.ws-module] wasi-main` field flows to + `package.json` so `et-ws-wasi-runner` knows which file to fetch. - Rust modules needing dependency injection: `cargo run -p et-cli -- module-package-json` merges `[package.metadata.ws-module.dependencies]` from `Cargo.toml` into `pkg/package.json` - `et-cli module-package-json` reads `pyproject.toml` (Python modules, via `[tool.ws-module]`) diff --git a/Cargo.lock b/Cargo.lock index 5fab8ad..cce5108 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,31 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec 1.15.1", + "tokio", + "tokio-util", +] + [[package]] name = "actix-codec" version = "0.5.2" @@ -61,7 +86,7 @@ dependencies = [ "derive_more", "encoding_rs", "flate2", - "foldhash", + "foldhash 0.1.5", "futures-core", "h2", "http 0.2.12", @@ -74,8 +99,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rand 0.10.1", - "sha1", - "smallvec", + "sha1 0.11.0", + "smallvec 1.15.1", "tokio", "tokio-util", "tracing", @@ -196,7 +221,7 @@ dependencies = [ "cookie", "derive_more", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "futures-util", "impl-more", @@ -211,7 +236,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "smallvec", + "smallvec 1.15.1", "socket2 0.6.3", "time", "tracing", @@ -244,6 +269,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "actix_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -274,6 +319,18 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -339,6 +396,27 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -351,7 +429,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 2.0.18", "time", ] @@ -429,12 +507,42 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.12.0" @@ -444,6 +552,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "brotli" version = "8.0.2" @@ -470,6 +587,29 @@ name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder" @@ -492,6 +632,84 @@ dependencies = [ "bytes", ] +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.1.4", + "smallvec 1.15.1", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" +dependencies = [ + "ambient-authority", + "rand 0.8.6", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.4", + "winx", +] + [[package]] name = "cast" version = "0.3.0" @@ -516,6 +734,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.10.0" @@ -590,6 +814,24 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -634,12 +876,31 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -659,138 +920,349 @@ dependencies = [ ] [[package]] -name = "crc32fast" -version = "1.5.0" +name = "cranelift-assembler-x64" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "f8628cc4ba7f88a9205a7ee42327697abc61195a1e3d92cfae172d6a946e722e" dependencies = [ - "cfg-if", + "cranelift-assembler-x64-meta", ] [[package]] -name = "crossterm" -version = "0.28.1" +name = "cranelift-assembler-x64-meta" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "d582754487e6c9a065a91c42ccf1bdd8d5977af33468dac5ae9bec0ce88acb3e" dependencies = [ - "bitflags", - "crossterm_winapi", - "parking_lot", - "rustix 0.38.44", - "winapi", + "cranelift-srcgen", ] [[package]] -name = "crossterm_winapi" -version = "0.9.1" +name = "cranelift-bforest" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +checksum = "fb59c81ace12ee7c33074db7903d4d75d1f40b28cd3e8e6f491de57b29129eb9" dependencies = [ - "winapi", + "cranelift-entity", + "wasmtime-internal-core", ] [[package]] -name = "crypto-common" -version = "0.2.1" +name = "cranelift-bitset" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "f25c06993a681be9cf3140798a3d4ac5bec955e7444416a2fdc87fda8567285d" dependencies = [ - "hybrid-array", + "serde", + "serde_derive", + "wasmtime-internal-core", ] [[package]] -name = "darling" -version = "0.20.11" +name = "cranelift-codegen" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "27b61f95c5a211918f5d336254a61a488b36a5818de47a868e8c4658dce9cccc" dependencies = [ - "darling_core", - "darling_macro", + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.16.1", + "libm", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash 2.1.2", + "serde", + "smallvec 1.15.1", + "target-lexicon", + "wasmtime-internal-core", ] [[package]] -name = "darling_core" -version = "0.20.11" +name = "cranelift-codegen-meta" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "0b85aa822fce72080d041d7c2cf7c3f5c6ecdea7afae68379ba4ef85269c4fa5" dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", ] [[package]] -name = "darling_macro" -version = "0.20.11" +name = "cranelift-codegen-shared" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "833eb9fc89326cd072cc19e96892f09b5692c0dfe17cd4da2858ba30c2cd85c0" + +[[package]] +name = "cranelift-control" +version = "0.131.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d005320f487e6e8a3edcc7f2fd4f43fcc9946d1013bf206ea649789ac1617fc" dependencies = [ - "darling_core", - "quote", - "syn", + "arbitrary", ] [[package]] -name = "data-encoding" -version = "2.11.0" +name = "cranelift-entity" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "5e62ef34c6e720f347a79ece043e8584e242d168911da640bac654a33a6aaaf5" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] [[package]] -name = "der-parser" -version = "10.0.0" +name = "cranelift-frontend" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +checksum = "dfa2ad00399dd47e7e7e33cb1dc23b0e39ed9dcd01e8f026fc37af91655031b8" dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", + "cranelift-codegen", + "log", + "smallvec 1.15.1", + "target-lexicon", ] [[package]] -name = "deranged" -version = "0.5.8" +name = "cranelift-isle" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "02c51975ed217b4e8e5a7fd11e9ec83a96104bdff311dddcb505d1d8a9fd7fc6" + +[[package]] +name = "cranelift-native" +version = "0.131.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b1889e00da9729d8f8525f3c12998ded86ea709058ff844ebe00b97548de0e" dependencies = [ - "powerfmt", + "cranelift-codegen", + "libc", + "target-lexicon", ] [[package]] -name = "derive_builder" -version = "0.20.2" +name = "cranelift-srcgen" +version = "0.131.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +checksum = "d5a8f82fd5124f009f72167e60139245cd3b56cfd4b53050f22110c48c5f4da1" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "derive_builder_macro", + "cfg-if", ] [[package]] -name = "derive_builder_core" -version = "0.20.2" +name = "crossbeam-channel" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", + "crossbeam-utils", ] [[package]] -name = "derive_builder_macro" -version = "0.20.2" +name = "crossbeam-deque" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "derive_builder_core", - "syn", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "parking_lot", + "rustix 0.38.44", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", ] [[package]] @@ -816,15 +1288,56 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer", + "block-buffer 0.12.0", "const-oid", - "crypto-common", + "crypto-common 0.2.1", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", ] [[package]] @@ -838,6 +1351,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dunce" version = "1.0.5" @@ -867,6 +1389,18 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -903,7 +1437,7 @@ dependencies = [ "serde_json", "serde_yaml", "tempfile", - "toml", + "toml 0.8.23", ] [[package]] @@ -929,6 +1463,23 @@ dependencies = [ "onnx-extractor", ] +[[package]] +name = "et-otlp" +version = "0.1.0" +dependencies = [ + "edge-toolkit", + "hostname", + "log", + "opentelemetry", + "opentelemetry-appender-tracing", + "opentelemetry-otlp", + "opentelemetry_sdk", + "tracing", + "tracing-log", + "tracing-opentelemetry", + "tracing-subscriber", +] + [[package]] name = "et-storage-service" version = "0.1.0" @@ -1140,16 +1691,12 @@ dependencies = [ "clap", "edge-toolkit", "et-modules-service", + "et-otlp", "et-storage-service", "et-ws-service", "futures-util", - "hostname", "local-ip-address", "log", - "opentelemetry", - "opentelemetry-appender-tracing", - "opentelemetry-otlp", - "opentelemetry_sdk", "qr2term", "rcgen", "regex", @@ -1164,8 +1711,6 @@ dependencies = [ "tokio", "tracing", "tracing-actix-web", - "tracing-log", - "tracing-opentelemetry", "tracing-subscriber", "uuid", ] @@ -1205,6 +1750,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "et-ws-test-server" +version = "0.1.0" +dependencies = [ + "actix", + "actix-rt", + "actix-web", + "et-modules-service", + "et-storage-service", + "et-ws-service", + "tempfile", + "tracing-actix-web", +] + [[package]] name = "et-ws-video1" version = "0.1.0" @@ -1224,33 +1783,108 @@ dependencies = [ ] [[package]] -name = "et-ws-wasm-agent" +name = "et-ws-wasi-comm1" version = "0.1.0" dependencies = [ - "chrono", - "edge-toolkit", - "js-sys", - "serde", "serde_json", - "tracing", - "tracing-wasm", - "wasm-bindgen", - "wasm-bindgen-test", - "web-sys", + "wit-bindgen 0.57.1", ] [[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +name = "et-ws-wasi-data1" +version = "0.1.0" +dependencies = [ + "serde_json", + "wit-bindgen 0.57.1", +] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "et-ws-wasi-runner" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytemuck", + "edge-toolkit", + "et-otlp", + "et-ws-test-server", + "futures-util", + "opentelemetry", + "opentelemetry-http", + "ort", + "otlp-mock", + "pollster", + "reqwest", + "rstest", + "serde", + "serde-env", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "wasmtime", + "wasmtime-wasi", + "wasmtime-wasi-nn", + "wgpu", +] + +[[package]] +name = "et-ws-wasm-agent" +version = "0.1.0" +dependencies = [ + "chrono", + "edge-toolkit", + "js-sys", + "serde", + "serde_json", + "tracing", + "tracing-wasm", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "filetime" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5b2eef6fafbf69f877e55509ce5b11a760690ac9700a2921be067aa6afaef6" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fixedbitset" version = "0.5.7" @@ -1279,6 +1913,27 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1288,12 +1943,37 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -1362,6 +2042,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1372,6 +2053,30 @@ dependencies = [ "slab", ] +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash 2.1.2", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1379,8 +2084,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1390,9 +2097,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1421,12 +2130,58 @@ dependencies = [ "syn", ] +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.18", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags", +] + [[package]] name = "h2" version = "0.3.27" @@ -1446,13 +2201,38 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -1460,6 +2240,9 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -1467,6 +2250,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hostname" version = "0.4.2" @@ -1564,11 +2353,27 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "smallvec", + "smallvec 1.15.1", "tokio", "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.0", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1653,7 +2458,7 @@ dependencies = [ "icu_normalizer_data", "icu_properties", "icu_provider", - "smallvec", + "smallvec 1.15.1", "zerovec", ] @@ -1717,7 +2522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", - "smallvec", + "smallvec 1.15.1", "utf8_iter", ] @@ -1749,6 +2554,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + [[package]] name = "ipnet" version = "2.12.0" @@ -1776,6 +2597,26 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1810,6 +2651,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1828,12 +2675,31 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1852,6 +2718,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "local-channel" version = "0.1.5" @@ -1895,6 +2767,32 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "macro-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1904,6 +2802,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.8.0" @@ -1911,8 +2815,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "memmap2" -version = "0.9.10" +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + +[[package]] +name = "memmap2" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ @@ -1985,6 +2898,49 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" +[[package]] +name = "naga" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.18", + "unicode-ident", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "neli" version = "0.7.4" @@ -2068,6 +3024,80 @@ dependencies = [ "libm", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -2106,6 +3136,49 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openssl" +version = "0.10.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.31.0" @@ -2116,7 +3189,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror", + "thiserror 2.0.18", "tracing", ] @@ -2159,7 +3232,7 @@ dependencies = [ "prost", "reqwest", "serde_json", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -2191,7 +3264,48 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand 0.9.4", - "thiserror", + "thiserror 2.0.18", +] + +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ort" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa7e49bd669d32d7bc2a15ec540a527e7764aec722a45467814005725bcd721" +dependencies = [ + "ort-sys", + "smallvec 2.0.0-alpha.10", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2aba9f5c7c479925205799216e7e5d07cc1d4fa76ea8058c60a9a30f6a4e890" +dependencies = [ + "flate2", + "pkg-config", + "sha2", + "tar", + "ureq", +] + +[[package]] +name = "otlp-mock" +version = "0.1.0" +dependencies = [ + "actix-rt", + "actix-web", + "serde_json", ] [[package]] @@ -2213,7 +3327,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec", + "smallvec 1.15.1", "windows-link", ] @@ -2227,19 +3341,38 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset 0.4.2", + "indexmap", +] + [[package]] name = "petgraph" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset", + "fixedbitset 0.5.7", "hashbrown 0.15.5", "indexmap", ] @@ -2276,6 +3409,39 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2300,6 +3466,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "prettyplease" version = "0.2.37" @@ -2350,6 +3522,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + [[package]] name = "proptest" version = "1.11.0" @@ -2359,7 +3537,7 @@ dependencies = [ "bitflags", "num-traits", "rand 0.9.4", - "rand_chacha", + "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", "unarray", @@ -2385,7 +3563,7 @@ dependencies = [ "itertools", "log", "multimap", - "petgraph", + "petgraph 0.8.3", "prettyplease", "prost", "prost-types", @@ -2416,6 +3594,29 @@ dependencies = [ "prost", ] +[[package]] +name = "pulley-interpreter" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9326e3a0093d170582cf64ed9e4cf253b8aac155ec4a294ff62330450bbf094" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c6433917e3789605b1f4cd2a589f637ff17212344e7fa5ba99544625ba52c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "qr2term" version = "0.3.3" @@ -2432,6 +3633,61 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.2", + "rustls", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash 2.1.2", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -2453,13 +3709,24 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] @@ -2474,6 +3741,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -2484,6 +3761,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "rand_core" version = "0.9.5" @@ -2509,21 +3795,65 @@ dependencies = [ ] [[package]] -name = "rcgen" -version = "0.14.7" +name = "range-alloc" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "x509-parser", - "yasna", -] +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" [[package]] -name = "redox_syscall" +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" @@ -2531,6 +3861,31 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash 2.1.2", + "smallvec 1.15.1", +] + [[package]] name = "regex" version = "1.12.3" @@ -2572,6 +3927,12 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "reqwest" version = "0.12.28" @@ -2587,23 +3948,31 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", + "webpki-roots", ] [[package]] @@ -2649,6 +4018,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2693,6 +4080,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + [[package]] name = "rustls" version = "0.23.40" @@ -2702,6 +4099,7 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2714,6 +4112,7 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ + "web-time", "zeroize", ] @@ -2750,6 +4149,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2766,11 +4174,38 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2867,6 +4302,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2892,6 +4336,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha1" version = "0.11.0" @@ -2900,7 +4355,18 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -2945,6 +4411,15 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "smallvec" +version = "2.0.0-alpha.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d44cfb396c3caf6fbfd0ab422af02631b69ddd96d2eff0b0f0724f9024051b" [[package]] name = "socket2" @@ -2966,12 +4441,38 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -3015,6 +4516,39 @@ dependencies = [ "syn", ] +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -3028,13 +4562,42 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3098,6 +4661,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.52.3" @@ -3147,6 +4725,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3167,11 +4757,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_edit 0.22.27", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -3181,6 +4786,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -3198,7 +4812,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", "winnow 0.7.15", @@ -3231,6 +4845,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tonic" version = "0.14.6" @@ -3375,7 +4995,7 @@ checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" dependencies = [ "js-sys", "opentelemetry", - "smallvec", + "smallvec 1.15.1", "tracing", "tracing-core", "tracing-log", @@ -3394,7 +5014,7 @@ dependencies = [ "once_cell", "regex-automata", "sharded-slab", - "smallvec", + "smallvec 1.15.1", "thread_local", "tracing", "tracing-core", @@ -3418,6 +5038,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.8.6", + "sha1 0.10.6", + "thiserror 1.0.69", + "utf-8", +] + [[package]] name = "typenum" version = "1.20.0" @@ -3448,6 +5086,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -3467,22 +5111,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] -name = "url" -version = "2.5.8" +name = "ureq" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ - "form_urlencoded", - "idna", + "base64", + "der", + "log", + "native-tls", "percent-encoding", - "serde", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf8-zero", + "webpki-root-certs", ] [[package]] -name = "utf8_iter" -version = "1.0.4" +name = "ureq-proto" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http 1.4.0", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" @@ -3514,6 +5200,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -3641,74 +5333,737 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.71" +name = "wasm-bindgen-test-macro" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" + +[[package]] +name = "wasm-compose" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05a2b3bad87cc1ce45b63425ec09a854cc4cb369231c9fed1fee31538103efb" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "log", + "petgraph 0.6.5", + "smallvec 1.15.1", + "wasm-encoder 0.246.2", + "wasmparser 0.246.2", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" +dependencies = [ + "leb128fmt", + "wasmparser 0.246.2", +] + +[[package]] +name = "wasm-encoder" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +dependencies = [ + "leb128fmt", + "wasmparser 0.247.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac92cf547bc18d27ecc521015c08c353b4f18b84ab388bb6d1b6b682c620d9b6" +dependencies = [ + "leb128fmt", + "wasmparser 0.248.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.247.0", + "wasmparser 0.247.0", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +dependencies = [ + "bitflags", + "hashbrown 0.16.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e41f7493ba994b8a779430a4c25ff550fd5a40d291693af43a6ef48688f00e3" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.246.2", +] + +[[package]] +name = "wasmtime" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372db8bbad8ec962038101f75ab2c3ffcd18797d7d3ae877a58ab9873cd0c4bd" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec 1.15.1", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.246.2", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e15aa0d1545e48d9b25ca604e9e27b4cd6d5886d30ac5787b57b3a2daf85b57" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.16.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec 1.15.1", + "target-lexicon", + "wasm-encoder 0.246.2", + "wasmparser 0.246.2", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5441170843ac2ab28a1d7646b04a93a46d63bd4083274fd246c6a80189b37767" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix 1.1.4", + "serde", + "serde_derive", + "sha2", + "toml 0.9.12+spec-1.1.0", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c136cb0d2d47850d6d04a58157130ac98b0df4c17626cd30b083d26b607b7027" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.246.2", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df3d3b4fa2119c6fd161e475b4e21aaefb51d082353b922b433bea37facc65" + +[[package]] +name = "wasmtime-internal-core" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2c7fa6523647262bfb4095dbdf4087accefe525813e783f81a0c682f418ce4" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98c032f422e39061dfc43f32190c0a3526b04161ec4867f362958f3fe9d1fe29" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec 1.15.1", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8dd76d80adf450cc260ba58f23c28030401930b19149695b1d121f7d621e791" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab453cc600b28ee5d3f9495aa6d4cb2c81eda40903e9287296b548fba8b2391d" +dependencies = [ + "cc", + "object", + "rustix 1.1.4", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1859e920871515d324fb9757c3e448d6ed1512ca6ccdff14b6e016505d6ada" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dfe405bd6adb1386d935a30f16a236bd4ef0d3c383e7cbbab98d063c9d9b73" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a9b9165fc45d42c81edfe3e9cb458e58720594ad5db6553c4079ea041a4a581" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f439b70ba3855a8c808d2cd798eef79bcd389f78aa48a8a694ea8e2904410c" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object", + "target-lexicon", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c7ced16dc16d2027f9f8d3a503e191dcce0f53fe9218e7990135b31f8f6fdb" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser 0.246.2", +] + +[[package]] +name = "wasmtime-wasi" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3d57dd833d0c3ea2016a2aa54c6c517bf8dad9e79d8a593b0252c12bc961e3" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", + "system-interface", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6650bb4c61012b2221e751b7bc1162c7fd11bd1bc29e0714ad6ca463777a3422" +dependencies = [ + "async-trait", + "bytes", + "futures", + "tracing", + "wasmtime", +] + +[[package]] +name = "wasmtime-wasi-nn" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0f926060e466f55182fc771bc1003970e7ae129f53d9cfe57345fd782faaa1" +dependencies = [ + "ort", + "thiserror 2.0.18", + "tracing", + "walkdir", + "wasmtime", + "wiggle", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "248.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc54622ed5a5cddafcdf152043f9d4aed54d4a653d686b7dfe874809fca99d7" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.248.0", +] + +[[package]] +name = "wat" +version = "1.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75cd9e510603909748e6ebab89f27cd04472c1d9d85a3c88a7a6fc51a1a7934" +dependencies = [ + "wast 248.0.0", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wgpu" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb3feacc458f7bee8bc1737149b42b6c731aa461039a4264a67bb6681646b250" +dependencies = [ + "arrayvec", + "bitflags", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "log", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec 1.15.1", + "static_assertions", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02da3ad1b568337f25513b317870960ef87073ea0945502e44b864b67a8c77b7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec 1.15.1", + "thiserror 2.0.18", + "wgpu-core-deps-apple", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" +checksum = "62e51b5447e144b3dbba4feb01f80f4fa21696fa0cd99afb2c3df1affd6fdb28" dependencies = [ - "proc-macro2", - "quote", - "syn", + "wgpu-hal", ] [[package]] -name = "wasm-bindgen-test-shared" -version = "0.2.121" +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" +checksum = "1bfb01076d0aa08b0ba9bd741e178b5cc440f5abe99d9581323a4c8b5d1a1916" +dependencies = [ + "wgpu-hal", +] [[package]] -name = "wasm-encoder" -version = "0.244.0" +name = "wgpu-hal" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "31f8e1a9e7a8512f276f7c62e018c7fa8d60954303fed2e5750114332049193f" dependencies = [ - "leb128fmt", - "wasmparser", + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags", + "block2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "libc", + "libloading", + "log", + "naga", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", + "objc2-quartz-core", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec 1.15.1", + "thiserror 2.0.18", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "wgpu-naga-bridge" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +checksum = "59c654c483f058800972c3645e95388a7eca31bf9fe1933bc20e036588a0be02" dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", + "naga", + "wgpu-types", ] [[package]] -name = "wasmparser" -version = "0.244.0" +name = "wgpu-types" +version = "29.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "a9bcc31518a0e9735aefebedb5f7a9ef3ed1c42549c9f4c882fa9060ceaac639" dependencies = [ "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", + "bytemuck", + "log", + "raw-window-handle", ] [[package]] -name = "web-sys" -version = "0.3.98" +name = "wiggle" +version = "44.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +checksum = "7f878b066ad36054ad6e7724230f28ea7f981f44e595e39946d5225fd9e87755" dependencies = [ - "js-sys", - "wasm-bindgen", + "bitflags", + "thiserror 2.0.18", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", ] [[package]] -name = "web-time" -version = "1.1.0" +name = "wiggle-generate" +version = "44.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "f57f0bc709dacc9c69869006457ab4e1bc9d93695400f06224f33cbe8af81778" dependencies = [ - "js-sys", - "wasm-bindgen", + "heck", + "proc-macro2", + "quote", + "syn", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63976fe41647f7c55c680b88a7b9b68aae9184f5a6b4a0971bf3eb39c287467f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wiggle-generate", ] [[package]] @@ -3742,6 +6097,46 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "44.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da7c536f3cfe5ff63537f795902fed56b8b5adcc7a87843a86dd8d4e57a7946" +dependencies = [ + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec 1.15.1", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.246.2", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -3755,6 +6150,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -3783,6 +6189,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -3807,7 +6223,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3816,7 +6232,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -3834,14 +6259,40 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", ] [[package]] @@ -3850,48 +6301,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.15" @@ -3910,13 +6409,23 @@ dependencies = [ "memchr", ] +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "wit-bindgen-rust-macro", + "wit-bindgen-rust-macro 0.51.0", ] [[package]] @@ -3924,6 +6433,10 @@ name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro 0.57.1", +] [[package]] name = "wit-bindgen-core" @@ -3933,7 +6446,18 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.247.0", ] [[package]] @@ -3947,9 +6471,25 @@ dependencies = [ "indexmap", "prettyplease", "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "wasm-metadata 0.244.0", + "wit-bindgen-core 0.51.0", + "wit-component 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata 0.247.0", + "wit-bindgen-core 0.57.1", + "wit-component 0.247.0", ] [[package]] @@ -3963,8 +6503,24 @@ dependencies = [ "proc-macro2", "quote", "syn", - "wit-bindgen-core", - "wit-bindgen-rust", + "wit-bindgen-core 0.51.0", + "wit-bindgen-rust 0.51.0", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core 0.57.1", + "wit-bindgen-rust 0.57.1", ] [[package]] @@ -3980,10 +6536,29 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.244.0", + "wasm-metadata 0.244.0", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-component" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.247.0", + "wasm-metadata 0.247.0", + "wasmparser 0.247.0", + "wit-parser 0.247.0", ] [[package]] @@ -4001,7 +6576,57 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.246.2", +] + +[[package]] +name = "wit-parser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.247.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", ] [[package]] @@ -4024,10 +6649,20 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror", + "thiserror 2.0.18", "time", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + [[package]] name = "yasna" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 8fa54a9..4258e6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ rust-version = "1.87.0" [workspace] members = [ "libs/edge-toolkit", + "libs/et-otlp", + "libs/otlp-mock", "libs/web", "services/ws-modules/audio1", "services/ws-modules/bluetooth", @@ -20,33 +22,52 @@ members = [ "services/ws-modules/sensor1", "services/ws-modules/speech-recognition", "services/ws-modules/video1", + "services/ws-modules/wasi-comm1", + "services/ws-modules/wasi-data1", "services/modules", "services/storage", "services/ws", "services/ws-server", "services/ws-wasm-agent", + "services/ws-wasi-runner", + "services/ws-test-server", "utilities/cli", "utilities/onnx", ] resolver = "2" [workspace.dependencies] +actix = "0.13" +actix-rt = "2" +actix-web = { version = "4", features = ["rustls-0_23"] } anyhow = "1.0" base64 = "0.22.1" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.4", features = ["derive"] } +edge-toolkit = { path = "libs/edge-toolkit" } +et-otlp = { path = "libs/et-otlp" } et-web = { path = "libs/web" } lets_find_up = "0.0.4" log = "0.4" onnx-extractor = "0.3" +opentelemetry = "0.31" rstest = "0.26" secrecy = { version = "0.10.3", features = ["serde"] } -serde = { version = "1", features = ["derive"] } +serde = { version = "1.0.228", features = ["derive"] } serde-env = "0.3" serde-inline-default = "1.0" serde_default = "0.2" serde_json = "1" serde_yaml = "0.9" +tempfile = "3" +thiserror = "2" toml = "0.8" tracing = "0.1" +tracing-actix-web = { version = "0.7", default-features = false, features = [ + "emit_event_on_error", + "opentelemetry_0_31", + "uuid_v7", +] } +tracing-opentelemetry = "0.32" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1", features = ["serde", "v4", "v7"] } diff --git a/README.md b/README.md index 43e84a7..959a58f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ mise settings set cargo.binstall true The mise configuration is stored in [`.mise.toml`](.mise.toml). +Before installing dependencies, please install openssl development files. + +```bash +mise install +``` + ## Contributing Use `mise run fmt` and `mise run check` to run formatters and checkers. diff --git a/config/ast-grep/rules/no-rust-line-continuation.yml b/config/ast-grep/rules/no-rust-line-continuation.yml new file mode 100644 index 0000000..a347e71 --- /dev/null +++ b/config/ast-grep/rules/no-rust-line-continuation.yml @@ -0,0 +1,9 @@ +id: no-rust-line-continuation +language: Rust +severity: error +message: | + Trailing backslash line-continuation is forbidden in Rust string literals. + Use `concat!("a", "b")` or a single long line instead. +rule: + kind: string_literal + regex: '\\\n' diff --git a/config/ast-grep/sgconfig.yml b/config/ast-grep/sgconfig.yml new file mode 100644 index 0000000..2dd8e53 --- /dev/null +++ b/config/ast-grep/sgconfig.yml @@ -0,0 +1,2 @@ +ruleDirs: + - rules diff --git a/libs/et-otlp/Cargo.toml b/libs/et-otlp/Cargo.toml new file mode 100644 index 0000000..c49ba71 --- /dev/null +++ b/libs/et-otlp/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "et-otlp" +description = "Shared OTLP tracing setup for edge-toolkit services" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +edge-toolkit.workspace = true +hostname = "0.4" +log.workspace = true +opentelemetry.workspace = true +opentelemetry-appender-tracing = "0.31" +opentelemetry-otlp = { version = "0.31", default-features = false, features = [ + "http-json", + "http-proto", + "logs", + "reqwest-blocking-client", + "trace", +] } +opentelemetry_sdk = "0.31" +tracing.workspace = true +tracing-log = "0.2" +tracing-opentelemetry.workspace = true +tracing-subscriber.workspace = true diff --git a/libs/et-otlp/src/lib.rs b/libs/et-otlp/src/lib.rs new file mode 100644 index 0000000..69521c7 --- /dev/null +++ b/libs/et-otlp/src/lib.rs @@ -0,0 +1,123 @@ +//! Shared OpenTelemetry / OTLP setup for edge-toolkit services. +//! +//! Wires up: +//! - The W3C tracecontext propagator (so `traceparent` headers cross +//! process boundaries on HTTP). +//! - An OTLP/HTTP span exporter (binary or JSON, per `OtlpConfig`). +//! - An OTLP/HTTP log exporter, exposed through `tracing` so `info!` and +//! friends are forwarded. +//! - A `tracing` subscriber that fans `info!`/`error!` out to stdout *and* +//! the OTel pipeline. +//! +//! Returns an `OtelHandles` which the caller must `shutdown()` before exit +//! so batched spans/logs are flushed — otherwise short-lived processes +//! (e.g. the wasi-runner, which exits as soon as a module finishes) drop +//! their tail-end spans. + +use edge_toolkit::config::{OtlpConfig, OtlpProtocol}; +use opentelemetry::{KeyValue, trace::TracerProvider as _}; +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; +use opentelemetry_otlp::{LogExporter, WithExportConfig, WithHttpConfig}; +use opentelemetry_sdk::logs::SdkLoggerProvider; +use opentelemetry_sdk::trace::SdkTracerProvider; +use opentelemetry_sdk::{Resource, propagation::TraceContextPropagator}; +use tracing::subscriber::set_global_default; +use tracing_opentelemetry::OpenTelemetryLayer; +use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt}; + +pub const RUST_LOG: &str = "RUST_LOG"; + +/// Handles for the spans + logs pipelines. Drop alone won't flush — call +/// [`OtelHandles::shutdown`] at the end of `main()` (or in a Drop guard). +pub struct OtelHandles { + pub tracer_provider: SdkTracerProvider, + pub logger_provider: SdkLoggerProvider, +} + +impl OtelHandles { + /// Flush any buffered spans/logs and tear down the exporters. + pub fn shutdown(self) { + // Errors here are non-fatal — the process is exiting anyway. + let _ = self.tracer_provider.shutdown(); + let _ = self.logger_provider.shutdown(); + } +} + +/// Initialise the global tracing subscriber + OTel pipeline against +/// `config`. Call exactly once per process; subsequent calls panic via +/// `set_global_default`. +pub fn init(config: &OtlpConfig) -> OtelHandles { + // tracing_log forwards `log` crate records (used by transitive deps) + // through the tracing subscriber. + let _ = tracing_log::LogTracer::init(); + + let mut headers = std::collections::HashMap::new(); + if let Some(auth) = &config.auth { + auth.add_basic_auth_header(&mut headers); + } + + opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new()); + + let trace_endpoint = format!("{}/traces", config.collector_url); + let log_endpoint = format!("{}/logs", config.collector_url); + let protocol = match config.protocol { + OtlpProtocol::Binary => opentelemetry_otlp::Protocol::HttpBinary, + OtlpProtocol::JSON => opentelemetry_otlp::Protocol::HttpJson, + }; + + let span_exporter = opentelemetry_otlp::SpanExporter::builder() + .with_http() + .with_protocol(protocol) + .with_endpoint(trace_endpoint) + .with_headers(headers.clone()) + .build() + .expect("build OTLP span exporter"); + + let mut service_descriptors = vec![KeyValue::new("service.version", env!("CARGO_PKG_VERSION").to_string())]; + if let Some(hostname) = hostname::get().ok().and_then(|h| h.into_string().ok()) { + service_descriptors.push(KeyValue::new("service.instance", hostname)); + } + let resource = Resource::builder() + .with_service_name(config.service_label.clone()) + .with_attributes(service_descriptors) + .build(); + + let tracer_provider = SdkTracerProvider::builder() + .with_batch_exporter(span_exporter) + .with_resource(resource.clone()) + .build(); + + let otel_tracing_layer = OpenTelemetryLayer::new(tracer_provider.tracer(config.service_label.clone())); + + let log_directives = std::env::var(RUST_LOG).unwrap_or_else(|_| "info".to_string()); + let env_filter = EnvFilter::try_new(log_directives).expect("valid RUST_LOG"); + + let log_exporter = LogExporter::builder() + .with_http() + .with_protocol(protocol) + .with_endpoint(log_endpoint) + .with_headers(headers) + .build() + .expect("build OTLP log exporter"); + + let logger_provider = SdkLoggerProvider::builder() + .with_batch_exporter(log_exporter) + .with_resource(resource) + .build(); + + let otel_log_layer = OpenTelemetryTracingBridge::new(&logger_provider); + let stdout_fmt_layer = tracing_subscriber::fmt::layer().event_format(tracing_subscriber::fmt::format().compact()); + + let subscriber = Registry::default() + .with(env_filter) + .with(stdout_fmt_layer) + .with(otel_tracing_layer) + .with(otel_log_layer); + + set_global_default(subscriber).expect("set tracing subscriber"); + + OtelHandles { + tracer_provider, + logger_provider, + } +} diff --git a/libs/otlp-mock/Cargo.toml b/libs/otlp-mock/Cargo.toml new file mode 100644 index 0000000..fa0993c --- /dev/null +++ b/libs/otlp-mock/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "otlp-mock" +description = "In-process mock OTLP/HTTP-JSON collector for trace-propagation tests" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +actix-rt.workspace = true +actix-web.workspace = true +serde_json.workspace = true diff --git a/libs/otlp-mock/src/lib.rs b/libs/otlp-mock/src/lib.rs new file mode 100644 index 0000000..bf396b0 --- /dev/null +++ b/libs/otlp-mock/src/lib.rs @@ -0,0 +1,182 @@ +//! In-process mock OTLP/HTTP-JSON collector. +//! +//! Used by integration tests to verify trace-context propagation across +//! processes: both the system-under-test (ws-server, ws-wasi-runner, ...) +//! point their OTLP exporters at this collector, then the test reads the +//! captured spans back to assert that trace ids match. +//! +//! This is **not** a real OTLP implementation — it just buffers JSON +//! payloads. The endpoints match the URL shape `et-otlp::init` produces: +//! +//! - `POST /traces` +//! - `POST /logs` +//! +//! so tests should set `OTLP_COLLECTOR_URL=` and +//! `OTLP_PROTOCOL=JSON`. + +use std::net::TcpListener; +use std::sync::{Arc, Mutex}; + +use actix_web::{App, HttpResponse, HttpServer, post, web}; +use serde_json::Value; + +#[derive(Default)] +struct Captured { + traces: Mutex>, + logs: Mutex>, +} + +/// Handle to a running mock collector. The server is shut down when this +/// struct is dropped (the actix runtime is owned by the spawned thread — +/// when our struct goes out of scope, the spawned thread's tokio runtime +/// stays alive but the handle pointing at it is dropped, which is fine for +/// test scope). +pub struct OtlpMock { + /// Pass this to `OTLP_COLLECTOR_URL` in env so OTLP exporters target + /// the mock. Trace endpoint is `/traces`; logs is + /// `/logs` — matches `et_otlp::init`'s URL convention. + pub collector_url: String, + captured: Arc, +} + +impl OtlpMock { + /// Snapshot the trace payloads received so far. Each element is one + /// `ExportTraceServiceRequest` body (top-level shape: + /// `{ "resourceSpans": [...] }`). + #[must_use] + pub fn traces(&self) -> Vec { + self.captured.traces.lock().unwrap().clone() + } + + /// Snapshot the log payloads received so far. + #[must_use] + pub fn logs(&self) -> Vec { + self.captured.logs.lock().unwrap().clone() + } + + /// Walk every span across every captured request, returning each span + /// with its parent `Resource`'s `service.name` attribute (so the test + /// can group spans by service). Trace and span ids stay as the + /// base64-encoded strings the OTLP/HTTP-JSON encoding uses — equality + /// comparison is what tests need, not decoding. + #[must_use] + pub fn flatten_spans(&self) -> Vec { + let mut out = Vec::new(); + for req in self.traces() { + let Some(resource_spans) = req.get("resourceSpans").and_then(Value::as_array) else { + continue; + }; + for rs in resource_spans { + let service_name = rs + .get("resource") + .and_then(|r| r.get("attributes")) + .and_then(Value::as_array) + .and_then(|attrs| { + attrs.iter().find_map(|attr| { + if attr.get("key").and_then(Value::as_str) == Some("service.name") { + attr.get("value") + .and_then(|v| v.get("stringValue")) + .and_then(Value::as_str) + .map(str::to_string) + } else { + None + } + }) + }) + .unwrap_or_default(); + let Some(scope_spans) = rs.get("scopeSpans").and_then(Value::as_array) else { + continue; + }; + for ss in scope_spans { + let Some(spans) = ss.get("spans").and_then(Value::as_array) else { + continue; + }; + for span in spans { + let trace_id = span.get("traceId").and_then(Value::as_str).unwrap_or("").to_string(); + let span_id = span.get("spanId").and_then(Value::as_str).unwrap_or("").to_string(); + let parent_span_id = span + .get("parentSpanId") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let name = span.get("name").and_then(Value::as_str).unwrap_or("").to_string(); + out.push(FlatSpan { + service_name: service_name.clone(), + trace_id, + span_id, + parent_span_id, + name, + }); + } + } + } + } + out + } +} + +/// Flattened span view for assertions. +#[derive(Clone, Debug)] +pub struct FlatSpan { + pub service_name: String, + /// Base64-encoded 16-byte trace id (OTLP/HTTP-JSON proto-JSON encoding). + pub trace_id: String, + pub span_id: String, + pub parent_span_id: String, + pub name: String, +} + +#[post("/traces")] +async fn handle_traces(state: web::Data>, body: web::Json) -> HttpResponse { + state.traces.lock().unwrap().push(body.into_inner()); + // OTLP success: empty `ExportTraceServiceResponse` is just `{}`. + HttpResponse::Ok().content_type("application/json").body("{}") +} + +#[post("/logs")] +async fn handle_logs(state: web::Data>, body: web::Json) -> HttpResponse { + state.logs.lock().unwrap().push(body.into_inner()); + HttpResponse::Ok().content_type("application/json").body("{}") +} + +/// Start the mock on a free port and return its handle. The HTTP server +/// runs on its own thread + actix runtime; the test's runtime is untouched. +pub fn start() -> OtlpMock { + // Bind to :0 to grab a free port, then drop the listener so the actix + // runtime can re-bind to it. (Same trick as `et-ws-test-server`.) + let port = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); + let captured = Arc::new(Captured::default()); + let captured_for_server = captured.clone(); + let addr = format!("127.0.0.1:{port}"); + + std::thread::spawn(move || { + actix_rt::System::new().block_on(async move { + let data = web::Data::new(captured_for_server); + HttpServer::new(move || { + App::new() + .app_data(data.clone()) + .app_data(web::JsonConfig::default().limit(64 * 1024 * 1024)) + .service(handle_traces) + .service(handle_logs) + }) + .bind(&addr) + .unwrap() + .run() + .await + .unwrap(); + }); + }); + + // Wait for the server to start accepting connections so the caller can + // immediately point exporters at it. + for _ in 0..50 { + if std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_ok() { + return OtlpMock { + collector_url: format!("http://127.0.0.1:{port}"), + captured, + }; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + panic!("otlp-mock did not start within 5 seconds on port {port}"); +} diff --git a/services/modules/Cargo.toml b/services/modules/Cargo.toml index 7ab854f..04d52ee 100644 --- a/services/modules/Cargo.toml +++ b/services/modules/Cargo.toml @@ -8,7 +8,7 @@ repository.workspace = true [dependencies] actix-files = "0.6" actix-web = "4" -edge-toolkit = { path = "../../libs/edge-toolkit" } +edge-toolkit.workspace = true serde.workspace = true serde-inline-default.workspace = true serde_default.workspace = true diff --git a/services/storage/Cargo.toml b/services/storage/Cargo.toml index d0d4c7b..22b8d14 100644 --- a/services/storage/Cargo.toml +++ b/services/storage/Cargo.toml @@ -8,7 +8,7 @@ repository.workspace = true [dependencies] actix-files = "0.6" actix-web = "4" -edge-toolkit = { path = "../../libs/edge-toolkit" } +edge-toolkit.workspace = true futures-util = "0.3" serde.workspace = true serde-inline-default.workspace = true diff --git a/services/ws-modules/audio1/src/lib.rs b/services/ws-modules/audio1/src/lib.rs index 4f94a6c..fbaccf4 100644 --- a/services/ws-modules/audio1/src/lib.rs +++ b/services/ws-modules/audio1/src/lib.rs @@ -94,7 +94,7 @@ pub async fn run() -> Result<(), JsValue> { let mut client = WsClient::new(WsClientConfig::new(ws_url)); client.connect()?; wait_for_connected(&client).await?; - log(&format!("websocket connected with agent_id={}", client.get_client_id()))?; + log(&format!("websocket connected with agent_id={}", client.get_agent_id()))?; log("requesting microphone access")?; let access = MicrophoneAccess::request().await?; diff --git a/services/ws-modules/bluetooth/src/lib.rs b/services/ws-modules/bluetooth/src/lib.rs index 38e06e3..954a40e 100644 --- a/services/ws-modules/bluetooth/src/lib.rs +++ b/services/ws-modules/bluetooth/src/lib.rs @@ -111,7 +111,7 @@ pub async fn run() -> Result<(), JsValue> { let mut client = WsClient::new(WsClientConfig::new(ws_url)); client.connect()?; wait_for_connected(&client).await?; - log(&format!("websocket connected with agent_id={}", client.get_client_id()))?; + log(&format!("websocket connected with agent_id={}", client.get_agent_id()))?; log("requesting bluetooth access")?; let access = BluetoothAccess::request().await?; diff --git a/services/ws-modules/comm1/src/lib.rs b/services/ws-modules/comm1/src/lib.rs index 773355e..cd8ce12 100644 --- a/services/ws-modules/comm1/src/lib.rs +++ b/services/ws-modules/comm1/src/lib.rs @@ -167,7 +167,7 @@ async fn wait_for_connected(client: &WsClient) -> Result<(), JsValue> { async fn wait_for_agent_id(client: &WsClient) -> Result { for _ in 0..100 { - let agent_id = client.get_client_id(); + let agent_id = client.get_agent_id(); if !agent_id.is_empty() { return Ok(agent_id); } diff --git a/services/ws-modules/dart-comm1/lib/dart_comm1.dart b/services/ws-modules/dart-comm1/lib/dart_comm1.dart index a3c9b28..a773eaf 100644 --- a/services/ws-modules/dart-comm1/lib/dart_comm1.dart +++ b/services/ws-modules/dart-comm1/lib/dart_comm1.dart @@ -15,7 +15,7 @@ extension type WsClient._(JSObject _) implements JSObject { // ignore: non_constant_identifier_names external String get_state(); // ignore: non_constant_identifier_names - external String get_client_id(); + external String get_agent_id(); external void send(String message); // ignore: non_constant_identifier_names external void set_on_message(JSFunction callback); @@ -70,7 +70,7 @@ Future waitForConnected(WsClient client) async { Future waitForAgentId(WsClient client) async { for (var i = 0; i < 100; i++) { - final id = client.get_client_id(); + final id = client.get_agent_id(); if (id.isNotEmpty) return id; await sleep(100); } diff --git a/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js b/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js index b7ef44a..44be2b9 100644 --- a/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js +++ b/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js @@ -16,7 +16,9 @@ export async function run() { } // Dart @JS() interop resolves against globalThis, so expose the wasm-agent // classes there for the duration of the call. - const { WsClient, WsClientConfig } = await import("/modules/et-ws-wasm-agent/et_ws_wasm_agent.js"); + const wasmAgent = await import("/modules/et-ws-wasm-agent/et_ws_wasm_agent.js"); + await wasmAgent.default(); + const { WsClient, WsClientConfig } = wasmAgent; globalThis.WsClient = WsClient; globalThis.WsClientConfig = WsClientConfig; try { diff --git a/services/ws-modules/data1/src/lib.rs b/services/ws-modules/data1/src/lib.rs index c96086b..cdcf6c3 100644 --- a/services/ws-modules/data1/src/lib.rs +++ b/services/ws-modules/data1/src/lib.rs @@ -178,7 +178,7 @@ async fn wait_for_connected(client: &WsClient) -> Result<(), JsValue> { async fn wait_for_agent_id(client: &WsClient) -> Result { for _ in 0..100 { - let agent_id = client.get_client_id(); + let agent_id = client.get_agent_id(); if !agent_id.is_empty() { return Ok(agent_id); } diff --git a/services/ws-modules/face-detection/src/lib.rs b/services/ws-modules/face-detection/src/lib.rs index 4a85e77..7a813a4 100644 --- a/services/ws-modules/face-detection/src/lib.rs +++ b/services/ws-modules/face-detection/src/lib.rs @@ -136,7 +136,7 @@ pub async fn run() -> Result<(), JsValue> { let mut client = WsClient::new(WsClientConfig::new(ws_url.clone())); client.connect()?; wait_for_connected(&client).await?; - log(&format!("websocket connected with agent_id={}", client.get_client_id()))?; + log(&format!("websocket connected with agent_id={}", client.get_agent_id()))?; let capture = match VideoCapture::request().await { Ok(capture) => capture, diff --git a/services/ws-modules/geolocation/src/lib.rs b/services/ws-modules/geolocation/src/lib.rs index 0954eaa..c4a1e47 100644 --- a/services/ws-modules/geolocation/src/lib.rs +++ b/services/ws-modules/geolocation/src/lib.rs @@ -121,7 +121,7 @@ pub async fn run() -> Result<(), JsValue> { let mut client = WsClient::new(WsClientConfig::new(ws_url)); client.connect()?; wait_for_connected(&client).await?; - log(&format!("websocket connected with agent_id={}", client.get_client_id()))?; + log(&format!("websocket connected with agent_id={}", client.get_agent_id()))?; log("requesting geolocation access")?; let reading = GeolocationReading::request().await?; diff --git a/services/ws-modules/graphics-info/src/lib.rs b/services/ws-modules/graphics-info/src/lib.rs index 19c9221..5fe999f 100644 --- a/services/ws-modules/graphics-info/src/lib.rs +++ b/services/ws-modules/graphics-info/src/lib.rs @@ -650,7 +650,7 @@ pub async fn run() -> Result<(), JsValue> { let mut client = WsClient::new(WsClientConfig::new(ws_url)); client.connect()?; wait_for_connected(&client).await?; - log(&format!("websocket connected with agent_id={}", client.get_client_id()))?; + log(&format!("websocket connected with agent_id={}", client.get_agent_id()))?; log("detecting graphics support")?; let support = GraphicsSupport::detect()?; diff --git a/services/ws-modules/har1/src/lib.rs b/services/ws-modules/har1/src/lib.rs index 3e00e0a..ccfc52e 100644 --- a/services/ws-modules/har1/src/lib.rs +++ b/services/ws-modules/har1/src/lib.rs @@ -313,7 +313,7 @@ pub async fn run() -> Result<(), JsValue> { log("connecting websocket client")?; client.connect()?; wait_for_connected(&client).await?; - log(&format!("websocket connected with agent_id={}", client.get_client_id()))?; + log(&format!("websocket connected with agent_id={}", client.get_agent_id()))?; let mut sensors = DeviceSensors::new(); log("starting har1 workflow")?; diff --git a/services/ws-modules/nfc/src/lib.rs b/services/ws-modules/nfc/src/lib.rs index 721d933..b69a980 100644 --- a/services/ws-modules/nfc/src/lib.rs +++ b/services/ws-modules/nfc/src/lib.rs @@ -209,7 +209,7 @@ pub async fn run() -> Result<(), JsValue> { let mut client = WsClient::new(WsClientConfig::new(ws_url)); client.connect()?; wait_for_connected(&client).await?; - log(&format!("websocket connected with agent_id={}", client.get_client_id()))?; + log(&format!("websocket connected with agent_id={}", client.get_agent_id()))?; log("waiting for NFC tap (60 second timeout)...")?; set_module_status("nfc: Waiting for NFC tap...\nPlease hold your device near an NFC tag.")?; diff --git a/services/ws-modules/pydata1/pkg/et_ws_pydata1.js b/services/ws-modules/pydata1/pkg/et_ws_pydata1.js index 006e9a4..7128dfe 100644 --- a/services/ws-modules/pydata1/pkg/et_ws_pydata1.js +++ b/services/ws-modules/pydata1/pkg/et_ws_pydata1.js @@ -41,7 +41,9 @@ export async function run() { const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${wsProtocol}//${window.location.host}/ws`; - const { WsClient, WsClientConfig } = await import("/modules/et-ws-wasm-agent/et_ws_wasm_agent.js"); + const wasmAgent = await import("/modules/et-ws-wasm-agent/et_ws_wasm_agent.js"); + await wasmAgent.default(); + const { WsClient, WsClientConfig } = wasmAgent; const client = new WsClient(new WsClientConfig(wsUrl)); let responseResolvers = []; @@ -72,7 +74,7 @@ export async function run() { let agentId = ""; for (let i = 0; i < 100; i++) { - agentId = client.get_client_id(); + agentId = client.get_agent_id(); if (agentId) break; await sleep(100); if (i === 99) throw new Error("Timeout waiting for agent_id"); diff --git a/services/ws-modules/pydata1/pyproject.toml b/services/ws-modules/pydata1/pyproject.toml index e65ec0b..79539a4 100644 --- a/services/ws-modules/pydata1/pyproject.toml +++ b/services/ws-modules/pydata1/pyproject.toml @@ -13,6 +13,3 @@ requires = ["uv_build>=0.10.2,<0.11.0"] [tool.uv.build-backend] module-name = "pydata1" module-root = "" - -[tool.ws-module] -js-main = "et_ws_pydata1.js" diff --git a/services/ws-modules/pyface1/pkg/et_ws_pyface1.js b/services/ws-modules/pyface1/pkg/et_ws_pyface1.js index 60bfb8f..7b191b8 100644 --- a/services/ws-modules/pyface1/pkg/et_ws_pyface1.js +++ b/services/ws-modules/pyface1/pkg/et_ws_pyface1.js @@ -46,13 +46,15 @@ export async function run() { let state = null; try { - const { WsClient, WsClientConfig } = await import("/modules/et-ws-wasm-agent/et_ws_wasm_agent.js"); + const wasmAgent = await import("/modules/et-ws-wasm-agent/et_ws_wasm_agent.js"); + await wasmAgent.default(); + const { WsClient, WsClientConfig } = wasmAgent; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; client = new WsClient(new WsClientConfig(`${protocol}//${window.location.host}/ws`)); client.connect(); for (let i = 0; client.get_state() !== "connected" && i < 100; i++) await sleep(100); if (client.get_state() !== "connected") throw new Error("Timed out waiting for websocket connection"); - log(`websocket connected with agent_id=${client.get_client_id()}`); + log(`websocket connected with agent_id=${client.get_agent_id()}`); stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true }); const video = element("video-preview", HTMLVideoElement); diff --git a/services/ws-modules/pyface1/pyproject.toml b/services/ws-modules/pyface1/pyproject.toml index d7732c4..0708aa7 100644 --- a/services/ws-modules/pyface1/pyproject.toml +++ b/services/ws-modules/pyface1/pyproject.toml @@ -14,9 +14,6 @@ requires = ["uv_build>=0.10.2,<0.11.0"] module-name = "pyface1" module-root = "" -[tool.ws-module] -js-main = "et_ws_pyface1.js" - [tool.ws-module.dependencies] et-model-face1 = "*" onnxruntime-web = "*" diff --git a/services/ws-modules/speech-recognition/src/lib.rs b/services/ws-modules/speech-recognition/src/lib.rs index d526c81..948b983 100644 --- a/services/ws-modules/speech-recognition/src/lib.rs +++ b/services/ws-modules/speech-recognition/src/lib.rs @@ -302,7 +302,7 @@ pub async fn run() -> Result<(), JsValue> { let mut client = WsClient::new(WsClientConfig::new(ws_url)); client.connect()?; wait_for_connected(&client).await?; - log(&format!("websocket connected with agent_id={}", client.get_client_id()))?; + log(&format!("websocket connected with agent_id={}", client.get_agent_id()))?; log("starting speech recognition session")?; let session = Rc::new(SpeechRecognitionSession::new()?); diff --git a/services/ws-modules/video1/src/lib.rs b/services/ws-modules/video1/src/lib.rs index 35293cd..fc9346f 100644 --- a/services/ws-modules/video1/src/lib.rs +++ b/services/ws-modules/video1/src/lib.rs @@ -94,7 +94,7 @@ pub async fn run() -> Result<(), JsValue> { let mut client = WsClient::new(WsClientConfig::new(ws_url)); client.connect()?; wait_for_connected(&client).await?; - log(&format!("websocket connected with agent_id={}", client.get_client_id()))?; + log(&format!("websocket connected with agent_id={}", client.get_agent_id()))?; log("requesting video capture access")?; let capture = VideoCapture::request().await?; diff --git a/services/ws-modules/wasi-comm1/.gitignore b/services/ws-modules/wasi-comm1/.gitignore new file mode 100644 index 0000000..133547d --- /dev/null +++ b/services/ws-modules/wasi-comm1/.gitignore @@ -0,0 +1,2 @@ +/pkg/*.wasm +/pkg/package.json diff --git a/services/ws-modules/wasi-comm1/Cargo.toml b/services/ws-modules/wasi-comm1/Cargo.toml new file mode 100644 index 0000000..96c6b28 --- /dev/null +++ b/services/ws-modules/wasi-comm1/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "et-ws-wasi-comm1" +description = "WASI Preview 2 port of the comm1 module — list-agents + broadcast + direct message" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0 OR MIT" +repository.workspace = true + +[lib] +crate-type = ["cdylib"] + +# See wasi-data1/Cargo.toml for the rationale behind the wasi-only target +# scope; the lib body is gated identically. +[target.'cfg(target_os = "wasi")'.dependencies] +serde_json = "1" +wit-bindgen = "0.57" diff --git a/services/ws-modules/wasi-comm1/src/lib.rs b/services/ws-modules/wasi-comm1/src/lib.rs new file mode 100644 index 0000000..bc3114f --- /dev/null +++ b/services/ws-modules/wasi-comm1/src/lib.rs @@ -0,0 +1,132 @@ +//! Rust WASI Preview 2 port of the comm1 workflow module. +//! +//! Browser comm1 (`services/ws-modules/comm1`) waits for a second agent to +//! be connected, then exchanges broadcast and direct messages with it. The +//! integration test only spins up a single runner, so the WASI port instead +//! exercises the message round-trip with the server itself: +//! 1. Connect and capture our agent_id. +//! 2. Send `list_agents`, recv a `list_agents_response`, assert the list +//! contains our agent_id (we're at least in our own roster). +//! 3. Send a `broadcast_message` (fire-and-forget when no peer is online). +//! 4. Disconnect cleanly. +//! +//! Wire-format messages are built with `serde_json::json!` and serialised +//! before going through `ws.send-text`; recv'd frames are parsed with +//! `serde_json::Value`. This mirrors the WsMessage enum in +//! `libs/edge-toolkit/src/ws.rs` but avoids depending on that crate (its +//! transitive deps don't all compile to wasm32-wasip2). + +// Crate-level cfg gate: wit-bindgen's generated extern declarations only +// resolve on `wasm32-wasip2`. Gating the whole module on `target_os = "wasi"` +// lets the crate sit in the parent workspace — `cargo check --workspace` +// from the repo root produces an empty cdylib for the host target without +// linker errors. +#![cfg(target_os = "wasi")] + +wit_bindgen::generate!({ + path: "../../ws-wasi-runner/wit", + world: "module", + generate_all, +}); + +use exports::et::ws_wasi::entry::Guest; +use serde_json::{Value, json}; +use wasi::logging::logging::{self, Level}; + +const LOG_CONTEXT: &str = env!("CARGO_PKG_NAME"); +/// Total time we'll wait for a `list_agents_response`. The server replies +/// immediately under normal load, but we leave headroom for the inbox queue. +const LIST_AGENTS_TIMEOUT_MS: u32 = 2_000; + +fn info(message: &str) { + logging::log(Level::Info, LOG_CONTEXT, message); +} + +struct Component; + +impl Guest for Component { + fn run() -> Result<(), String> { + info("entered run()"); + + et::ws_wasi::ws::connect().map_err(|e| format!("ws connect failed: {e}"))?; + let agent_id = wait_for_agent_id().ok_or_else(|| "did not receive agent_id".to_string())?; + info(&format!("websocket connected with agent_id={agent_id}")); + + send_message(&json!({ "type": "list_agents" }))?; + + let response = wait_for_message_kind("list_agents_response", LIST_AGENTS_TIMEOUT_MS) + .ok_or_else(|| "no list_agents_response within timeout".to_string())?; + let agents = response + .get("agents") + .and_then(Value::as_array) + .ok_or_else(|| "list_agents_response missing `agents` array".to_string())?; + info(&format!("list_agents_response: {} agent(s) registered", agents.len())); + + let self_listed = agents + .iter() + .any(|a| a.get("agent_id").and_then(Value::as_str) == Some(agent_id.as_str())); + if !self_listed { + return Err(format!("own agent_id {agent_id} missing from list_agents_response")); + } + info("self present in roster"); + + send_message(&json!({ + "type": "broadcast_message", + "message": { + "module": "wasi-comm1", + "from_agent_id": agent_id, + "message": "wasi-comm1 broadcast — likely peerless under the runner test", + } + }))?; + info("broadcast sent"); + + et::ws_wasi::ws::disconnect(); + info("workflow complete"); + Ok(()) + } +} + +fn send_message(value: &Value) -> Result<(), String> { + let text = serde_json::to_string(value).map_err(|e| format!("serialize message: {e}"))?; + et::ws_wasi::ws::send_text(&text).map_err(|e| format!("ws.send_text: {e}")) +} + +/// Drain the recv inbox until we see a message whose `type` matches `kind`. +/// Each `recv` call blocks for the remaining budget; we keep going until +/// either the budget is exhausted or the inbox runs dry. +fn wait_for_message_kind(kind: &str, total_timeout_ms: u32) -> Option { + let mut remaining = total_timeout_ms; + while remaining > 0 { + let chunk = remaining.min(200); + match et::ws_wasi::ws::recv(chunk).ok()? { + Some(text) => { + if let Ok(value) = serde_json::from_str::(&text) + && value.get("type").and_then(Value::as_str) == Some(kind) + { + return Some(value); + } + } + None => {} + } + remaining = remaining.saturating_sub(chunk); + } + None +} + +fn wait_for_agent_id() -> Option { + for _ in 0..100 { + let id = et::ws_wasi::ws::agent_id(); + if !id.is_empty() { + return Some(id); + } + sleep_ms(50); + } + None +} + +fn sleep_ms(ms: u64) { + let pollable = wasi::clocks::monotonic_clock::subscribe_duration(ms * 1_000_000); + wasi::io::poll::poll(&[&pollable]); +} + +export!(Component); diff --git a/services/ws-modules/wasi-data1/.gitignore b/services/ws-modules/wasi-data1/.gitignore new file mode 100644 index 0000000..133547d --- /dev/null +++ b/services/ws-modules/wasi-data1/.gitignore @@ -0,0 +1,2 @@ +/pkg/*.wasm +/pkg/package.json diff --git a/services/ws-modules/wasi-data1/Cargo.toml b/services/ws-modules/wasi-data1/Cargo.toml new file mode 100644 index 0000000..877ba30 --- /dev/null +++ b/services/ws-modules/wasi-data1/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "et-ws-wasi-data1" +description = "WASI Preview 2 port of the data1 module — keyvalue store roundtrip" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0 OR MIT" +repository.workspace = true + +[lib] +crate-type = ["cdylib"] + +# Scope the wit-bindgen + serde_json deps to wasi targets so a host +# `cargo check --workspace` doesn't pull them in. The lib body is gated +# the same way (`#![cfg(target_os = "wasi")]` at the top of src/lib.rs), +# so on the host target the crate compiles to an empty cdylib. +[target.'cfg(target_os = "wasi")'.dependencies] +serde_json = "1" +wit-bindgen = "0.57" diff --git a/services/ws-modules/wasi-data1/src/lib.rs b/services/ws-modules/wasi-data1/src/lib.rs new file mode 100644 index 0000000..9dc8128 --- /dev/null +++ b/services/ws-modules/wasi-data1/src/lib.rs @@ -0,0 +1,99 @@ +//! Rust WASI Preview 2 port of the data1 workflow module. +//! +//! Browser data1 (`services/ws-modules/data1`) round-trips a file through +//! the ws-server's storage service by: +//! 1. Asking the server (via `StoreFile`) for a PUT URL, +//! 2. HTTP-PUTting bytes, +//! 3. Asking (via `FetchFile`) for a GET URL, +//! 4. HTTP-GETting and verifying. +//! +//! The WASI runner doesn't expose generic HTTP; the equivalent here uses +//! `wasi:keyvalue/store` directly. The bucket identifier is the agent's +//! own `agent_id`, which maps host-side to `/storage/{agent_id}/{key}` — +//! same backend store, same auth boundary (writes only succeed inside +//! one's own bucket), one fewer protocol hop. +//! +//! Crate-level cfg gate: the wit-bindgen-generated extern declarations +//! reference WASI imports that only resolve on `wasm32-wasip2`. Gating the +//! whole module on `target_os = "wasi"` lets the crate sit in the parent +//! workspace — `cargo check --workspace` from the repo root produces an +//! empty cdylib for the host target without linker errors. + +#![cfg(target_os = "wasi")] + +wit_bindgen::generate!({ + path: "../../ws-wasi-runner/wit", + world: "module", + generate_all, +}); + +use exports::et::ws_wasi::entry::Guest; +use wasi::keyvalue::store; +use wasi::logging::logging::{self, Level}; + +const LOG_CONTEXT: &str = env!("CARGO_PKG_NAME"); +const FILENAME: &str = "test_data.txt"; + +fn info(message: &str) { + logging::log(Level::Info, LOG_CONTEXT, message); +} + +struct Component; + +impl Guest for Component { + fn run() -> Result<(), String> { + info("entered run()"); + + et::ws_wasi::ws::connect().map_err(|e| format!("ws connect failed: {e}"))?; + let agent_id = wait_for_agent_id().ok_or_else(|| "did not receive agent_id".to_string())?; + info(&format!("websocket connected with agent_id={agent_id}")); + + let bucket = store::open(&agent_id).map_err(|e| format!("store.open({agent_id}): {e:?}"))?; + + let test_content = format!("Hello from wasi-data1, agent={agent_id}!").into_bytes(); + info(&format!("storing {} bytes to key {FILENAME}", test_content.len())); + bucket + .set(FILENAME, &test_content) + .map_err(|e| format!("bucket.set({FILENAME}): {e:?}"))?; + + info(&format!("fetching key {FILENAME}")); + let fetched = bucket + .get(FILENAME) + .map_err(|e| format!("bucket.get({FILENAME}): {e:?}"))? + .ok_or_else(|| format!("bucket.get({FILENAME}) returned none after set"))?; + + if fetched != test_content { + return Err(format!( + "data mismatch: sent {} bytes, got {} bytes", + test_content.len(), + fetched.len() + )); + } + info("VERIFICATION SUCCESS — keyvalue roundtrip matches"); + + et::ws_wasi::ws::disconnect(); + info("workflow complete"); + Ok(()) + } +} + +/// `ws.connect` waits briefly for the `ConnectAck` server message, but the +/// host returns once that wait expires regardless. Poll `agent_id` to be +/// safe under load. +fn wait_for_agent_id() -> Option { + for _ in 0..100 { + let id = et::ws_wasi::ws::agent_id(); + if !id.is_empty() { + return Some(id); + } + sleep_ms(50); + } + None +} + +fn sleep_ms(ms: u64) { + let pollable = wasi::clocks::monotonic_clock::subscribe_duration(ms * 1_000_000); + wasi::io::poll::poll(&[&pollable]); +} + +export!(Component); diff --git a/services/ws-modules/wasi-graphics-info/.gitignore b/services/ws-modules/wasi-graphics-info/.gitignore new file mode 100644 index 0000000..bed95b7 --- /dev/null +++ b/services/ws-modules/wasi-graphics-info/.gitignore @@ -0,0 +1,16 @@ +# componentize-py outputs (regenerated each `bindings .` run). The world's +# bindings package is `wit_world/`; the runtime/types/poll-loop helpers and +# async-support shim are written alongside. +/wit_world/ +/componentize_py_async_support/ +/componentize_py_runtime.pyi +/componentize_py_types.py +/poll_loop.py + +# Build artefacts populated by mise run build-ws-wasi-graphics-info-module. +/pkg/*.wasm +/pkg/*.onnx +/pkg/package.json + +# Python build cache. +**/__pycache__/ diff --git a/services/ws-modules/wasi-graphics-info/pyproject.toml b/services/ws-modules/wasi-graphics-info/pyproject.toml new file mode 100644 index 0000000..18e7fe6 --- /dev/null +++ b/services/ws-modules/wasi-graphics-info/pyproject.toml @@ -0,0 +1,18 @@ +[project] +dependencies = [] +description = "Python graphics-info WASI component via componentize-py" +license = "Apache-2.0 OR MIT" +name = "et-ws-wasi-graphics-info" +requires-python = ">=3.10" +version = "0.1.0" + +[project.urls] +repository = "https://github.com/edge-toolkit/core" + +[build-system] +build-backend = "uv_build" +requires = ["uv_build>=0.10.2,<0.11.0"] + +[tool.uv.build-backend] +module-name = "wasi_graphics_info" +module-root = "" diff --git a/services/ws-modules/wasi-graphics-info/wasi_graphics_info/__init__.py b/services/ws-modules/wasi-graphics-info/wasi_graphics_info/__init__.py new file mode 100644 index 0000000..230bba1 --- /dev/null +++ b/services/ws-modules/wasi-graphics-info/wasi_graphics_info/__init__.py @@ -0,0 +1,452 @@ +"""WASI port of `graphics-info`, with both wasi-webgpu compute and a wasi-nn ML demo. + +The module exercises two standardised WASI interfaces on the host: + +* `wasi:webgpu/webgpu` — a trimmed subset of WebAssembly/wasi-gfx (see + `wit/deps/wasi-webgpu/`). The guest builds a real GPU pipeline (adapter, + device, buffers, shader, bind group, compute pass) and runs a 4x4 matmul. +* `wasi:nn/{graph, tensor, inference}` — the same WIT surface wasmCloud / + Spin / Fermyon production workloads use, backed on our host by + `wasmtime-wasi-nn` + ONNX Runtime. We load `mnist-12.onnx` and run a + single forward pass. + +Workflow per run(): + 1. set up a webgpu adapter/device + report graphics info + 2. run a 4x4 matmul compute pass and verify the (0,0) result + 3. load `mnist-12.onnx` (bundled, ~26KB) via `wasi:nn/graph.load` + 4. run inference on a fixed 28x28 all-zeros input + 5. argmax -> predicted MNIST digit class + 6. verify against EXPECTED_MNIST_CLASS, send the result over ws +""" + +import array +import json +import struct + +from wit_world.imports import logging, monotonic_clock, poll, store, webgpu, ws +from wit_world.imports.logging import Level +from wit_world.imports import graph as nn_graph +from wit_world.imports.graph import ExecutionTarget, GraphEncoding +from wit_world.imports.tensor import Tensor, TensorType +from wit_world.imports.webgpu import ( + GpuBindGroupDescriptor, + GpuBindGroupEntry, + GpuBindGroupLayoutDescriptor, + GpuBindGroupLayoutEntry, + GpuBindingResource_GpuBufferBinding, + GpuBufferBinding, + GpuBufferBindingLayout, + GpuBufferBindingType, + GpuBufferDescriptor, + GpuBufferUsage, + GpuComputePipelineDescriptor, + GpuLayoutMode_Specific, + GpuMapMode, + GpuPipelineLayoutDescriptor, + GpuProgrammableStage, + GpuShaderModuleDescriptor, + GpuShaderStage, +) + + +# An all-zero 28x28 input is the simplest reproducible MNIST query - bytes +# don't drift across rebuilds and we don't have to ship a real digit image. +# Whatever class ONNX Runtime returns for this fixed input is what we +# hard-code as expected. mnist-12.onnx happens to confidently predict class +# 5 for an all-black image (its bias toward "5" on blank-ish inputs is a +# well-known property). +MNIST_INPUT_SHAPE = [1, 1, 28, 28] +EXPECTED_MNIST_CLASS = 5 + +# mnist-12.onnx I/O names - established by the model file, not configurable. +MNIST_INPUT_NAME = "Input3" +MNIST_OUTPUT_NAME = "Plus214_Output_0" + +# Identity * (2*I) matmul. Result C[0][0] = 2.0, C[1][1] = 2.0, ... +MAT_A = [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, +] +MAT_B = [ + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2.0, +] +MATRIX_BYTES = 16 * 4 # 16 f32 = 64 bytes +EXPECTED_C00 = 2.0 + +WGSL = """ +@group(0) @binding(0) var matA : array; +@group(0) @binding(1) var matB : array; +@group(0) @binding(2) var matC : array; + +@compute @workgroup_size(4, 4) +fn main(@builtin(global_invocation_id) gid : vec3) { + let row = gid.y; + let col = gid.x; + var sum : f32 = 0.0; + for (var k : u32 = 0u; k < 4u; k = k + 1u) { + sum = sum + matA[row * 4u + k] * matB[k * 4u + col]; + } + matC[row * 4u + col] = sum; +} +""" + + +LOG_CONTEXT = "wasi-graphics-info" + + +def _log(message: str) -> None: + # All current call sites are informational. Drop down to `logging.log` with + # an explicit level if a warn/error path needs it. + logging.log(Level.INFO, LOG_CONTEXT, message) + + +def _send_event(category: str, kind: str, body: dict) -> None: + ws.send_event(category, kind, json.dumps(body)) + + +def _now_ms() -> int: + # `monotonic_clock.now()` returns nanoseconds from an arbitrary epoch. + # Only used here for elapsed-time deltas, so the epoch doesn't matter. + return monotonic_clock.now() // 1_000_000 + + +def _sleep_ms(ms: int) -> None: + # The standard WASI sleep: subscribe a duration pollable, then block on + # it via wasi:io/poll. Equivalent to `clock.sleep-ms` in the old custom + # interface, but uses interfaces every WASI Preview 2 runtime provides. + pollable = monotonic_clock.subscribe_duration(ms * 1_000_000) + poll.poll([pollable]) + + +def _wait_for_connected(timeout_ms: int = 2000) -> bool: + elapsed = 0 + step = 50 + while elapsed < timeout_ms: + if ws.get_state() == ws.State.CONNECTED: + return True + _sleep_ms(step) + elapsed += step + return False + + +def _zero_input_bytes() -> bytes: + # 28x28 = 784 float32 zeros, little-endian (wasi-nn's tensor data layout + # follows host native; both wasmtime targets and CPython use little-endian + # on every platform we ship to). + return array.array("f", [0.0] * (28 * 28)).tobytes() + + +def _matrix_bytes(values: list) -> bytes: + return struct.pack(f"<{len(values)}f", *values) + + +def _entry(binding: int, read_only: bool) -> GpuBindGroupLayoutEntry: + """One COMPUTE-visible storage-buffer bind-group-layout entry. Bindings 0 + and 1 are read-only (matA / matB), binding 2 is read-write (matC). This + has to match the WGSL `var` / `var` + qualifiers exactly or wgpu's create-compute-pipeline validation rejects.""" + return GpuBindGroupLayoutEntry( + binding=binding, + visibility=GpuShaderStage.compute(), + buffer=GpuBufferBindingLayout( + type=GpuBufferBindingType.READ_ONLY_STORAGE + if read_only + else GpuBufferBindingType.STORAGE, + has_dynamic_offset=False, + min_binding_size=None, + ), + ) + + +def _bind_entry(binding: int, buffer) -> GpuBindGroupEntry: + return GpuBindGroupEntry( + binding=binding, + resource=GpuBindingResource_GpuBufferBinding( + value=GpuBufferBinding(buffer=buffer, offset=0, size=MATRIX_BYTES), + ), + ) + + +def _run_matmul() -> dict: + """Build a full wasi-webgpu compute pipeline and run the 4x4 matmul. + + Returns the wire-format dict for `gpu_compute`. Raises `RuntimeError` + if the readback's first element doesn't match `EXPECTED_C00`. + """ + _log("wasi-webgpu: requesting adapter") + gpu = webgpu.get_gpu() + adapter = gpu.request_adapter(None) + if adapter is None: + raise RuntimeError("wasi-webgpu: no GPU adapter available") + + info = adapter.info() + gpu_info = { + "vendor": info.vendor(), + "renderer": info.device(), + "architecture": info.architecture(), + "description": info.description(), + "source": "wasi-webgpu", + } + _log( + f"wasi-webgpu adapter: vendor={gpu_info['vendor']} renderer={gpu_info['renderer']}" + f" architecture={gpu_info['architecture']}" + ) + + device = adapter.request_device(None) + queue = device.queue() + + started = _now_ms() + + storage_init_usage = GpuBufferUsage.storage() | GpuBufferUsage.copy_dst() + storage_out_usage = GpuBufferUsage.storage() | GpuBufferUsage.copy_src() + readback_usage = GpuBufferUsage.map_read() | GpuBufferUsage.copy_dst() + + buf_a = device.create_buffer( + GpuBufferDescriptor( + size=MATRIX_BYTES, + usage=storage_init_usage, + mapped_at_creation=False, + label="matA", + ) + ) + buf_b = device.create_buffer( + GpuBufferDescriptor( + size=MATRIX_BYTES, + usage=storage_init_usage, + mapped_at_creation=False, + label="matB", + ) + ) + buf_c = device.create_buffer( + GpuBufferDescriptor( + size=MATRIX_BYTES, + usage=storage_out_usage, + mapped_at_creation=False, + label="matC", + ) + ) + buf_readback = device.create_buffer( + GpuBufferDescriptor( + size=MATRIX_BYTES, + usage=readback_usage, + mapped_at_creation=False, + label="readback", + ) + ) + + queue.write_buffer_with_copy(buf_a, 0, _matrix_bytes(MAT_A), None, None) + queue.write_buffer_with_copy(buf_b, 0, _matrix_bytes(MAT_B), None, None) + + shader = device.create_shader_module( + GpuShaderModuleDescriptor(code=WGSL, compilation_hints=None, label="matmul-4x4") + ) + + bgl = device.create_bind_group_layout( + GpuBindGroupLayoutDescriptor( + entries=[ + _entry(0, read_only=True), + _entry(1, read_only=True), + _entry(2, read_only=False), + ], + label="matmul-bgl", + ) + ) + pl = device.create_pipeline_layout( + GpuPipelineLayoutDescriptor(bind_group_layouts=[bgl], label="matmul-pl") + ) + + pipeline = device.create_compute_pipeline( + GpuComputePipelineDescriptor( + compute=GpuProgrammableStage( + module=shader, entry_point="main", constants=None + ), + layout=GpuLayoutMode_Specific(value=pl), + label="matmul-pipeline", + ) + ) + + bind_group = device.create_bind_group( + GpuBindGroupDescriptor( + layout=bgl, + entries=[ + _bind_entry(0, buf_a), + _bind_entry(1, buf_b), + _bind_entry(2, buf_c), + ], + label="matmul-bg", + ) + ) + + encoder = device.create_command_encoder(None) + pass_ = encoder.begin_compute_pass(None) + pass_.set_pipeline(pipeline) + pass_.set_bind_group(0, bind_group, None, None, None) + # The shader uses @workgroup_size(4,4), so one workgroup covers all 16 cells. + pass_.dispatch_workgroups(1, 1, 1) + pass_.end() + + encoder.copy_buffer_to_buffer(buf_c, 0, buf_readback, 0, MATRIX_BYTES) + command_buffer = encoder.finish(None) + queue.submit([command_buffer]) + + buf_readback.map_async(GpuMapMode.read(), 0, MATRIX_BYTES) + data = buf_readback.get_mapped_range_get_with_copy(0, MATRIX_BYTES) + buf_readback.unmap() + elapsed_ms = float(_now_ms() - started) + + result_c00 = struct.unpack(" 1e-4: + raise RuntimeError( + f"wasi-webgpu: matmul produced C[0][0]={result_c00}, expected {EXPECTED_C00}" + ) + _log(f"wasi-webgpu matmul: C[0][0]={result_c00:.4f} in {elapsed_ms:.2f}ms") + + return { + "gpu_info": gpu_info, + "gpu_compute": { + "success": True, + "elapsed_ms": elapsed_ms, + "result_c00": float(result_c00), + }, + "webgpu_probe": {"adapter_found": True, "device_created": True}, + } + + +def _mnist_inference() -> dict: + """Load mnist-12.onnx and run a single forward pass via wasi-nn. + + Returns a dict suitable for inclusion in the `client_event` payload, or + raises a `RuntimeError` if the prediction doesn't match expectation. + """ + _log("loading mnist-12.onnx") + # The model file is a sibling static asset, served from pkg/ by + # et-modules-service. We treat it as a read-only wasi:keyvalue bucket + # backed by the module's static-asset directory (`/modules//`). + module_assets = store.open("modules/et-ws-wasi-graphics-info") + model_value = module_assets.get("mnist-12.onnx") + if model_value is None: + raise RuntimeError("mnist-12.onnx not found in modules bucket") + model_bytes = bytes(model_value) + _log(f"model loaded: {len(model_bytes)} bytes") + + # wasi-nn's `graph.load` takes a list of builders - for ONNX it's just + # the single model file. ExecutionTarget.GPU is a hint; the host backend + # (ORT) decides what hardware to dispatch to. + g = nn_graph.load([model_bytes], GraphEncoding.ONNX, ExecutionTarget.GPU) + _log("graph loaded") + + ctx = g.init_execution_context() + _log("execution context ready") + + input_tensor = Tensor(MNIST_INPUT_SHAPE, TensorType.FP32, _zero_input_bytes()) + + _log("running inference") + started = _now_ms() + outputs = ctx.compute([(MNIST_INPUT_NAME, input_tensor)]) + elapsed_ms = _now_ms() - started + _log(f"inference complete in {elapsed_ms}ms") + + if not outputs: + raise RuntimeError("wasi-nn returned no outputs") + + out_name, out_tensor = outputs[0] + if out_name != MNIST_OUTPUT_NAME: + _log( + f"warning: output name {out_name!r} differs from expected {MNIST_OUTPUT_NAME!r}" + ) + + raw = out_tensor.data() + arr = array.array("f") + arr.frombytes(raw) + logits = list(arr) + + if len(logits) != 10: + raise RuntimeError(f"expected 10 MNIST logits, got {len(logits)}") + + predicted = max(range(10), key=lambda i: logits[i]) + _log(f"predicted class: {predicted}, logits: {[round(v, 3) for v in logits]}") + + if predicted != EXPECTED_MNIST_CLASS: + raise RuntimeError( + f"MNIST verification FAILED: predicted {predicted}, expected {EXPECTED_MNIST_CLASS}" + ) + _log("MNIST verification: ok") + + return { + "framework": "wasi-nn (onnxruntime)", + "model": "mnist-12.onnx", + "input_shape": MNIST_INPUT_SHAPE, + "predicted_class": predicted, + "expected_class": EXPECTED_MNIST_CLASS, + "elapsed_ms": elapsed_ms, + "logits": [round(float(v), 4) for v in logits], + } + + +class Entry: + """Implements the `entry` interface exported by the world.""" + + def run(self) -> None: + _log("entered run()") + + try: + ws.connect() + except Exception as e: # noqa: BLE001 - bindings raise generic exceptions + _log(f"ws connect failed: {e}") + raise + + if not _wait_for_connected(): + _log("websocket did not reach connected state") + + agent_id = ws.agent_id() + _log(f"websocket connected with agent_id={agent_id}") + + gpu_block = _run_matmul() + # No browser-level detection in WASI; report the wasi-webgpu fact as + # the only WebGPU signal and the legacy WebGL / WebNN flags as False. + support = {"webgl": False, "webgl2": False, "webgpu": True, "webnn": False} + + mnist_result = _mnist_inference() + + _send_event( + "graphics", + "info_detected", + { + "support": support, + "webgpu_probe": gpu_block["webgpu_probe"], + "gpu": gpu_block["gpu_info"], + "gpu_compute": gpu_block["gpu_compute"], + "mnist_inference": mnist_result, + }, + ) + + ws.disconnect() diff --git a/services/ws-server/Cargo.toml b/services/ws-server/Cargo.toml index 69b0a6e..801369d 100644 --- a/services/ws-server/Cargo.toml +++ b/services/ws-server/Cargo.toml @@ -6,29 +6,18 @@ license.workspace = true repository.workspace = true [dependencies] -actix-rt = "2" -actix-web = { version = "4", features = ["rustls-0_23"] } +actix-rt.workspace = true +actix-web.workspace = true chrono.workspace = true clap.workspace = true -edge-toolkit = { path = "../../libs/edge-toolkit" } +edge-toolkit.workspace = true et-modules-service = { path = "../modules" } +et-otlp.workspace = true et-storage-service = { path = "../storage" } et-ws-service = { path = "../ws" } futures-util = "0.3" -hostname = "0.4" local-ip-address = "0.6" log.workspace = true -opentelemetry = "0.31" -opentelemetry-appender-tracing = "0.31" -opentelemetry-otlp = { version = "0.31", default-features = false, features = [ - "http-json", - "http-proto", - "logs", - "metrics", - "reqwest-blocking-client", - "trace", -] } -opentelemetry_sdk = "0.31" qr2term = "0.3" rcgen = "0.14" regex = { version = "1.12", default-features = false } @@ -42,12 +31,6 @@ serde_json.workspace = true serde_yaml = "0.9" tokio = { version = "1", features = ["full"] } tracing.workspace = true -tracing-actix-web = { version = "0.7", default-features = false, features = [ - "emit_event_on_error", - "opentelemetry_0_31", - "uuid_v7", -] } -tracing-log = "0.2" -tracing-opentelemetry = "0.32" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-actix-web.workspace = true +tracing-subscriber.workspace = true uuid.workspace = true diff --git a/services/ws-server/src/main.rs b/services/ws-server/src/main.rs index 0789a9b..de3b8be 100644 --- a/services/ws-server/src/main.rs +++ b/services/ws-server/src/main.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use actix_web::middleware::{DefaultHeaders, Logger}; +use actix_web::middleware::DefaultHeaders; use actix_web::{App, HttpServer, web}; use clap::Parser; use et_modules_service::list_modules; @@ -8,9 +8,9 @@ use et_ws_server::config::Config; use et_ws_server::configure_app; use et_ws_service::load_registry; use tracing::{error, info}; +use tracing_actix_web::TracingLogger; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -mod otlp; mod tls; #[derive(Parser, Debug)] @@ -29,9 +29,9 @@ async fn main() -> Result<(), std::io::Error> { eprintln!("Starting with env vars {env:#?}"); - if let Some(otlp_config) = &env.otlp { + let otel_handles = if let Some(otlp_config) = &env.otlp { info!("OpenTelemetry configuration detected, initializing tracing..."); - let _provider = crate::otlp::init(otlp_config); + Some(et_otlp::init(otlp_config)) } else { info!("No OpenTelemetry configuration detected, using default tracing settings..."); tracing_subscriber::registry() @@ -41,7 +41,8 @@ async fn main() -> Result<(), std::io::Error> { ) .with(tracing_subscriber::fmt::layer()) .init(); - } + None + }; let network_ip = local_ip_address::local_ip() .map(|ip| ip.to_string()) @@ -89,8 +90,13 @@ async fn main() -> Result<(), std::io::Error> { let server = HttpServer::new(move || { let registry = agent_registry.clone(); let config = env.clone(); + // `TracingLogger` extracts the W3C `traceparent` header from + // incoming requests (via the `opentelemetry_0_31` feature) and uses + // it as the parent context of the per-request span — that's how + // traces propagate from the wasi-runner (or any client that injects + // `traceparent`) into the server. App::new() - .wrap(Logger::default()) + .wrap(TracingLogger::default()) .wrap( DefaultHeaders::new() .add(("Cross-Origin-Opener-Policy", "same-origin")) @@ -115,5 +121,11 @@ async fn main() -> Result<(), std::io::Error> { handle.stop(true).await; }); - server.await + let result = server.await; + // Flush batched spans/logs before exit; otherwise short-lived runs lose + // the tail of the trace. + if let Some(handles) = otel_handles { + handles.shutdown(); + } + result } diff --git a/services/ws-server/src/otlp.rs b/services/ws-server/src/otlp.rs deleted file mode 100644 index 9a67fda..0000000 --- a/services/ws-server/src/otlp.rs +++ /dev/null @@ -1,93 +0,0 @@ -use edge_toolkit::config::{OtlpConfig, OtlpProtocol}; -use opentelemetry::{KeyValue, trace::TracerProvider}; -use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; -use opentelemetry_otlp::{LogExporter, WithExportConfig, WithHttpConfig}; -use opentelemetry_sdk::logs::SdkLoggerProvider; -use opentelemetry_sdk::trace::SdkTracerProvider; -use opentelemetry_sdk::{Resource, propagation::TraceContextPropagator}; -use tracing::subscriber::set_global_default; -use tracing_opentelemetry::OpenTelemetryLayer; -use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt}; - -pub const RUST_LOG: &str = "RUST_LOG"; - -// Initialize OpenTelemetry. -pub fn init(config: &OtlpConfig) -> SdkTracerProvider { - tracing_log::LogTracer::init().unwrap(); - - let mut telemetry_collector_headers = std::collections::HashMap::new(); - if let Some(auth) = &config.auth { - auth.add_basic_auth_header(&mut telemetry_collector_headers); - } - - opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new()); - let trace_endpoint = format!("{}/traces", config.collector_url.clone()); - let log_endpoint = format!("{}/logs", config.collector_url.clone()); - - let protocol = match config.protocol { - OtlpProtocol::Binary => opentelemetry_otlp::Protocol::HttpBinary, - OtlpProtocol::JSON => opentelemetry_otlp::Protocol::HttpJson, - }; - - let otlp_exporter = opentelemetry_otlp::SpanExporter::builder() - .with_http() - .with_protocol(protocol) - .with_endpoint(trace_endpoint) - .with_headers(telemetry_collector_headers.clone()) - .build() - .unwrap(); - - let mut service_descriptors = vec![KeyValue::new("service.version", env!("CARGO_PKG_VERSION").to_string())]; - if let Some(hostname) = hostname::get().ok().and_then(|h| h.into_string().ok()) { - service_descriptors.push(KeyValue::new("service.instance", hostname)); - } - - let resource = Resource::builder() - .with_service_name(config.service_label.clone()) - .with_attributes(service_descriptors) - .build(); - - let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() - .with_batch_exporter(otlp_exporter) - .with_resource(resource.clone()) - .build(); - - let otel_tracing_layer = OpenTelemetryLayer::new(provider.tracer(config.service_label.clone())); - - let log_directives = if let Ok(level) = std::env::var(RUST_LOG) { - log::info!("{RUST_LOG}={level}"); - level - } else { - log::info!("{RUST_LOG} defaulted to info"); - "info".to_string() - }; - let env_filter = EnvFilter::try_new(log_directives).unwrap(); - - let exporter = LogExporter::builder() - .with_http() - .with_protocol(protocol) - .with_endpoint(log_endpoint) - .with_headers(telemetry_collector_headers) - .build() - .unwrap(); - - let log_provider = SdkLoggerProvider::builder() - .with_batch_exporter(exporter) - .with_resource(resource) - .build(); - - let otel_layer = OpenTelemetryTracingBridge::new(&log_provider); - - let stdout_format = tracing_subscriber::fmt::format().compact(); - - let stdout_fmt_layer = tracing_subscriber::fmt::layer().event_format(stdout_format); - - let subscriber = Registry::default() - .with(env_filter) - .with(stdout_fmt_layer) - .with(otel_tracing_layer) - .with(otel_layer); - - set_global_default(subscriber).unwrap(); - provider -} diff --git a/services/ws-server/static/app.js b/services/ws-server/static/app.js index efa9f7d..0a434ab 100644 --- a/services/ws-server/static/app.js +++ b/services/ws-server/static/app.js @@ -204,21 +204,21 @@ try { client.set_on_state_change((state) => { append(`state: ${state}`); if (state === "connecting") { - updateAgentCard("Connecting to websocket server...", client.get_client_id() || readStoredAgentId()); + updateAgentCard("Connecting to websocket server...", client.get_agent_id() || readStoredAgentId()); } else if (state === "connected") { updateAgentCard( "Socket connected. Waiting for server identity acknowledgement...", - client.get_client_id() || readStoredAgentId(), + client.get_agent_id() || readStoredAgentId(), ); } else if (state === "reconnecting") { updateAgentCard( "Disconnected. Trying to re-use retained agent ID...", - client.get_client_id() || readStoredAgentId(), + client.get_agent_id() || readStoredAgentId(), ); } else if (state === "disconnected") { updateAgentCard( "Socket disconnected. Retained agent ID will be re-used on next connect.", - client.get_client_id() || readStoredAgentId(), + client.get_agent_id() || readStoredAgentId(), ); } }); @@ -232,9 +232,9 @@ try { retainedAgentId ? "Attempting websocket connect with retained agent ID from local storage." : "Attempting first websocket connect. Waiting for server-assigned agent ID.", - client.get_client_id() || retainedAgentId, + client.get_agent_id() || retainedAgentId, ); - append(`client_id: ${client.get_client_id() || "(awaiting server assignment)"}`); + append(`agent_id: ${client.get_agent_id() || "(awaiting server assignment)"}`); runModuleButton.addEventListener("click", async () => { const selectedModule = WORKFLOW_MODULES.get(moduleSelect.value); diff --git a/services/ws-test-server/Cargo.toml b/services/ws-test-server/Cargo.toml new file mode 100644 index 0000000..0c321f5 --- /dev/null +++ b/services/ws-test-server/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "et-ws-test-server" +description = "In-process ws-server for integration tests" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +actix.workspace = true +actix-rt.workspace = true +actix-web.workspace = true +et-modules-service = { path = "../modules" } +et-storage-service = { path = "../storage" } +et-ws-service = { path = "../ws" } +tempfile.workspace = true +# Same TracingLogger setup as the real ws-server, so tests that init OTLP +# in-process see server-side spans parented on the propagated traceparent. +tracing-actix-web.workspace = true diff --git a/services/ws-test-server/src/lib.rs b/services/ws-test-server/src/lib.rs new file mode 100644 index 0000000..e3b7ef4 --- /dev/null +++ b/services/ws-test-server/src/lib.rs @@ -0,0 +1,67 @@ +use std::net::TcpListener; + +use actix_web::{App, HttpServer, web}; +use et_modules_service::{ModulesConfig, configure as configure_modules}; +use et_storage_service::{StorageConfig, configure as configure_storage}; +use et_ws_service::{AgentSession, WsAgentRegistry, configure as configure_ws}; +use tempfile::TempDir; +use tracing_actix_web::TracingLogger; + +/// A running test server. The temporary storage directory is cleaned up on drop. +pub struct TestServer { + pub base_url: String, + pub ws_url: String, + pub storage_dir: TempDir, +} + +/// Start an in-process ws-server on a free port with a temporary storage directory. +/// +/// Serves modules from the default module paths (same as production). +pub fn start() -> TestServer { + let storage_dir = TempDir::new().expect("failed to create temp storage dir"); + let storage_path = storage_dir.path().to_path_buf(); + + // Bind to port 0 to get a free port, then drop the listener so the server can bind it. + let port = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); + + let storage_config = StorageConfig { path: storage_path }; + let modules_config = ModulesConfig::default(); + let addr = format!("127.0.0.1:{port}"); + + std::thread::spawn(move || { + actix_rt::System::new().block_on(async move { + let registry = web::Data::new(WsAgentRegistry::default()); + let storage = web::Data::new(storage_config); + let modules = modules_config; + HttpServer::new(move || { + // `TracingLogger` mirrors the real ws-server's pipeline: + // extracts `traceparent` from incoming requests so server + // spans are children of the caller's trace. + App::new() + .wrap(TracingLogger::default()) + .app_data(registry.clone()) + .app_data(storage.clone()) + .configure(configure_ws) + .configure(|cfg| configure_storage::(cfg, &storage)) + .configure(|cfg| configure_modules(cfg, &modules)) + }) + .bind(&addr) + .unwrap() + .run() + .await + .unwrap(); + }); + }); + + for _ in 0..50 { + if std::net::TcpStream::connect(format!("127.0.0.1:{port}")).is_ok() { + return TestServer { + base_url: format!("http://127.0.0.1:{port}"), + ws_url: format!("ws://127.0.0.1:{port}/ws"), + storage_dir, + }; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + panic!("test ws-server did not start within 5 seconds on port {port}"); +} diff --git a/services/ws-wasi-runner/Cargo.toml b/services/ws-wasi-runner/Cargo.toml new file mode 100644 index 0000000..459b483 --- /dev/null +++ b/services/ws-wasi-runner/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "et-ws-wasi-runner" +description = "Native runner that fetches a ws-module WASI component from the server and executes it via wasmtime" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lib] +name = "et_ws_wasi_runner" +path = "src/lib.rs" + +[[bin]] +name = "et-ws-wasi-runner" +path = "src/main.rs" + +[features] +default = [] +# Enable wasmtime-wasi-nn's `onnx-cuda` feature, which flips the cfg in its +# ONNX backend so `ExecutionTarget::Gpu` dispatches to `CUDAExecutionProvider` +# instead of warning and falling back to CPU. `onnx-cuda` transitively turns +# on `ort/cuda` too, so the EP is compiled into ort. Opt-in because +# `ort/download-binaries` then pulls the CUDA-flavoured ONNX Runtime prebuilt, +# which only exists for some host triples (Linux x86_64 reliably; Linux +# aarch64 / macOS are not covered by Pyke's mirror at this version). +# Build on CUDA hosts with: +# cargo build -p et-ws-wasi-runner --features cuda +cuda = ["wasmtime-wasi-nn/onnx-cuda"] + +[dependencies] +async-trait = "0.1" +bytemuck = { version = "1.16", features = ["derive"] } +edge-toolkit.workspace = true +et-otlp.workspace = true +futures-util = "0.3" +opentelemetry.workspace = true +opentelemetry-http = "0.31" +pollster = "0.4" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } +serde.workspace = true +serde-env.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "sync", "time"] } +tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect"] } +tracing.workspace = true +tracing-opentelemetry.workspace = true +tracing-subscriber.workspace = true +wasmtime = { version = "44", features = ["async", "component-model"] } +wasmtime-wasi = "44" +# wasi-nn standardised ML inference. `default-features = false` drops +# `openvino` + `winml` which we don't use and which need system libs. +wasmtime-wasi-nn = { version = "44", default-features = false, features = ["onnx"] } +# Pin `ort` to rc.10 because wasmtime-wasi-nn 44.0.1 was built against that +# specific prerelease; rc.11+ moved `ort::session::{Input, Output}` and +# `ort::tensor`, and the cargo resolver would otherwise pick the latest rc. +ort = { version = "=2.0.0-rc.10", default-features = false, features = ["copy-dylibs", "download-binaries"] } +wgpu = { version = "29", default-features = false, features = ["dx12", "metal", "vulkan", "wgsl"] } + +[dev-dependencies] +et-ws-test-server = { path = "../ws-test-server" } +otlp-mock = { path = "../../libs/otlp-mock" } +rstest.workspace = true diff --git a/services/ws-wasi-runner/src/host/log.rs b/services/ws-wasi-runner/src/host/log.rs new file mode 100644 index 0000000..f41d2ac --- /dev/null +++ b/services/ws-wasi-runner/src/host/log.rs @@ -0,0 +1,24 @@ +//! Implements `wasi:logging/logging`. Levels are routed into the `tracing` +//! macros so log lines flow through whatever subscriber the runner installed +//! (stdout fmt layer in dev, OTel logs in production). `context` is attached +//! as a structured field rather than baked into the message. + +use crate::HostState; +use crate::bindings::wasi::logging::logging::{Host, Level}; + +impl Host for HostState { + async fn log(&mut self, level: Level, context: String, message: String) { + match level { + Level::Trace => tracing::trace!(target: "wasi_logging", context = %context, "{message}"), + Level::Debug => tracing::debug!(target: "wasi_logging", context = %context, "{message}"), + Level::Info => tracing::info!(target: "wasi_logging", context = %context, "{message}"), + Level::Warn => tracing::warn!(target: "wasi_logging", context = %context, "{message}"), + Level::Error => tracing::error!(target: "wasi_logging", context = %context, "{message}"), + // `tracing` has no `critical` level. Route to error and tag the + // attribute so a log processor can distinguish if it cares. + Level::Critical => { + tracing::error!(target: "wasi_logging", context = %context, critical = true, "{message}") + } + } + } +} diff --git a/services/ws-wasi-runner/src/host/mod.rs b/services/ws-wasi-runner/src/host/mod.rs new file mode 100644 index 0000000..6f71171 --- /dev/null +++ b/services/ws-wasi-runner/src/host/mod.rs @@ -0,0 +1,61 @@ +//! Host-side implementation of the `et:ws-wasi` WIT world. +//! +//! `HostState` is the per-store object held by `wasmtime::Store`. It +//! owns the WASI Preview 2 context (for stdio/env/random/etc.), an HTTP client +//! used by the storage interface, the ws connection state, and the wgpu device +//! used by the gfx interface. + +use std::sync::Arc; + +use tokio::sync::Mutex; +use wasmtime::component::ResourceTable; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + +mod log; +pub mod wasi_keyvalue; +pub mod wasi_nn; +pub mod wasi_webgpu; +mod ws; + +pub use ws::WsBackend; + +pub struct HostState { + pub wasi_ctx: WasiCtx, + pub resource_table: ResourceTable, + + /// HTTP base of the ws-server (e.g. `http://localhost:8080`). + pub http_base: String, + /// WebSocket URL of the ws-server (e.g. `ws://localhost:8080/ws`). + pub ws_url: String, + + pub http: reqwest::Client, + pub ws: Arc>>, + /// wasi-nn context. Constructed once at startup so model loads + compute + /// reuse the same `ort` session pool across calls. + pub wasi_nn_ctx: wasmtime_wasi_nn::wit::WasiNnCtx, +} + +impl HostState { + pub async fn new(http_base: String, ws_url: String) -> Self { + let wasi_ctx = WasiCtxBuilder::new().inherit_stdio().inherit_env().build(); + + Self { + wasi_ctx, + resource_table: ResourceTable::new(), + http_base, + ws_url, + http: reqwest::Client::new(), + ws: Arc::new(Mutex::new(None)), + wasi_nn_ctx: wasi_nn::new_ctx(), + } + } +} + +impl WasiView for HostState { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi_ctx, + table: &mut self.resource_table, + } + } +} diff --git a/services/ws-wasi-runner/src/host/wasi_keyvalue.rs b/services/ws-wasi-runner/src/host/wasi_keyvalue.rs new file mode 100644 index 0000000..37f1602 --- /dev/null +++ b/services/ws-wasi-runner/src/host/wasi_keyvalue.rs @@ -0,0 +1,131 @@ +//! Implements `wasi:keyvalue/store` against the ws-server's storage and +//! modules services. The bucket identifier names a path-prefix: +//! +//! * `` → bucket prefix `/storage/{agent-uuid}/`. Reads work for +//! any agent's bucket (the server static-serves everything under +//! `/storage/`); writes only succeed when the runner's own agent owns the +//! bucket (server enforces `agent_id` is registered). +//! * `modules/` → bucket prefix `/modules//`. Used +//! by guests to fetch their own static assets bundled in `pkg/`. Writes +//! return `access-denied` since et-modules-service serves files static. +//! +//! The `Bucket` resource is just a thin owner of the prefix string; the +//! HTTP work happens in `get` / `set`. + +use wasmtime::component::Resource; + +use crate::HostState; +use crate::bindings::wasi::keyvalue::store::{Error, Host, HostBucket, KeyResponse}; + +pub struct Bucket { + /// URL path-prefix on the ws-server, including the leading slash and + /// trailing slash. Keys are appended verbatim. + prefix: String, + /// Whether this bucket accepts writes. False for `modules/...` buckets. + writable: bool, +} + +impl Bucket { + fn url(&self, http_base: &str, key: &str) -> String { + format!("{http_base}{}{key}", self.prefix) + } +} + +/// Map a `store.open` identifier to a bucket prefix and writability. +fn bucket_from_identifier(identifier: &str) -> Result { + if let Some(module_name) = identifier.strip_prefix("modules/") { + if module_name.is_empty() || module_name.contains('/') { + return Err(Error::Other(format!( + "invalid module bucket identifier: {identifier:?}" + ))); + } + return Ok(Bucket { + prefix: format!("/modules/{module_name}/"), + writable: false, + }); + } + if identifier.is_empty() || identifier.contains('/') { + return Err(Error::Other(format!("invalid bucket identifier: {identifier:?}"))); + } + Ok(Bucket { + prefix: format!("/storage/{identifier}/"), + writable: true, + }) +} + +impl Host for HostState { + async fn open(&mut self, identifier: String) -> Result, Error> { + let bucket = bucket_from_identifier(&identifier)?; + let res = self + .resource_table + .push(bucket) + .map_err(|e| Error::Other(format!("resource table push: {e}")))?; + Ok(res) + } +} + +impl HostBucket for HostState { + async fn get(&mut self, rep: Resource, key: String) -> Result>, Error> { + let bucket = self + .resource_table + .get(&rep) + .map_err(|e| Error::Other(format!("bucket handle: {e}")))?; + let url = bucket.url(&self.http_base, &key); + let resp = self + .http + .get(&url) + .send() + .await + .map_err(|e| Error::Other(format!("GET {url}: {e}")))?; + if resp.status() == reqwest::StatusCode::NOT_FOUND { + return Ok(None); + } + if !resp.status().is_success() { + return Err(Error::Other(format!("GET {url}: HTTP {}", resp.status()))); + } + let bytes = resp + .bytes() + .await + .map_err(|e| Error::Other(format!("GET {url} body: {e}")))?; + Ok(Some(bytes.to_vec())) + } + + async fn set(&mut self, rep: Resource, key: String, value: Vec) -> Result<(), Error> { + let bucket = self + .resource_table + .get(&rep) + .map_err(|e| Error::Other(format!("bucket handle: {e}")))?; + if !bucket.writable { + return Err(Error::AccessDenied); + } + let url = bucket.url(&self.http_base, &key); + let resp = self + .http + .put(&url) + .body(value) + .send() + .await + .map_err(|e| Error::Other(format!("PUT {url}: {e}")))?; + if !resp.status().is_success() { + return Err(Error::Other(format!("PUT {url}: HTTP {}", resp.status()))); + } + Ok(()) + } + + async fn delete(&mut self, _rep: Resource, _key: String) -> Result<(), Error> { + Err(Error::Other("delete not implemented".into())) + } + + async fn exists(&mut self, _rep: Resource, _key: String) -> Result { + Err(Error::Other("exists not implemented".into())) + } + + async fn list_keys(&mut self, _rep: Resource, _cursor: Option) -> Result { + Err(Error::Other("list-keys not implemented".into())) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} diff --git a/services/ws-wasi-runner/src/host/wasi_nn.rs b/services/ws-wasi-runner/src/host/wasi_nn.rs new file mode 100644 index 0000000..512f4bf --- /dev/null +++ b/services/ws-wasi-runner/src/host/wasi_nn.rs @@ -0,0 +1,32 @@ +//! Glue between `wasmtime-wasi-nn` and our `HostState`. +//! +//! `wasmtime-wasi-nn` ships its own `bindgen!`-generated implementation of the +//! `wasi:nn` WIT interfaces; we just need to construct a `WasiNnCtx` with the +//! ONNX backend, expose a `WasiNnView` from our `HostState`, and call its +//! `add_to_linker` so the linker accepts components that import `wasi:nn/*`. +//! +//! Backend choice: the runner builds with `wasmtime-wasi-nn`'s `onnx` feature, +//! which routes inference through `ort` (ONNX Runtime). On macOS that means +//! the system CoreML / Apple Accelerate paths; on Linux it picks up CUDA / +//! ROCm if the system ONNX Runtime was built with them, else CPU. +//! +//! Why no `wasi:webgpu` integration here: wasi-nn delegates to a host-chosen +//! ML runtime which manages its own GPU access. The trimmed `wasi:webgpu` +//! interface remains the path for "raw" GPU compute from guests; `wasi-nn` +//! is the path for "popular ML pattern" (load model, set input, compute, +//! get output). + +use wasmtime_wasi_nn::wit::{WasiNnCtx, WasiNnView}; + +/// Build a `WasiNnCtx` configured with whatever backends the crate's feature +/// flags enabled (just `onnx` for us). Empty registry — guests load model +/// bytes directly via `graph.load`, so name-based lookup isn't needed. +pub fn new_ctx() -> WasiNnCtx { + let backends = wasmtime_wasi_nn::backend::list(); + let registry = wasmtime_wasi_nn::Registry::from(wasmtime_wasi_nn::InMemoryRegistry::new()); + WasiNnCtx::new(backends, registry) +} + +pub fn view(state: &mut crate::HostState) -> WasiNnView<'_> { + WasiNnView::new(&mut state.resource_table, &mut state.wasi_nn_ctx) +} diff --git a/services/ws-wasi-runner/src/host/wasi_webgpu.rs b/services/ws-wasi-runner/src/host/wasi_webgpu.rs new file mode 100644 index 0000000..0cba6c3 --- /dev/null +++ b/services/ws-wasi-runner/src/host/wasi_webgpu.rs @@ -0,0 +1,1478 @@ +//! Host impl of the trimmed `wasi:webgpu` surface (see +//! `wit/deps/wasi-webgpu/webgpu.wit` for the subset). Only the matmul +//! critical path is wired through to real `wgpu` calls; every other kept +//! method traps with `unimplemented!`, so guests that stray off the path +//! fail loudly rather than silently misbehaving. +//! +//! Resource handles are stored in `HostState.resource_table` via wasmtime's +//! `Resource` machinery; the mapping from WIT resource names to the +//! payload types declared in this file is set up by the `with:` block of +//! the `wasmtime::component::bindgen!` invocation in `lib.rs`. +//! +//! Compute passes don't get their own live `wgpu::ComputePass` — that type +//! borrows the encoder mutably, which can't sit in a resource table. We +//! buffer pass commands on the encoder resource and replay them inside +//! `end()`, so the real `ComputePass` lives only for the duration of one +//! synchronous block. + +use std::collections::BTreeMap; +use std::sync::Arc; + +use wasmtime::component::Resource; + +use crate::HostState; +use crate::bindings::wasi::webgpu::webgpu::{ + self as wg, CreatePipelineError, CreatePipelineErrorKind, GetMappedRangeError, GetMappedRangeErrorKind, + GpuBindGroupDescriptor, GpuBindGroupLayoutDescriptor, GpuBufferDescriptor, GpuBufferMapState, + GpuComputePassDescriptor, GpuComputePipelineDescriptor, GpuLayoutMode, GpuPipelineErrorReason, + GpuPipelineLayoutDescriptor, GpuRequestAdapterOptions, GpuShaderModuleDescriptor, Host, HostGpu, HostGpuAdapter, + HostGpuAdapterInfo, HostGpuBindGroup, HostGpuBindGroupLayout, HostGpuBuffer, HostGpuBufferUsage, + HostGpuCommandBuffer, HostGpuCommandEncoder, HostGpuComputePassEncoder, HostGpuComputePipeline, HostGpuDevice, + HostGpuMapMode, HostGpuPipelineLayout, HostGpuQueue, HostGpuShaderModule, HostGpuShaderStage, + HostGpuSupportedFeatures, HostGpuSupportedLimits, HostRecordGpuPipelineConstantValue, HostRecordOptionGpuSize64, + MapAsyncError, MapAsyncErrorKind, RequestDeviceError, RequestDeviceErrorKind, SetBindGroupError, + SetBindGroupErrorKind, UnmapError, UnmapErrorKind, WriteBufferError, WriteBufferErrorKind, +}; + +/// wgpu buffer-usage flags as the host wire-format. The WIT-side +/// `gpu-buffer-usage.STORAGE()` style accessors return these constants and +/// the guest ORs them into `gpu-buffer-descriptor.usage`. Matches the +/// WebGPU spec values so we can hand them directly to `wgpu::BufferUsages`. +mod usage { + pub const MAP_READ: u32 = 0x0001; + pub const MAP_WRITE: u32 = 0x0002; + pub const COPY_SRC: u32 = 0x0004; + pub const COPY_DST: u32 = 0x0008; + pub const INDEX: u32 = 0x0010; + pub const VERTEX: u32 = 0x0020; + pub const UNIFORM: u32 = 0x0040; + pub const STORAGE: u32 = 0x0080; + pub const INDIRECT: u32 = 0x0100; + pub const QUERY_RESOLVE: u32 = 0x0200; +} + +/// gpu-map-mode flag bits (WebGPU spec values). +mod map_mode { + pub const READ: u32 = 0x0001; + pub const WRITE: u32 = 0x0002; +} + +/// gpu-shader-stage flag bits (WebGPU spec values). +mod shader_stage { + pub const VERTEX: u32 = 0x1; + pub const FRAGMENT: u32 = 0x2; + pub const COMPUTE: u32 = 0x4; +} + +/// Top-level handle: no per-instance state — `request-adapter` constructs a +/// fresh `wgpu::Instance` each call rather than sharing one across guests. +pub struct Gpu; + +pub struct GpuAdapter { + pub adapter: Arc, +} + +pub struct GpuAdapterInfo { + pub info: wgpu::AdapterInfo, +} + +pub struct GpuSupportedFeatures; +pub struct GpuSupportedLimits; + +pub struct GpuDevice { + pub device: Arc, + pub queue: Arc, +} + +pub struct GpuQueue { + pub device: Arc, + pub queue: Arc, +} + +pub struct GpuBuffer { + pub buffer: Arc, + pub device: Arc, + pub size: u64, + pub usage: u32, + pub map_state: GpuBufferMapState, +} + +pub struct GpuBufferUsage; +pub struct GpuMapMode; +pub struct GpuShaderStage; + +pub struct GpuBindGroupLayout { + pub layout: Arc, +} + +pub struct GpuBindGroup { + pub group: Arc, +} + +pub struct GpuPipelineLayout { + pub layout: Arc, +} + +pub struct GpuShaderModule { + pub module: Arc, +} + +pub struct GpuComputePipeline { + pub pipeline: Arc, +} + +/// Buffered command for a not-yet-replayed compute pass. We clone the wgpu +/// objects (all `Clone` thanks to wgpu's internal Arc-ing), so the pass can +/// be replayed inside `compute-pass.end()` without holding live borrows +/// across resource-table calls. +pub enum PassCommand { + SetPipeline(Arc), + SetBindGroup { + index: u32, + group: Arc, + offsets: Vec, + }, + DispatchWorkgroups(u32, u32, u32), +} + +pub struct GpuCommandEncoder { + pub device: Arc, + pub encoder: Option, + /// Set to `Some(vec)` while a compute pass is being recorded; replayed + /// against a freshly-opened `wgpu::ComputePass` when the pass's + /// `end()` is called, then taken back out. + pub pending_pass: Option>, +} + +/// Compute-pass resource: a tag pointing back at its parent encoder so +/// command-recording methods can find the pending command list. We use the +/// resource `rep()` (a u32 identity) rather than holding a `Resource<...>` +/// since the parent encoder might be looked up in either get/get_mut form. +pub struct GpuComputePassEncoder { + pub encoder_rep: u32, + pub ended: bool, +} + +pub struct GpuCommandBuffer { + pub buffer: Option, +} + +/// `record-option-gpu-size64` and `record-gpu-pipeline-constant-value` are +/// WIT-side associative maps the guest can build up to pass to +/// `gpu-device-descriptor` / `gpu-programmable-stage`. We never actually +/// consume them in the matmul path, but they need at least a working ctor +/// so the WIT round-trip doesn't trap. +pub struct RecordOptionGpuSize64 { + pub map: BTreeMap>, +} + +pub struct RecordGpuPipelineConstantValue { + pub map: BTreeMap, +} + +// ============================================================================= +// Adapter / device construction. The wasi-webgpu contract is that guests +// request these explicitly, so each `request-adapter` call builds its own +// `wgpu::Instance` and adapter. This is cheap (no compiled shaders cached +// at the instance level) and avoids long-lived host-side GPU state. +// ============================================================================= + +/// Build a fresh adapter from a new instance. `request-adapter` could be +/// called multiple times by a guest; each call gets its own adapter handle, +/// even if they all back onto the same underlying GPU. +async fn request_adapter_inner(_options: Option) -> Option { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + flags: wgpu::InstanceFlags::default(), + backend_options: wgpu::BackendOptions::default(), + memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), + display: None, + }); + instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + force_fallback_adapter: false, + }) + .await + .ok() +} + +fn buffer_usage_from_flags(flags: u32) -> wgpu::BufferUsages { + let mut out = wgpu::BufferUsages::empty(); + if flags & usage::MAP_READ != 0 { + out |= wgpu::BufferUsages::MAP_READ; + } + if flags & usage::MAP_WRITE != 0 { + out |= wgpu::BufferUsages::MAP_WRITE; + } + if flags & usage::COPY_SRC != 0 { + out |= wgpu::BufferUsages::COPY_SRC; + } + if flags & usage::COPY_DST != 0 { + out |= wgpu::BufferUsages::COPY_DST; + } + if flags & usage::INDEX != 0 { + out |= wgpu::BufferUsages::INDEX; + } + if flags & usage::VERTEX != 0 { + out |= wgpu::BufferUsages::VERTEX; + } + if flags & usage::UNIFORM != 0 { + out |= wgpu::BufferUsages::UNIFORM; + } + if flags & usage::STORAGE != 0 { + out |= wgpu::BufferUsages::STORAGE; + } + if flags & usage::INDIRECT != 0 { + out |= wgpu::BufferUsages::INDIRECT; + } + if flags & usage::QUERY_RESOLVE != 0 { + out |= wgpu::BufferUsages::QUERY_RESOLVE; + } + out +} + +fn shader_stages_from_flags(flags: u32) -> wgpu::ShaderStages { + let mut out = wgpu::ShaderStages::empty(); + if flags & shader_stage::VERTEX != 0 { + out |= wgpu::ShaderStages::VERTEX; + } + if flags & shader_stage::FRAGMENT != 0 { + out |= wgpu::ShaderStages::FRAGMENT; + } + if flags & shader_stage::COMPUTE != 0 { + out |= wgpu::ShaderStages::COMPUTE; + } + out +} + +fn buffer_binding_type(t: Option) -> wgpu::BufferBindingType { + match t.unwrap_or(wg::GpuBufferBindingType::Storage) { + wg::GpuBufferBindingType::Uniform => wgpu::BufferBindingType::Uniform, + wg::GpuBufferBindingType::Storage => wgpu::BufferBindingType::Storage { read_only: false }, + wg::GpuBufferBindingType::ReadOnlyStorage => wgpu::BufferBindingType::Storage { read_only: true }, + } +} + +// ============================================================================= +// Top-level interface `Host` — only `get_gpu` is exported as a free function. +// ============================================================================= + +impl Host for HostState { + async fn get_gpu(&mut self) -> Resource { + self.resource_table.push(Gpu).expect("resource table push") + } +} + +// ============================================================================= +// `gpu` resource — entry point for adapter requests. +// ============================================================================= + +impl HostGpu for HostState { + async fn request_adapter( + &mut self, + _gpu: Resource, + options: Option, + ) -> Option> { + let adapter = request_adapter_inner(options).await?; + let res = self + .resource_table + .push(GpuAdapter { + adapter: Arc::new(adapter), + }) + .expect("resource table push"); + Some(res) + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// ============================================================================= +// `gpu-adapter` — info + request-device. Other accessors trap. +// ============================================================================= + +impl HostGpuAdapter for HostState { + async fn info(&mut self, rep: Resource) -> Resource { + let adapter = self.resource_table.get(&rep).expect("adapter handle"); + let info = adapter.adapter.get_info(); + self.resource_table + .push(GpuAdapterInfo { info }) + .expect("resource table push") + } + + async fn request_device( + &mut self, + rep: Resource, + _descriptor: Option, + ) -> Result, RequestDeviceError> { + let adapter = self.resource_table.get(&rep).expect("adapter handle").adapter.clone(); + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + label: Some("wasi-webgpu host device"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::downlevel_defaults(), + memory_hints: wgpu::MemoryHints::default(), + trace: wgpu::Trace::Off, + experimental_features: wgpu::ExperimentalFeatures::default(), + }) + .await + .map_err(|e| RequestDeviceError { + kind: RequestDeviceErrorKind::OperationError, + message: format!("{e}"), + })?; + let device = Arc::new(device); + let queue = Arc::new(queue); + let res = self + .resource_table + .push(GpuDevice { + device: device.clone(), + queue: queue.clone(), + }) + .expect("resource table push"); + Ok(res) + } + + async fn features(&mut self, _rep: Resource) -> Resource { + unimplemented!("wasi-webgpu: adapter.features not implemented in matmul subset") + } + + async fn limits(&mut self, _rep: Resource) -> Resource { + unimplemented!("wasi-webgpu: adapter.limits not implemented in matmul subset") + } + + async fn is_fallback_adapter(&mut self, _rep: Resource) -> bool { + unimplemented!("wasi-webgpu: adapter.is-fallback-adapter not implemented in matmul subset") + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// ============================================================================= +// `gpu-adapter-info` — the four string accessors used by the guest's status +// reporting; the subgroup-size getters trap. +// ============================================================================= + +impl HostGpuAdapterInfo for HostState { + async fn vendor(&mut self, rep: Resource) -> String { + let info = &self.resource_table.get(&rep).expect("info handle").info; + vendor_name(info.vendor) + } + + async fn architecture(&mut self, rep: Resource) -> String { + let info = &self.resource_table.get(&rep).expect("info handle").info; + format!("{:?}", info.device_type).to_lowercase() + } + + async fn device(&mut self, rep: Resource) -> String { + let info = &self.resource_table.get(&rep).expect("info handle").info; + info.name.clone() + } + + async fn description(&mut self, rep: Resource) -> String { + let info = &self.resource_table.get(&rep).expect("info handle").info; + format!("{} ({:?}, {})", info.name, info.backend, info.driver_info) + } + + async fn subgroup_min_size(&mut self, _rep: Resource) -> u32 { + unimplemented!("wasi-webgpu: adapter-info.subgroup-min-size not implemented") + } + + async fn subgroup_max_size(&mut self, _rep: Resource) -> u32 { + unimplemented!("wasi-webgpu: adapter-info.subgroup-max-size not implemented") + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +/// Map a PCI vendor id (as wgpu reports it) to a human-friendly name. Falls +/// back to the raw hex so unknown vendors still surface usefully. +fn vendor_name(id: u32) -> String { + match id { + 0x1002 => "AMD".into(), + 0x10de => "NVIDIA".into(), + 0x8086 => "Intel".into(), + 0x106b => "Apple".into(), + 0x13b5 => "ARM".into(), + 0x5143 => "Qualcomm".into(), + 0 => "unknown".into(), + other => format!("0x{other:04x}"), + } +} + +// ============================================================================= +// `gpu-supported-features` / `gpu-supported-limits` — both stubs in this +// subset; the matmul flow doesn't query them. +// ============================================================================= + +impl HostGpuSupportedFeatures for HostState { + async fn has(&mut self, _rep: Resource, _value: String) -> bool { + unimplemented!("wasi-webgpu: supported-features.has not implemented in matmul subset") + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +impl HostGpuSupportedLimits for HostState { + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } + + async fn max_texture_dimension1_d(&mut self, _rep: Resource) -> u32 { + unimplemented!("wasi-webgpu: limits accessor not implemented in matmul subset") + } + async fn max_texture_dimension2_d(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_texture_dimension3_d(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_texture_array_layers(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_bind_groups(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_bind_groups_plus_vertex_buffers(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_bindings_per_bind_group(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_dynamic_uniform_buffers_per_pipeline_layout(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_dynamic_storage_buffers_per_pipeline_layout(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_sampled_textures_per_shader_stage(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_samplers_per_shader_stage(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_storage_buffers_per_shader_stage(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_storage_textures_per_shader_stage(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_uniform_buffers_per_shader_stage(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_uniform_buffer_binding_size(&mut self, _rep: Resource) -> u64 { + unimplemented!() + } + async fn max_storage_buffer_binding_size(&mut self, _rep: Resource) -> u64 { + unimplemented!() + } + async fn min_uniform_buffer_offset_alignment(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn min_storage_buffer_offset_alignment(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_vertex_buffers(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_buffer_size(&mut self, _rep: Resource) -> u64 { + unimplemented!() + } + async fn max_vertex_attributes(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_vertex_buffer_array_stride(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_inter_stage_shader_variables(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_color_attachments(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_color_attachment_bytes_per_sample(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_compute_workgroup_storage_size(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_compute_invocations_per_workgroup(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_compute_workgroup_size_x(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_compute_workgroup_size_y(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_compute_workgroup_size_z(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } + async fn max_compute_workgroups_per_dimension(&mut self, _rep: Resource) -> u32 { + unimplemented!() + } +} + +// ============================================================================= +// `gpu-device` — buffer / shader / pipeline / encoder / bind-group creation. +// ============================================================================= + +impl HostGpuDevice for HostState { + async fn queue(&mut self, rep: Resource) -> Resource { + let dev = self.resource_table.get(&rep).expect("device handle"); + let (device, queue) = (dev.device.clone(), dev.queue.clone()); + self.resource_table + .push(GpuQueue { device, queue }) + .expect("resource table push") + } + + async fn create_buffer( + &mut self, + rep: Resource, + descriptor: GpuBufferDescriptor, + ) -> Resource { + let dev = self.resource_table.get(&rep).expect("device handle"); + let device = dev.device.clone(); + let usage_flags = descriptor.usage; + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: descriptor.label.as_deref(), + size: descriptor.size, + usage: buffer_usage_from_flags(usage_flags), + mapped_at_creation: descriptor.mapped_at_creation.unwrap_or(false), + }); + let map_state = if descriptor.mapped_at_creation.unwrap_or(false) { + GpuBufferMapState::Mapped + } else { + GpuBufferMapState::Unmapped + }; + self.resource_table + .push(GpuBuffer { + buffer: Arc::new(buffer), + device, + size: descriptor.size, + usage: usage_flags, + map_state, + }) + .expect("resource table push") + } + + async fn create_bind_group_layout( + &mut self, + rep: Resource, + descriptor: GpuBindGroupLayoutDescriptor, + ) -> Resource { + let device = self.resource_table.get(&rep).expect("device handle").device.clone(); + let entries: Vec = descriptor + .entries + .into_iter() + .map(|e| { + let buffer = e + .buffer + .map(|b| wgpu::BindingType::Buffer { + ty: buffer_binding_type(b.type_), + has_dynamic_offset: b.has_dynamic_offset.unwrap_or(false), + min_binding_size: b.min_binding_size.and_then(std::num::NonZeroU64::new), + }) + .expect("matmul subset only uses buffer bindings"); + wgpu::BindGroupLayoutEntry { + binding: e.binding, + visibility: shader_stages_from_flags(e.visibility), + ty: buffer, + count: None, + } + }) + .collect(); + let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: descriptor.label.as_deref(), + entries: &entries, + }); + self.resource_table + .push(GpuBindGroupLayout { + layout: Arc::new(layout), + }) + .expect("resource table push") + } + + async fn create_pipeline_layout( + &mut self, + rep: Resource, + descriptor: GpuPipelineLayoutDescriptor, + ) -> Resource { + let device = self.resource_table.get(&rep).expect("device handle").device.clone(); + let layouts_owned: Vec> = descriptor + .bind_group_layouts + .iter() + .map(|b| { + let b = b.as_ref().expect("matmul: pipeline layout entries must be Some"); + self.resource_table + .get(b) + .expect("bind-group-layout handle") + .layout + .clone() + }) + .collect(); + let layout_refs: Vec> = layouts_owned.iter().map(|a| Some(a.as_ref())).collect(); + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: descriptor.label.as_deref(), + bind_group_layouts: &layout_refs, + immediate_size: 0, + }); + self.resource_table + .push(GpuPipelineLayout { + layout: Arc::new(layout), + }) + .expect("resource table push") + } + + async fn create_bind_group( + &mut self, + rep: Resource, + descriptor: GpuBindGroupDescriptor, + ) -> Resource { + let device = self.resource_table.get(&rep).expect("device handle").device.clone(); + let layout = self + .resource_table + .get(&descriptor.layout) + .expect("bind-group-layout handle") + .layout + .clone(); + // Two passes: first materialize the (buffer Arc, offset, size) tuples, + // then borrow the buffers into BindingResource::Buffer with lifetimes + // that outlive the create_bind_group call. + let mut buffer_keep: Vec<(Arc, u64, Option)> = + Vec::with_capacity(descriptor.entries.len()); + let mut bindings: Vec<(u32, usize)> = Vec::with_capacity(descriptor.entries.len()); + for e in &descriptor.entries { + match &e.resource { + wg::GpuBindingResource::GpuBufferBinding(b) => { + let buf = self + .resource_table + .get(&b.buffer) + .expect("buffer handle") + .buffer + .clone(); + let offset = b.offset.unwrap_or(0); + let size = b.size.and_then(std::num::NonZeroU64::new); + buffer_keep.push((buf, offset, size)); + bindings.push((e.binding, buffer_keep.len() - 1)); + } + } + } + let entries: Vec = bindings + .iter() + .map(|(binding, idx)| { + let (buf, offset, size) = &buffer_keep[*idx]; + wgpu::BindGroupEntry { + binding: *binding, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: buf.as_ref(), + offset: *offset, + size: *size, + }), + } + }) + .collect(); + let group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: descriptor.label.as_deref(), + layout: layout.as_ref(), + entries: &entries, + }); + self.resource_table + .push(GpuBindGroup { group: Arc::new(group) }) + .expect("resource table push") + } + + async fn create_shader_module( + &mut self, + rep: Resource, + descriptor: GpuShaderModuleDescriptor, + ) -> Resource { + let device = self.resource_table.get(&rep).expect("device handle").device.clone(); + let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: descriptor.label.as_deref(), + source: wgpu::ShaderSource::Wgsl(descriptor.code.into()), + }); + self.resource_table + .push(GpuShaderModule { + module: Arc::new(module), + }) + .expect("resource table push") + } + + async fn create_compute_pipeline( + &mut self, + rep: Resource, + descriptor: GpuComputePipelineDescriptor, + ) -> Resource { + let device = self.resource_table.get(&rep).expect("device handle").device.clone(); + let module = self + .resource_table + .get(&descriptor.compute.module) + .expect("shader-module handle") + .module + .clone(); + let layout: Option> = match &descriptor.layout { + GpuLayoutMode::Specific(l) => Some( + self.resource_table + .get(l) + .expect("pipeline-layout handle") + .layout + .clone(), + ), + GpuLayoutMode::Auto => None, + }; + let entry_point = descriptor.compute.entry_point.as_deref(); + let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: descriptor.label.as_deref(), + layout: layout.as_deref().map(|a| a as &wgpu::PipelineLayout), + module: module.as_ref(), + entry_point, + compilation_options: Default::default(), + cache: None, + }); + self.resource_table + .push(GpuComputePipeline { + pipeline: Arc::new(pipeline), + }) + .expect("resource table push") + } + + async fn create_command_encoder( + &mut self, + rep: Resource, + descriptor: Option, + ) -> Resource { + let device = self.resource_table.get(&rep).expect("device handle").device.clone(); + let label = descriptor.as_ref().and_then(|d| d.label.as_deref()); + let encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }); + self.resource_table + .push(GpuCommandEncoder { + device, + encoder: Some(encoder), + pending_pass: None, + }) + .expect("resource table push") + } + + async fn features(&mut self, _rep: Resource) -> Resource { + unimplemented!("wasi-webgpu: device.features not implemented in matmul subset") + } + async fn limits(&mut self, _rep: Resource) -> Resource { + unimplemented!("wasi-webgpu: device.limits not implemented in matmul subset") + } + async fn adapter_info(&mut self, _rep: Resource) -> Resource { + unimplemented!("wasi-webgpu: device.adapter-info not implemented in matmul subset") + } + async fn destroy(&mut self, _rep: Resource) { + unimplemented!("wasi-webgpu: device.destroy not implemented (use resource drop instead)") + } + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: device.label not implemented in matmul subset") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: device.set-label not implemented in matmul subset") + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// ============================================================================= +// `gpu-buffer` — map-async / get-mapped-range / unmap. wgpu's `map_async` +// callback fires when `device.poll(Wait)` runs; we do that synchronously +// from a blocking task so the host call returns after the readback is real. +// ============================================================================= + +impl HostGpuBuffer for HostState { + async fn map_async( + &mut self, + rep: Resource, + _mode: u32, + offset: Option, + size: Option, + ) -> Result<(), MapAsyncError> { + let buf = self.resource_table.get(&rep).expect("buffer handle"); + let buffer = buf.buffer.clone(); + let device = buf.device.clone(); + let total_size = buf.size; + let offset = offset.unwrap_or(0); + let size = size.unwrap_or(total_size - offset); + let result = tokio::task::spawn_blocking(move || { + let (tx, rx) = std::sync::mpsc::channel(); + buffer + .slice(offset..offset + size) + .map_async(wgpu::MapMode::Read, move |r| { + let _ = tx.send(r); + }); + device + .poll(wgpu::PollType::Wait { + submission_index: None, + timeout: None, + }) + .map_err(|e| format!("device poll: {e:?}"))?; + rx.recv() + .map_err(|e| format!("map_async channel: {e}"))? + .map_err(|e| format!("map_async: {e}")) + }) + .await; + match result { + Ok(Ok(())) => { + let buf = self.resource_table.get_mut(&rep).expect("buffer handle"); + buf.map_state = GpuBufferMapState::Mapped; + Ok(()) + } + Ok(Err(msg)) => Err(MapAsyncError { + kind: MapAsyncErrorKind::OperationError, + message: msg, + }), + Err(join_err) => Err(MapAsyncError { + kind: MapAsyncErrorKind::OperationError, + message: format!("map_async task join: {join_err}"), + }), + } + } + + async fn get_mapped_range_get_with_copy( + &mut self, + rep: Resource, + offset: Option, + size: Option, + ) -> Result, GetMappedRangeError> { + let buf = self.resource_table.get(&rep).expect("buffer handle"); + let offset = offset.unwrap_or(0); + let size = size.unwrap_or(buf.size - offset); + let slice = buf.buffer.slice(offset..offset + size); + let data = slice.get_mapped_range(); + let bytes = data.to_vec(); + drop(data); + Ok(bytes) + } + + async fn unmap(&mut self, rep: Resource) -> Result<(), UnmapError> { + let buf = self.resource_table.get_mut(&rep).expect("buffer handle"); + buf.buffer.unmap(); + buf.map_state = GpuBufferMapState::Unmapped; + Ok(()) + } + + async fn destroy(&mut self, rep: Resource) { + let buf = self.resource_table.get(&rep).expect("buffer handle"); + buf.buffer.destroy(); + } + + async fn size(&mut self, _rep: Resource) -> u64 { + unimplemented!("wasi-webgpu: buffer.size not implemented in matmul subset") + } + async fn usage(&mut self, _rep: Resource) -> u32 { + unimplemented!("wasi-webgpu: buffer.usage not implemented in matmul subset") + } + async fn map_state(&mut self, _rep: Resource) -> GpuBufferMapState { + unimplemented!("wasi-webgpu: buffer.map-state not implemented in matmul subset") + } + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: buffer.label not implemented in matmul subset") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: buffer.set-label not implemented in matmul subset") + } + async fn get_mapped_range_set_with_copy( + &mut self, + _rep: Resource, + _data: Vec, + _offset: Option, + _size: Option, + ) -> Result<(), GetMappedRangeError> { + unimplemented!("wasi-webgpu: buffer.get-mapped-range-set-with-copy not implemented in matmul subset") + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// ============================================================================= +// Static flag resources — bindgen surfaces these as methods on the host +// trait that take no `Resource<>` self handle. +// ============================================================================= + +impl HostGpuBufferUsage for HostState { + async fn map_read(&mut self) -> u32 { + usage::MAP_READ + } + async fn map_write(&mut self) -> u32 { + usage::MAP_WRITE + } + async fn copy_src(&mut self) -> u32 { + usage::COPY_SRC + } + async fn copy_dst(&mut self) -> u32 { + usage::COPY_DST + } + async fn index(&mut self) -> u32 { + usage::INDEX + } + async fn vertex(&mut self) -> u32 { + usage::VERTEX + } + async fn uniform(&mut self) -> u32 { + usage::UNIFORM + } + async fn storage(&mut self) -> u32 { + usage::STORAGE + } + async fn indirect(&mut self) -> u32 { + usage::INDIRECT + } + async fn query_resolve(&mut self) -> u32 { + usage::QUERY_RESOLVE + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +impl HostGpuMapMode for HostState { + async fn read(&mut self) -> u32 { + map_mode::READ + } + async fn write(&mut self) -> u32 { + map_mode::WRITE + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +impl HostGpuShaderStage for HostState { + async fn vertex(&mut self) -> u32 { + shader_stage::VERTEX + } + async fn fragment(&mut self) -> u32 { + shader_stage::FRAGMENT + } + async fn compute(&mut self) -> u32 { + shader_stage::COMPUTE + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// ============================================================================= +// Resources with only label/set-label survivors after trimming. +// ============================================================================= + +impl HostGpuBindGroupLayout for HostState { + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: bind-group-layout.label not implemented") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: bind-group-layout.set-label not implemented") + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +impl HostGpuBindGroup for HostState { + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: bind-group.label not implemented") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: bind-group.set-label not implemented") + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +impl HostGpuPipelineLayout for HostState { + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: pipeline-layout.label not implemented") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: pipeline-layout.set-label not implemented") + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +impl HostGpuShaderModule for HostState { + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: shader-module.label not implemented") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: shader-module.set-label not implemented") + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +impl HostGpuComputePipeline for HostState { + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: compute-pipeline.label not implemented") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: compute-pipeline.set-label not implemented") + } + async fn get_bind_group_layout( + &mut self, + _rep: Resource, + _index: u32, + ) -> Resource { + unimplemented!("wasi-webgpu: compute-pipeline.get-bind-group-layout not implemented in matmul subset") + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +impl HostGpuCommandBuffer for HostState { + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: command-buffer.label not implemented") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: command-buffer.set-label not implemented") + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// ============================================================================= +// `gpu-command-encoder` — begin-compute-pass starts a buffered pass; the +// pass commands are replayed against a real `wgpu::ComputePass` inside +// `compute-pass.end()`. copy-buffer-to-buffer and finish are straight +// passthroughs to the wgpu encoder. +// ============================================================================= + +impl HostGpuCommandEncoder for HostState { + async fn begin_compute_pass( + &mut self, + rep: Resource, + _descriptor: Option, + ) -> Resource { + let encoder_rep = rep.rep(); + let enc = self.resource_table.get_mut(&rep).expect("encoder handle"); + assert!( + enc.pending_pass.is_none(), + "wasi-webgpu: nested compute passes not supported" + ); + enc.pending_pass = Some(Vec::new()); + self.resource_table + .push(GpuComputePassEncoder { + encoder_rep, + ended: false, + }) + .expect("resource table push") + } + + async fn copy_buffer_to_buffer( + &mut self, + rep: Resource, + source: Resource, + source_offset: u64, + destination: Resource, + destination_offset: u64, + size: u64, + ) { + let src = self + .resource_table + .get(&source) + .expect("src buffer handle") + .buffer + .clone(); + let dst = self + .resource_table + .get(&destination) + .expect("dst buffer handle") + .buffer + .clone(); + let enc = self.resource_table.get_mut(&rep).expect("encoder handle"); + let encoder = enc.encoder.as_mut().expect("encoder already finished"); + encoder.copy_buffer_to_buffer(src.as_ref(), source_offset, dst.as_ref(), destination_offset, size); + } + + async fn finish( + &mut self, + rep: Resource, + _descriptor: Option, + ) -> Resource { + let enc = self.resource_table.get_mut(&rep).expect("encoder handle"); + let encoder = enc.encoder.take().expect("encoder already finished"); + let buffer = encoder.finish(); + self.resource_table + .push(GpuCommandBuffer { buffer: Some(buffer) }) + .expect("resource table push") + } + + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: command-encoder.label not implemented") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: command-encoder.set-label not implemented") + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// ============================================================================= +// `gpu-compute-pass-encoder` — record commands onto the parent encoder's +// pending list; `end()` opens a real wgpu pass and replays them. +// ============================================================================= + +fn encoder_from_pass_rep(state: &mut HostState, pass: &Resource) -> u32 { + state.resource_table.get(pass).expect("pass handle").encoder_rep +} + +impl HostGpuComputePassEncoder for HostState { + async fn set_pipeline(&mut self, rep: Resource, pipeline: Resource) { + let pipe = self + .resource_table + .get(&pipeline) + .expect("pipeline handle") + .pipeline + .clone(); + let encoder_rep = encoder_from_pass_rep(self, &rep); + let enc_handle: Resource = Resource::new_borrow(encoder_rep); + let enc = self.resource_table.get_mut(&enc_handle).expect("encoder handle"); + enc.pending_pass + .as_mut() + .expect("compute pass not active") + .push(PassCommand::SetPipeline(pipe)); + } + + async fn set_bind_group( + &mut self, + rep: Resource, + index: u32, + bind_group: Option>, + _dynamic_offsets_data: Option>, + _dynamic_offsets_data_start: Option, + _dynamic_offsets_data_length: Option, + ) -> Result<(), SetBindGroupError> { + let bg = bind_group.ok_or(SetBindGroupError { + kind: SetBindGroupErrorKind::RangeError, + message: "set-bind-group with None not supported in matmul subset".into(), + })?; + let group = self.resource_table.get(&bg).expect("bind-group handle").group.clone(); + let encoder_rep = encoder_from_pass_rep(self, &rep); + let enc_handle: Resource = Resource::new_borrow(encoder_rep); + let enc = self.resource_table.get_mut(&enc_handle).expect("encoder handle"); + enc.pending_pass + .as_mut() + .expect("compute pass not active") + .push(PassCommand::SetBindGroup { + index, + group, + offsets: Vec::new(), + }); + Ok(()) + } + + async fn dispatch_workgroups( + &mut self, + rep: Resource, + x: u32, + y: Option, + z: Option, + ) { + let encoder_rep = encoder_from_pass_rep(self, &rep); + let enc_handle: Resource = Resource::new_borrow(encoder_rep); + let enc = self.resource_table.get_mut(&enc_handle).expect("encoder handle"); + enc.pending_pass + .as_mut() + .expect("compute pass not active") + .push(PassCommand::DispatchWorkgroups(x, y.unwrap_or(1), z.unwrap_or(1))); + } + + async fn end(&mut self, rep: Resource) { + let encoder_rep = { + let pass = self.resource_table.get_mut(&rep).expect("pass handle"); + if pass.ended { + return; + } + pass.ended = true; + pass.encoder_rep + }; + let enc_handle: Resource = Resource::new_borrow(encoder_rep); + let commands = { + let enc = self.resource_table.get_mut(&enc_handle).expect("encoder handle"); + enc.pending_pass.take().expect("compute pass not active at end()") + }; + let enc = self.resource_table.get_mut(&enc_handle).expect("encoder handle"); + let encoder = enc.encoder.as_mut().expect("encoder already finished"); + let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("wasi-webgpu buffered pass"), + timestamp_writes: None, + }); + for cmd in commands { + match cmd { + PassCommand::SetPipeline(p) => pass.set_pipeline(p.as_ref()), + PassCommand::SetBindGroup { index, group, offsets } => { + pass.set_bind_group(index, group.as_ref(), &offsets); + } + PassCommand::DispatchWorkgroups(x, y, z) => pass.dispatch_workgroups(x, y, z), + } + } + } + + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: compute-pass.label not implemented") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: compute-pass.set-label not implemented") + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// ============================================================================= +// `gpu-queue` — submit + write-buffer-with-copy. +// ============================================================================= + +impl HostGpuQueue for HostState { + async fn submit(&mut self, rep: Resource, command_buffers: Vec>) { + let queue = self.resource_table.get(&rep).expect("queue handle").queue.clone(); + let mut buffers: Vec = Vec::with_capacity(command_buffers.len()); + for cb_res in command_buffers { + let cb = self + .resource_table + .get_mut(&cb_res) + .expect("command-buffer handle") + .buffer + .take() + .expect("command buffer already submitted"); + buffers.push(cb); + } + queue.submit(buffers); + } + + async fn write_buffer_with_copy( + &mut self, + rep: Resource, + buffer: Resource, + buffer_offset: u64, + data: Vec, + data_offset: Option, + size: Option, + ) -> Result<(), WriteBufferError> { + let queue = self.resource_table.get(&rep).expect("queue handle").queue.clone(); + let buf = self.resource_table.get(&buffer).expect("buffer handle").buffer.clone(); + let data_offset = data_offset.unwrap_or(0) as usize; + let end = match size { + Some(s) => data_offset + s as usize, + None => data.len(), + }; + if end > data.len() { + return Err(WriteBufferError { + kind: WriteBufferErrorKind::OperationError, + message: format!("data slice [{data_offset}..{end}] exceeds source len {}", data.len()), + }); + } + queue.write_buffer(buf.as_ref(), buffer_offset, &data[data_offset..end]); + Ok(()) + } + + async fn label(&mut self, _rep: Resource) -> String { + unimplemented!("wasi-webgpu: queue.label not implemented") + } + async fn set_label(&mut self, _rep: Resource, _label: String) { + unimplemented!("wasi-webgpu: queue.set-label not implemented") + } + + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// ============================================================================= +// Associative-record resources kept for WIT round-trip compatibility but +// not used by the matmul flow. +// ============================================================================= + +impl HostRecordOptionGpuSize64 for HostState { + async fn new(&mut self) -> Resource { + self.resource_table + .push(RecordOptionGpuSize64 { map: BTreeMap::new() }) + .expect("resource table push") + } + async fn add(&mut self, rep: Resource, key: String, value: Option) { + self.resource_table + .get_mut(&rep) + .expect("record handle") + .map + .insert(key, value); + } + async fn get(&mut self, rep: Resource, key: String) -> Option> { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .get(&key) + .copied() + } + async fn has(&mut self, rep: Resource, key: String) -> bool { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .contains_key(&key) + } + async fn remove(&mut self, rep: Resource, key: String) { + self.resource_table + .get_mut(&rep) + .expect("record handle") + .map + .remove(&key); + } + async fn keys(&mut self, rep: Resource) -> Vec { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .keys() + .cloned() + .collect() + } + async fn values(&mut self, rep: Resource) -> Vec> { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .values() + .copied() + .collect() + } + async fn entries(&mut self, rep: Resource) -> Vec<(String, Option)> { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .iter() + .map(|(k, v)| (k.clone(), *v)) + .collect() + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +impl HostRecordGpuPipelineConstantValue for HostState { + async fn new(&mut self) -> Resource { + self.resource_table + .push(RecordGpuPipelineConstantValue { map: BTreeMap::new() }) + .expect("resource table push") + } + async fn add(&mut self, rep: Resource, key: String, value: f64) { + self.resource_table + .get_mut(&rep) + .expect("record handle") + .map + .insert(key, value); + } + async fn get(&mut self, rep: Resource, key: String) -> Option { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .get(&key) + .copied() + } + async fn has(&mut self, rep: Resource, key: String) -> bool { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .contains_key(&key) + } + async fn remove(&mut self, rep: Resource, key: String) { + self.resource_table + .get_mut(&rep) + .expect("record handle") + .map + .remove(&key); + } + async fn keys(&mut self, rep: Resource) -> Vec { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .keys() + .cloned() + .collect() + } + async fn values(&mut self, rep: Resource) -> Vec { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .values() + .copied() + .collect() + } + async fn entries(&mut self, rep: Resource) -> Vec<(String, f64)> { + self.resource_table + .get(&rep) + .expect("record handle") + .map + .iter() + .map(|(k, v)| (k.clone(), *v)) + .collect() + } + async fn drop(&mut self, rep: Resource) -> wasmtime::Result<()> { + self.resource_table.delete(rep)?; + Ok(()) + } +} + +// Suppress unused-import warnings on error variants we only mention via the +// kept-but-unimplemented stubs above. Keeping the import list complete makes +// the file's WIT-mapped surface obvious at a glance. Static-allocated values +// avoid the const-drop restriction that bites composite types like +// `CreatePipelineError`. +const _: fn() = || { + let _ = CreatePipelineError { + kind: CreatePipelineErrorKind::GpuPipelineError(GpuPipelineErrorReason::Validation), + message: String::new(), + }; + let _ = GetMappedRangeErrorKind::OperationError; + let _ = UnmapErrorKind::AbortError; +}; diff --git a/services/ws-wasi-runner/src/host/ws.rs b/services/ws-wasi-runner/src/host/ws.rs new file mode 100644 index 0000000..581f93e --- /dev/null +++ b/services/ws-wasi-runner/src/host/ws.rs @@ -0,0 +1,186 @@ +//! Implements `et:ws-wasi/ws` using `tokio-tungstenite`. +//! +//! On `connect`, we open a websocket, send `WsMessage::Connect { agent_id: None }`, +//! and spawn a task that pumps inbound text messages into a channel. Inbound +//! `connect_ack` messages capture our assigned `agent_id`. +//! +//! `send-event` builds the same `WsMessage::ClientEvent` JSON shape the browser +//! `et-ws-wasm-agent` uses, so the server treats both client kinds identically. + +use std::sync::Arc; +use std::time::Duration; + +use edge_toolkit::ws::WsMessage; +use futures_util::SinkExt; +use futures_util::stream::{SplitSink, StreamExt}; +use tokio::net::TcpStream; +use tokio::sync::{Mutex, mpsc}; +use tokio::task::JoinHandle; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, tungstenite}; + +use crate::HostState; +use crate::bindings::et::ws_wasi::ws::{Host, State}; + +type WsSink = SplitSink>, tungstenite::Message>; + +/// Live state for an open websocket connection. Owned by `HostState` behind a +/// `Mutex`; replaced on disconnect. +pub struct WsBackend { + sink: Arc>, + inbox: Arc>>, + agent_id: Arc>>, + connection_state: Arc>, + _reader: JoinHandle<()>, +} + +impl WsBackend { + async fn connect(ws_url: &str) -> Result { + let (stream, _) = tokio_tungstenite::connect_async(ws_url) + .await + .map_err(|e| format!("ws connect {ws_url}: {e}"))?; + let (mut sink, mut stream) = stream.split(); + + // Drive the registration handshake immediately so the agent_id is + // known by the time `connect()` returns. + let connect_msg = serde_json::to_string(&WsMessage::Connect { agent_id: None }) + .map_err(|e| format!("serialize connect: {e}"))?; + sink.send(tungstenite::Message::text(connect_msg)) + .await + .map_err(|e| format!("send connect: {e}"))?; + + let (tx, rx) = mpsc::unbounded_channel::(); + let agent_id = Arc::new(Mutex::new(None)); + let connection_state = Arc::new(Mutex::new(State::Connecting)); + + // Reader pump: route ConnectAck into `agent_id` + `connection_state`, + // forward all other text messages to the guest via `inbox`. + let agent_id_clone = agent_id.clone(); + let state_clone = connection_state.clone(); + let reader = tokio::spawn(async move { + while let Some(msg) = stream.next().await { + let Ok(msg) = msg else { + break; + }; + let tungstenite::Message::Text(text) = msg else { + continue; + }; + let text = text.to_string(); + if let Ok(parsed) = serde_json::from_str::(&text) + && let WsMessage::ConnectAck { agent_id, .. } = &parsed + { + *agent_id_clone.lock().await = Some(agent_id.clone()); + *state_clone.lock().await = State::Connected; + } + if tx.send(text).is_err() { + break; + } + } + *state_clone.lock().await = State::Closed; + }); + + Ok(Self { + sink: Arc::new(Mutex::new(sink)), + inbox: Arc::new(Mutex::new(rx)), + agent_id, + connection_state, + _reader: reader, + }) + } + + async fn send_text(&self, text: String) -> Result<(), String> { + let mut sink = self.sink.lock().await; + sink.send(tungstenite::Message::text(text)) + .await + .map_err(|e| format!("send text: {e}")) + } + + async fn current_state(&self) -> State { + *self.connection_state.lock().await + } + + async fn current_agent_id(&self) -> String { + self.agent_id.lock().await.clone().unwrap_or_default() + } + + async fn recv(&self, timeout_ms: u32) -> Result, String> { + let mut inbox = self.inbox.lock().await; + match tokio::time::timeout(Duration::from_millis(timeout_ms as u64), inbox.recv()).await { + Ok(Some(text)) => Ok(Some(text)), + Ok(None) => Err("ws inbox closed".into()), + Err(_) => Ok(None), + } + } +} + +impl Host for HostState { + async fn connect(&mut self) -> Result<(), String> { + let mut slot = self.ws.lock().await; + if slot.is_some() { + return Err("already connected".into()); + } + let backend = WsBackend::connect(&self.ws_url).await?; + // Wait briefly for ConnectAck before returning, so guests can call + // agent_id() right after connect() and get a value. + for _ in 0..50 { + if matches!(backend.current_state().await, State::Connected) { + break; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + *slot = Some(backend); + Ok(()) + } + + async fn get_state(&mut self) -> State { + let slot = self.ws.lock().await; + match slot.as_ref() { + Some(b) => b.current_state().await, + None => State::Closed, + } + } + + async fn agent_id(&mut self) -> String { + let slot = self.ws.lock().await; + match slot.as_ref() { + Some(b) => b.current_agent_id().await, + None => String::new(), + } + } + + async fn send_event(&mut self, category: String, kind: String, body_json: String) -> Result<(), String> { + let body: serde_json::Value = + serde_json::from_str(&body_json).map_err(|e| format!("body-json is not valid JSON: {e}"))?; + let payload = serde_json::to_string(&WsMessage::ClientEvent { + capability: category, + action: kind, + details: body, + }) + .map_err(|e| format!("serialize client_event: {e}"))?; + self.send_text(payload).await + } + + async fn send_text(&mut self, text: String) -> Result<(), String> { + let slot = self.ws.lock().await; + let Some(backend) = slot.as_ref() else { + return Err("not connected".into()); + }; + backend.send_text(text).await + } + + async fn recv(&mut self, timeout_ms: u32) -> Result, String> { + let slot = self.ws.lock().await; + let Some(backend) = slot.as_ref() else { + return Err("not connected".into()); + }; + backend.recv(timeout_ms).await + } + + async fn disconnect(&mut self) { + let mut slot = self.ws.lock().await; + if let Some(backend) = slot.as_ref() { + *backend.connection_state.lock().await = State::Closing; + let _ = backend.sink.lock().await.close().await; + } + *slot = None; + } +} diff --git a/services/ws-wasi-runner/src/lib.rs b/services/ws-wasi-runner/src/lib.rs new file mode 100644 index 0000000..f3dd0d2 --- /dev/null +++ b/services/ws-wasi-runner/src/lib.rs @@ -0,0 +1,185 @@ +//! Native runner that executes ws-modules compiled to WASI Preview 2 components. +//! +//! Counterpart to `et-ws-worker`: that crate runs browser-targeted WASM modules +//! inside an embedded V8 (rustyscript). This crate runs WASI components inside +//! `wasmtime`, with host imports for ws-server interaction (websocket, storage, +//! logging, sleep) and a wgpu-backed trimmed `wasi:webgpu/webgpu` interface +//! (subset of WebAssembly/wasi-gfx) for real GPU compute access. +//! +//! See `wit/world.wit` for the host/guest contract. + +use opentelemetry_http::HeaderInjector; +use thiserror::Error; +use tracing::Instrument; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use wasmtime::component::{Component, HasSelf, Linker}; +use wasmtime::{Config, Engine, Store}; + +/// Errors `run_module` can fail with. `reqwest::Error` is forwarded +/// transparently — it already carries the URL it failed on. wasmtime's +/// `Error` (an alias for `anyhow::Error` upstream) doesn't nest cleanly +/// through `std::error::Error`, so the `From` impl flattens it to its +/// formatted chain via `{err:#}`. +#[derive(Debug, Error)] +pub enum RunnerError { + #[error("could not derive HTTP base from WS_SERVER_URL={ws_url}")] + InvalidWsUrl { ws_url: String }, + + #[error(transparent)] + Http(#[from] reqwest::Error), + + #[error("module {module} package.json missing `main` field")] + PackageJsonMissingMain { module: String }, + + #[error("wasm component model: {0}")] + Wasm(String), + + #[error("module run() returned err: {0}")] + Guest(String), +} + +impl From for RunnerError { + fn from(err: wasmtime::Error) -> Self { + RunnerError::Wasm(format!("{err:#}")) + } +} + +pub mod bindings { + wasmtime::component::bindgen!({ + path: "wit", + world: "runner", + imports: { default: async }, + exports: { default: async }, + // Map every wasi-webgpu resource to a payload type owned by us so + // resource_table operations work on real wgpu objects rather than + // bindgen-generated marker structs. The types live in + // `host::wasi_webgpu` and are wgpu-backed for the matmul subset. + with: { + "wasi:keyvalue/store.bucket": super::host::wasi_keyvalue::Bucket, + "wasi:webgpu/webgpu.gpu": super::host::wasi_webgpu::Gpu, + "wasi:webgpu/webgpu.gpu-adapter": super::host::wasi_webgpu::GpuAdapter, + "wasi:webgpu/webgpu.gpu-adapter-info": super::host::wasi_webgpu::GpuAdapterInfo, + "wasi:webgpu/webgpu.gpu-supported-features": super::host::wasi_webgpu::GpuSupportedFeatures, + "wasi:webgpu/webgpu.gpu-supported-limits": super::host::wasi_webgpu::GpuSupportedLimits, + "wasi:webgpu/webgpu.gpu-device": super::host::wasi_webgpu::GpuDevice, + "wasi:webgpu/webgpu.gpu-queue": super::host::wasi_webgpu::GpuQueue, + "wasi:webgpu/webgpu.gpu-buffer": super::host::wasi_webgpu::GpuBuffer, + "wasi:webgpu/webgpu.gpu-buffer-usage": super::host::wasi_webgpu::GpuBufferUsage, + "wasi:webgpu/webgpu.gpu-map-mode": super::host::wasi_webgpu::GpuMapMode, + "wasi:webgpu/webgpu.gpu-shader-stage": super::host::wasi_webgpu::GpuShaderStage, + "wasi:webgpu/webgpu.gpu-bind-group-layout": super::host::wasi_webgpu::GpuBindGroupLayout, + "wasi:webgpu/webgpu.gpu-bind-group": super::host::wasi_webgpu::GpuBindGroup, + "wasi:webgpu/webgpu.gpu-pipeline-layout": super::host::wasi_webgpu::GpuPipelineLayout, + "wasi:webgpu/webgpu.gpu-shader-module": super::host::wasi_webgpu::GpuShaderModule, + "wasi:webgpu/webgpu.gpu-compute-pipeline": super::host::wasi_webgpu::GpuComputePipeline, + "wasi:webgpu/webgpu.gpu-command-encoder": super::host::wasi_webgpu::GpuCommandEncoder, + "wasi:webgpu/webgpu.gpu-compute-pass-encoder": super::host::wasi_webgpu::GpuComputePassEncoder, + "wasi:webgpu/webgpu.gpu-command-buffer": super::host::wasi_webgpu::GpuCommandBuffer, + "wasi:webgpu/webgpu.record-option-gpu-size64": super::host::wasi_webgpu::RecordOptionGpuSize64, + "wasi:webgpu/webgpu.record-gpu-pipeline-constant-value": + super::host::wasi_webgpu::RecordGpuPipelineConstantValue, + }, + }); +} + +pub mod host; + +pub use host::HostState; + +/// Inject the W3C `traceparent` (and any `tracestate`) for the current span +/// into `req`. Downstream HTTP servers running `tracing-actix-web`'s +/// `TracingLogger` (or any propagator-aware middleware) parent their +/// request span on the value, which is how a single trace id covers both +/// processes. +fn inject_traceparent(req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + let mut headers = reqwest::header::HeaderMap::new(); + let cx = tracing::Span::current().context(); + opentelemetry::global::get_text_map_propagator(|propagator| { + propagator.inject_context(&cx, &mut HeaderInjector(&mut headers)); + }); + req.headers(headers) +} + +/// Convert a `ws://host[:port]/ws` URL to its `http://host[:port]` HTTP base +/// (or `wss://` → `https://`). Returns `None` if `ws_url` is not a websocket +/// URL. +pub fn derive_http_base(ws_url: &str) -> Option { + let (scheme, rest) = if let Some(r) = ws_url.strip_prefix("wss://") { + ("https", r) + } else if let Some(r) = ws_url.strip_prefix("ws://") { + ("http", r) + } else { + return None; + }; + let host_port = rest.strip_suffix("/ws").unwrap_or(rest); + Some(format!("{scheme}://{host_port}")) +} + +/// Where to find the .wasm component for a given module. +/// +/// Resolved against `package.json`'s `main` field as served by the ws-server. +async fn resolve_component_url(http_base: &str, module_name: &str) -> Result { + let pkg_url = format!("{http_base}/modules/{module_name}/package.json"); + let pkg: serde_json::Value = inject_traceparent(reqwest::Client::new().get(&pkg_url)) + .send() + .instrument(tracing::info_span!("fetch_package_json", url = %pkg_url)) + .await? + .error_for_status()? + .json() + .await?; + let main = pkg + .get("main") + .and_then(|v| v.as_str()) + .ok_or_else(|| RunnerError::PackageJsonMissingMain { + module: module_name.to_string(), + })?; + Ok(format!("{http_base}/modules/{module_name}/{main}")) +} + +/// Download, link, and run the WASI component for `module_name`. Returns when +/// the guest's exported `entry.run` finishes (either by returning `ok` or +/// trapping). Guest `err` returns are surfaced as `RunnerError::Guest`. +/// +/// The whole call is wrapped in a `run_module` span — every outgoing +/// request inherits its trace context, and ws-server's request span ends +/// up as a child of it. +pub async fn run_module(module_name: &str, ws_url: &str) -> Result<(), RunnerError> { + let span = tracing::info_span!("run_module", module = module_name); + run_module_inner(module_name, ws_url).instrument(span).await +} + +async fn run_module_inner(module_name: &str, ws_url: &str) -> Result<(), RunnerError> { + let http_base = derive_http_base(ws_url).ok_or_else(|| RunnerError::InvalidWsUrl { + ws_url: ws_url.to_string(), + })?; + + let wasm_url = resolve_component_url(&http_base, module_name).await?; + tracing::info!(%wasm_url, "fetching WASI component"); + let wasm_bytes = inject_traceparent(reqwest::Client::new().get(&wasm_url)) + .send() + .instrument(tracing::info_span!("fetch_component", url = %wasm_url)) + .await? + .error_for_status()? + .bytes() + .await?; + + let mut config = Config::new(); + config.wasm_component_model(true); + let engine = Engine::new(&config)?; + + let component = Component::from_binary(&engine, &wasm_bytes)?; + + let mut linker: Linker = Linker::new(&engine); + wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; + bindings::Runner::add_to_linker::>(&mut linker, |s| s)?; + wasmtime_wasi_nn::wit::add_to_linker(&mut linker, host::wasi_nn::view)?; + + let host_state = HostState::new(http_base, ws_url.to_string()).await; + let mut store = Store::new(&engine, host_state); + + let module = bindings::Runner::instantiate_async(&mut store, &component, &linker).await?; + + let guest_result = module.et_ws_wasi_entry().call_run(&mut store).await?; + + guest_result.map_err(RunnerError::Guest) +} diff --git a/services/ws-wasi-runner/src/main.rs b/services/ws-wasi-runner/src/main.rs new file mode 100644 index 0000000..84346f5 --- /dev/null +++ b/services/ws-wasi-runner/src/main.rs @@ -0,0 +1,45 @@ +use edge_toolkit::config::OtlpConfig; +use et_ws_wasi_runner::run_module; +use serde::Deserialize; +use tracing::info; + +/// Tiny envelope so the same `OTLP_*` env vars used by ws-server's `Config` +/// (deserialised via serde-env) work here too. +#[derive(Debug, Default, Deserialize)] +struct EnvConfig { + otlp: Option, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let env_config = serde_env::from_env::().unwrap_or_default(); + + let otel_handles = if let Some(otlp_config) = &env_config.otlp { + Some(et_otlp::init(otlp_config)) + } else { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())) + .init(); + None + }; + + let module_name = std::env::var("RUNNER_MODULE").map_err(|_| "RUNNER_MODULE not set")?; + let ws_url = std::env::var("WS_SERVER_URL").unwrap_or_else(|_| { + format!( + "ws://localhost:{}/ws", + edge_toolkit::ports::Services::InsecureWebSocketServer.port() + ) + }); + + info!("et-ws-wasi-runner: module={module_name} server={ws_url}"); + let result = run_module(&module_name, &ws_url).await; + + // Flush before exit so the mock OTLP collector sees the spans we emitted + // — `BatchExporter` would otherwise drop the tail when the process exits. + if let Some(handles) = otel_handles { + handles.shutdown(); + } + result?; + info!("module {module_name} completed successfully"); + Ok(()) +} diff --git a/services/ws-wasi-runner/tests/modules.rs b/services/ws-wasi-runner/tests/modules.rs new file mode 100644 index 0000000..194bdf6 --- /dev/null +++ b/services/ws-wasi-runner/tests/modules.rs @@ -0,0 +1,22 @@ +//! Integration tests: start a ws-server in-process and run each WASI module +//! via et-ws-wasi-runner. Mirror of `services/ws-worker/tests/modules.rs`'s +//! removed predecessor — same shape, but the spawned binary runs WASI +//! components rather than browser-targeted JS. +use rstest::rstest; + +#[rstest] +#[case::wasi_comm1("et-ws-wasi-comm1")] +#[case::wasi_data1("et-ws-wasi-data1")] +#[case::wasi_graphics_info("et-ws-wasi-graphics-info")] +fn module_runs_successfully(#[case] module: &str) { + let server = et_ws_test_server::start(); + + let bin = env!("CARGO_BIN_EXE_et-ws-wasi-runner"); + let status = std::process::Command::new(bin) + .env("RUNNER_MODULE", module) + .env("WS_SERVER_URL", &server.ws_url) + .status() + .expect("failed to spawn et-ws-wasi-runner"); + + assert!(status.success(), "{module} exited with code {:?}", status.code()); +} diff --git a/services/ws-wasi-runner/tests/otel_propagation.rs b/services/ws-wasi-runner/tests/otel_propagation.rs new file mode 100644 index 0000000..61377f5 --- /dev/null +++ b/services/ws-wasi-runner/tests/otel_propagation.rs @@ -0,0 +1,132 @@ +//! End-to-end trace-context propagation test. +//! +//! Wires up: +//! 1. A mock OTLP collector (in-process, on a free port). +//! 2. A ws-test-server (in-process, with OTLP init pointing at the mock, +//! so its `TracingLogger` middleware emits server spans here). +//! 3. The `et-ws-wasi-runner` binary spawned as a child process, with +//! `OTLP_COLLECTOR_URL` pointing at the same mock + service name set +//! to `et-ws-wasi-runner`. +//! +//! Then asserts that at least one trace id appears in spans from both the +//! `et-ws-server`/`et-ws-test` resource and the `et-ws-wasi-runner` +//! resource — i.e. the runner's outgoing `traceparent` was extracted by +//! the server's `TracingLogger`, so the two processes share a trace. +//! +//! The wasi-data1 module makes two HTTP calls (GET package.json, GET +//! .wasm) before exiting, so even though the test only runs one module, +//! we should see ≥2 server spans and ≥3 runner spans (the `run_module` +//! parent + two child fetch spans) on a successful run. It's used here +//! instead of wasi-graphics-info because it's the cheapest WASI module +//! to exercise — no wgpu / wasi-nn work. + +use std::collections::HashSet; +use std::time::Duration; + +use edge_toolkit::config::{OtlpConfig, OtlpProtocol}; + +#[test] +fn trace_ids_propagate_between_runner_and_server() { + // 1. Start the mock collector. Both processes will export to it. + let mock = otlp_mock::start(); + + // 2. Init OTLP in the test process *before* spawning the test server, + // so the global tracing subscriber + propagator are in place when + // actix-web's TracingLogger fires. + // + // The service.name lets us distinguish server-side spans from the + // runner subprocess's spans in the captured payloads. + // OtlpConfig is `non_exhaustive`, so build via Default + field + // assignment. + let mut server_otlp = OtlpConfig::default(); + server_otlp.collector_url = mock.collector_url.clone(); + server_otlp.protocol = OtlpProtocol::JSON; + server_otlp.service_label = "et-ws-test".to_string(); + server_otlp.auth = None; + let server_handles = et_otlp::init(&server_otlp); + + let server = et_ws_test_server::start(); + + // 3. Spawn the runner pointed at both the test server *and* the mock + // OTLP. Every `OTLP_*` env var is consumed by the runner's + // `serde_env::from_env::()` call. + let bin = env!("CARGO_BIN_EXE_et-ws-wasi-runner"); + let status = std::process::Command::new(bin) + .env("RUNNER_MODULE", "et-ws-wasi-data1") + .env("WS_SERVER_URL", &server.ws_url) + .env("OTLP_COLLECTOR_URL", &mock.collector_url) + .env("OTLP_PROTOCOL", "JSON") + .env("OTLP_SERVICE_LABEL", "et-ws-wasi-runner") + .status() + .expect("failed to spawn et-ws-wasi-runner"); + + assert!(status.success(), "runner exited with code {:?}", status.code()); + + // 4. Flush our own (server-side) batch exporter so any pending spans + // land in the mock before we read it. + server_handles.shutdown(); + // BatchExporter's HTTP POST is async-on-its-own-runtime — give it a + // moment to drain. The runner subprocess already shut its provider + // down before exiting (see services/ws-wasi-runner/src/main.rs). + std::thread::sleep(Duration::from_millis(500)); + + // 5. Inspect captured spans. + let spans = mock.flatten_spans(); + assert!( + !spans.is_empty(), + "mock OTLP received zero spans — exporters may not be flushing" + ); + + let trace_ids_by_service: std::collections::HashMap> = + spans.iter().fold(std::collections::HashMap::new(), |mut acc, span| { + acc.entry(span.service_name.clone()) + .or_default() + .insert(span.trace_id.clone()); + acc + }); + + let server_trace_ids = trace_ids_by_service.get("et-ws-test").cloned().unwrap_or_default(); + let runner_trace_ids = trace_ids_by_service + .get("et-ws-wasi-runner") + .cloned() + .unwrap_or_default(); + + assert!( + !server_trace_ids.is_empty(), + "no spans from `et-ws-test` service. captured: {:#?}", + trace_ids_by_service + ); + assert!( + !runner_trace_ids.is_empty(), + "no spans from `et-ws-wasi-runner` service. captured: {:#?}", + trace_ids_by_service + ); + + let shared: Vec<&String> = server_trace_ids.intersection(&runner_trace_ids).collect(); + assert!( + !shared.is_empty(), + concat!( + "no trace id was emitted by *both* processes — propagation failed.\n", + "server trace ids: {:?}\n", + "runner trace ids: {:?}", + ), + server_trace_ids, + runner_trace_ids, + ); + + // The server's request span should be a child of one of the runner's + // spans (parentSpanId points back into the runner's trace), proving + // the propagation direction (runner → server). + let server_with_parent = spans + .iter() + .filter(|s| s.service_name == "et-ws-test" && !s.parent_span_id.is_empty()) + .count(); + assert!( + server_with_parent > 0, + "no server span had a non-empty parentSpanId: TracingLogger didnt extract `traceparent`. server spans: {:#?}", + spans + .iter() + .filter(|s| s.service_name == "et-ws-test") + .collect::>() + ); +} diff --git a/services/ws-wasi-runner/tests/url_helpers.rs b/services/ws-wasi-runner/tests/url_helpers.rs new file mode 100644 index 0000000..339170f --- /dev/null +++ b/services/ws-wasi-runner/tests/url_helpers.rs @@ -0,0 +1,31 @@ +//! Smoke tests for the URL-handling helpers in `et-ws-wasi-runner`. +//! +//! Full integration with a real ws-server lives outside this crate (the +//! parent workspace's `et-ws-test-server` can't be pulled in here — see +//! `Cargo.toml`). When you want to run a module end-to-end: +//! mise run ws-server # in one terminal +//! mise run ws-wasi-runner # in another, with RUNNER_MODULE=wasi-graphics-info + +use et_ws_wasi_runner::derive_http_base; + +#[test] +fn derive_http_base_strips_ws_suffix() { + assert_eq!( + derive_http_base("ws://localhost:8080/ws"), + Some("http://localhost:8080".to_string()) + ); + assert_eq!( + derive_http_base("wss://example.com/ws"), + Some("https://example.com".to_string()) + ); + assert_eq!( + derive_http_base("ws://10.0.0.1:9000"), + Some("http://10.0.0.1:9000".to_string()) + ); +} + +#[test] +fn derive_http_base_rejects_non_ws_schemes() { + assert_eq!(derive_http_base("http://localhost:8080"), None); + assert_eq!(derive_http_base("not-a-url"), None); +} diff --git a/services/ws-wasi-runner/wit/deps/wasi-clocks/clocks.wit b/services/ws-wasi-runner/wit/deps/wasi-clocks/clocks.wit new file mode 100644 index 0000000..d638f1a --- /dev/null +++ b/services/ws-wasi-runner/wit/deps/wasi-clocks/clocks.wit @@ -0,0 +1,157 @@ +package wasi:clocks@0.2.6; + +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +@since(version = 0.2.0) +interface monotonic-clock { + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.2.0) + type instant = u64; + + /// A duration of time, in nanoseconds. + @since(version = 0.2.0) + type duration = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + @since(version = 0.2.0) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.2.0) + resolution: func() -> duration; + + /// Create a `pollable` which will resolve once the specified instant + /// has occurred. + @since(version = 0.2.0) + subscribe-instant: func(when: instant) -> pollable; + + /// Create a `pollable` that will resolve after the specified duration has + /// elapsed from the time this function is invoked. + @since(version = 0.2.0) + subscribe-duration: func(when: duration) -> pollable; +} + +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +@since(version = 0.2.0) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.2.0) + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.2.0) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.2.0) + resolution: func() -> datetime; +} + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; +} + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import wasi:io/poll@0.2.6; + @since(version = 0.2.0) + import monotonic-clock; + @since(version = 0.2.0) + import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/services/ws-wasi-runner/wit/deps/wasi-io/io.wit b/services/ws-wasi-runner/wit/deps/wasi-io/io.wit new file mode 100644 index 0000000..08ad78e --- /dev/null +++ b/services/ws-wasi-runner/wit/deps/wasi-io/io.wit @@ -0,0 +1,331 @@ +package wasi:io@0.2.6; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed, + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func(len: u64) -> result, stream-error>; + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func(len: u64) -> result, stream-error>; + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func(len: u64) -> result; + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func(len: u64) -> result; + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func(contents: list) -> result<_, stream-error>; + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func(len: u64) -> result<_, stream-error>; + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func(src: borrow, len: u64) -> result; + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func(src: borrow, len: u64) -> result; + } +} + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import error; + @since(version = 0.2.0) + import poll; + @since(version = 0.2.0) + import streams; +} diff --git a/services/ws-wasi-runner/wit/deps/wasi-keyvalue/store.wit b/services/ws-wasi-runner/wit/deps/wasi-keyvalue/store.wit new file mode 100644 index 0000000..840c2aa --- /dev/null +++ b/services/ws-wasi-runner/wit/deps/wasi-keyvalue/store.wit @@ -0,0 +1,65 @@ +// wasi:keyvalue (https://github.com/WebAssembly/wasi-keyvalue), 0.2.0-draft. +// Trimmed to the `store` interface (`open`, `bucket.get`, `bucket.set`, +// `bucket.delete`, `bucket.exists`, `bucket.list-keys`) — the atomics and +// batch interfaces from the upstream proposal are not currently wired. +// wasmtime-wasi does not ship a host impl; the runner implements it in +// `src/host/wasi_keyvalue.rs`, mapping bucket identifiers to URL prefixes +// on the ws-server. + +package wasi:keyvalue@0.2.0-draft; + +/// A keyvalue interface that provides simple get/set/delete operations. +interface store { + /// The set of errors which may be raised by functions in this package. + variant error { + /// The host does not recognize the store identifier requested. + no-such-store, + /// The requesting component does not have access to the specified store + /// (which may or may not exist). + access-denied, + /// Some implementation-specific error has occurred (e.g. I/O). + other(string), + } + + /// A bucket is a collection of key-value pairs. Each key-value pair is + /// stored as a discrete record. + resource bucket { + /// Get the value associated with the specified `key`. + /// + /// The value is returned as a list of bytes. The bytes are not + /// interpreted by the host so any serialization/deserialization is the + /// responsibility of the component. + /// + /// Returns `ok(none)` if the key does not exist. + get: func(key: string) -> result>, error>; + + /// Set the value associated with the key in the store. + /// + /// If the key already exists in the store, it is overwritten. + set: func(key: string, value: list) -> result<_, error>; + + /// Delete the key-value pair associated with the key in the store. + /// + /// If the key does not exist in the store, it is a no-op. + delete: func(key: string) -> result<_, error>; + + /// Check if the key exists in the store. + exists: func(key: string) -> result; + + /// Get all the keys in the store with an optional cursor (for + /// pagination). Returns a list of keys. + list-keys: func(cursor: option) -> result; + } + + /// A response to a `list-keys` operation. + record key-response { + /// The list of keys returned by the query. + keys: list, + /// The continuation token to use to fetch the next page of keys. If + /// this is null, then there are no more keys to fetch. + cursor: option, + } + + /// Get the bucket with the specified identifier. + open: func(identifier: string) -> result; +} diff --git a/services/ws-wasi-runner/wit/deps/wasi-logging/logging.wit b/services/ws-wasi-runner/wit/deps/wasi-logging/logging.wit new file mode 100644 index 0000000..b89a18e --- /dev/null +++ b/services/ws-wasi-runner/wit/deps/wasi-logging/logging.wit @@ -0,0 +1,38 @@ +// WASI Logging is a proposal for the standardised logging interface in WASI +// Preview 2. The proposal lives at https://github.com/WebAssembly/wasi-logging +// and is consumed by Spin, wasmCloud, and others. wasmtime-wasi does not ship +// a host impl, so the runner implements `logging.Host` itself in +// `src/host/log.rs`, routing levels through `tracing`. + +package wasi:logging@0.1.0-draft; + +/// WASI Logging is a logging API intended to let users emit log messages +/// with simple priority levels and context. +interface logging { + /// A log level, describing a kind of message. + enum level { + /// Describes messages about the values of variables and the flow of + /// control within a program. + trace, + /// Describes messages likely to be of interest to someone debugging a + /// program. + debug, + /// Describes messages likely to be of interest to someone monitoring a + /// program. + info, + /// Describes messages indicating hazardous situations. + warn, + /// Describes messages indicating serious errors. + error, + /// Describes messages indicating fatal errors. + critical, + } + + /// Emit a log message. + /// + /// A log message has a `level` describing what kind of message is being + /// sent, a `context`, which is an uninterpreted string meant to help + /// consumers group similar messages, and a string containing the message + /// text. + log: func(level: level, context: string, message: string); +} diff --git a/services/ws-wasi-runner/wit/deps/wasi-nn/wasi-nn.wit b/services/ws-wasi-runner/wit/deps/wasi-nn/wasi-nn.wit new file mode 100644 index 0000000..277f98d --- /dev/null +++ b/services/ws-wasi-runner/wit/deps/wasi-nn/wasi-nn.wit @@ -0,0 +1,160 @@ +package wasi:nn@0.2.0-rc-2024-10-28; + +/// `wasi-nn` is a WASI API for performing machine learning (ML) inference. The API is not (yet) +/// capable of performing ML training. WebAssembly programs that want to use a host's ML +/// capabilities can access these capabilities through `wasi-nn`'s core abstractions: _graphs_ and +/// _tensors_. A user `load`s an ML model -- instantiated as a _graph_ -- to use in an ML _backend_. +/// Then, the user passes _tensor_ inputs to the _graph_, computes the inference, and retrieves the +/// _tensor_ outputs. +/// +/// This example world shows how to use these primitives together. +world ml { + import tensor; + import graph; + import inference; + import errors; +} + +/// All inputs and outputs to an ML inference are represented as `tensor`s. +interface tensor { + /// The dimensions of a tensor. + /// + /// The array length matches the tensor rank and each element in the array describes the size of + /// each dimension + type tensor-dimensions = list; + + /// The type of the elements in a tensor. + enum tensor-type { + FP16, + FP32, + FP64, + BF16, + U8, + I32, + I64 + } + + /// The tensor data. + /// + /// Initially conceived as a sparse representation, each empty cell would be filled with zeros + /// and the array length must match the product of all of the dimensions and the number of bytes + /// in the type (e.g., a 2x2 tensor with 4-byte f32 elements would have a data array of length + /// 16). Naturally, this representation requires some knowledge of how to lay out data in + /// memory--e.g., using row-major ordering--and could perhaps be improved. + type tensor-data = list; + + resource tensor { + constructor(dimensions: tensor-dimensions, ty: tensor-type, data: tensor-data); + + // Describe the size of the tensor (e.g., 2x2x2x2 -> [2, 2, 2, 2]). To represent a tensor + // containing a single value, use `[1]` for the tensor dimensions. + dimensions: func() -> tensor-dimensions; + + // Describe the type of element in the tensor (e.g., `f32`). + ty: func() -> tensor-type; + + // Return the tensor data. + data: func() -> tensor-data; + } +} + +/// A `graph` is a loaded instance of a specific ML model (e.g., MobileNet) for a specific ML +/// framework (e.g., TensorFlow): +interface graph { + use errors.{error}; + use tensor.{tensor}; + use inference.{graph-execution-context}; + + /// An execution graph for performing inference (i.e., a model). + resource graph { + init-execution-context: func() -> result; + } + + /// Describes the encoding of the graph. This allows the API to be implemented by various + /// backends that encode (i.e., serialize) their graph IR with different formats. + enum graph-encoding { + openvino, + onnx, + tensorflow, + pytorch, + tensorflowlite, + ggml, + autodetect, + } + + /// Define where the graph should be executed. + enum execution-target { + cpu, + gpu, + tpu + } + + /// The graph initialization data. + /// + /// This gets bundled up into an array of buffers because implementing backends may encode their + /// graph IR in parts (e.g., OpenVINO stores its IR and weights separately). + type graph-builder = list; + + /// Load a `graph` from an opaque sequence of bytes to use for inference. + load: func(builder: list, encoding: graph-encoding, target: execution-target) -> result; + + /// Load a `graph` by name. + /// + /// How the host expects the names to be passed and how it stores the graphs for retrieval via + /// this function is **implementation-specific**. This allows hosts to choose name schemes that + /// range from simple to complex (e.g., URLs?) and caching mechanisms of various kinds. + load-by-name: func(name: string) -> result; +} + +/// An inference "session" is encapsulated by a `graph-execution-context`. This structure binds a +/// `graph` to input tensors before `compute`-ing an inference: +interface inference { + use errors.{error}; + use tensor.{tensor}; + + /// Identify a tensor by name; this is necessary to associate tensors to + /// graph inputs and outputs. + type named-tensor = tuple; + + /// Bind a `graph` to the input and output tensors for an inference. + /// + /// TODO: this may no longer be necessary in WIT + /// (https://github.com/WebAssembly/wasi-nn/issues/43) + resource graph-execution-context { + /// Compute the inference on the given inputs. + compute: func(inputs: list) -> result, error>; + } +} + +/// TODO: create function-specific errors (https://github.com/WebAssembly/wasi-nn/issues/42) +interface errors { + enum error-code { + // Caller module passed an invalid argument. + invalid-argument, + // Invalid encoding. + invalid-encoding, + // The operation timed out. + timeout, + // Runtime Error. + runtime-error, + // Unsupported operation. + unsupported-operation, + // Graph is too large. + too-large, + // Graph not found. + not-found, + // The operation is insecure or has insufficient privilege to be performed. + // e.g., cannot access a hardware feature requested + security, + // The operation failed for an unspecified reason. + unknown + } + + resource error { + /// Return the error code. + code: func() -> error-code; + + /// Errors can propagated with backend specific status through a string value. + data: func() -> string; + } +} diff --git a/services/ws-wasi-runner/wit/deps/wasi-webgpu/imports.wit b/services/ws-wasi-runner/wit/deps/wasi-webgpu/imports.wit new file mode 100644 index 0000000..774b702 --- /dev/null +++ b/services/ws-wasi-runner/wit/deps/wasi-webgpu/imports.wit @@ -0,0 +1,5 @@ +package wasi:webgpu@0.0.1; + +world imports { + import webgpu; +} diff --git a/services/ws-wasi-runner/wit/deps/wasi-webgpu/webgpu.wit b/services/ws-wasi-runner/wit/deps/wasi-webgpu/webgpu.wit new file mode 100644 index 0000000..1b1fd66 --- /dev/null +++ b/services/ws-wasi-runner/wit/deps/wasi-webgpu/webgpu.wit @@ -0,0 +1,480 @@ +package wasi:webgpu@0.0.1; + +// Trimmed from upstream WebAssembly/wasi-gfx@03c3e95 (main as of 2026-05-11). +// The preserved surface is what `services/ws-modules/wasi-graphics-info` needs +// to run a 4x4 compute matmul on the host GPU: request an adapter+device, +// create buffers / a shader module / a compute pipeline / bind group(s), +// submit a compute pass, read back results. Everything else (render pipelines, +// textures, samplers, canvas/surface presentation, render bundles, query sets, +// async pipeline creation, error-scope / lost-device machinery, debug markers, +// indirect dispatch) is stripped. Removed items are noted inline as +// `// TRIMMED: ...` so re-syncing with upstream stays mechanical. +// +// NOTE: this file is *not wire-compatible* with the unmodified wasi-webgpu; +// it's deliberately a subset. If wasi-gfx publishes on crates.io as proper +// versioned crates, replace this whole vendored tree with upstream WIT plus +// the matching wasmtime host crate instead of carrying the divergence. + +// TRIMMED: use wasi:io/poll@0.2.0.{ pollable } - only referenced by +// gpu-device.onuncapturederror-subscribe, which is trimmed. +// TRIMMED: use wasi:graphics-context/graphics-context@0.0.1.{ context, abstract-buffer } +// - feeds gpu-device.connect-graphics-context and +// gpu-texture.from-graphics-buffer, both trimmed. + +interface webgpu { + resource gpu-supported-limits { + max-texture-dimension1-d: func() -> u32; + max-texture-dimension2-d: func() -> u32; + max-texture-dimension3-d: func() -> u32; + max-texture-array-layers: func() -> u32; + max-bind-groups: func() -> u32; + max-bind-groups-plus-vertex-buffers: func() -> u32; + max-bindings-per-bind-group: func() -> u32; + max-dynamic-uniform-buffers-per-pipeline-layout: func() -> u32; + max-dynamic-storage-buffers-per-pipeline-layout: func() -> u32; + max-sampled-textures-per-shader-stage: func() -> u32; + max-samplers-per-shader-stage: func() -> u32; + max-storage-buffers-per-shader-stage: func() -> u32; + max-storage-textures-per-shader-stage: func() -> u32; + max-uniform-buffers-per-shader-stage: func() -> u32; + max-uniform-buffer-binding-size: func() -> u64; + max-storage-buffer-binding-size: func() -> u64; + min-uniform-buffer-offset-alignment: func() -> u32; + min-storage-buffer-offset-alignment: func() -> u32; + max-vertex-buffers: func() -> u32; + max-buffer-size: func() -> u64; + max-vertex-attributes: func() -> u32; + max-vertex-buffer-array-stride: func() -> u32; + max-inter-stage-shader-variables: func() -> u32; + max-color-attachments: func() -> u32; + max-color-attachment-bytes-per-sample: func() -> u32; + max-compute-workgroup-storage-size: func() -> u32; + max-compute-invocations-per-workgroup: func() -> u32; + max-compute-workgroup-size-x: func() -> u32; + max-compute-workgroup-size-y: func() -> u32; + max-compute-workgroup-size-z: func() -> u32; + max-compute-workgroups-per-dimension: func() -> u32; + } + resource gpu-supported-features { + has: func(value: string) -> bool; + } + // TRIMMED resource wgsl-language-features (queried via gpu.wgsl-language-features, trimmed). + resource gpu-adapter-info { + vendor: func() -> string; + architecture: func() -> string; + device: func() -> string; + description: func() -> string; + subgroup-min-size: func() -> u32; + subgroup-max-size: func() -> u32; + } + resource gpu { + request-adapter: func(options: option) -> option; + // TRIMMED: get-preferred-canvas-format (canvas presentation not used). + // TRIMMED: wgsl-language-features (no shader feature querying needed). + } + enum gpu-power-preference { + low-power, + high-performance, + } + record gpu-request-adapter-options { + feature-level: option, + power-preference: option, + force-fallback-adapter: option, + xr-compatible: option, + } + resource gpu-adapter { + features: func() -> gpu-supported-features; + limits: func() -> gpu-supported-limits; + info: func() -> gpu-adapter-info; + is-fallback-adapter: func() -> bool; + request-device: func(descriptor: option) -> result; + } + resource record-option-gpu-size64 { + constructor(); + add: func(key: string, value: option); + get: func(key: string) -> option>; + has: func(key: string) -> bool; + remove: func(key: string); + keys: func() -> list; + values: func() -> list>; + entries: func() -> list>>; + } + enum gpu-feature-name { + depth-clip-control, + depth32float-stencil8, + texture-compression-bc, + texture-compression-bc-sliced3d, + texture-compression-etc2, + texture-compression-astc, + texture-compression-astc-sliced3d, + timestamp-query, + indirect-first-instance, + shader-f16, + rg11b10ufloat-renderable, + bgra8unorm-storage, + float32-filterable, + float32-blendable, + clip-distances, + dual-source-blending, + subgroups, + } + resource gpu-device { + features: func() -> gpu-supported-features; + limits: func() -> gpu-supported-limits; + adapter-info: func() -> gpu-adapter-info; + queue: func() -> gpu-queue; + destroy: func(); + create-buffer: func(descriptor: gpu-buffer-descriptor) -> gpu-buffer; + // TRIMMED: create-texture (no texture pipeline in this subset). + // TRIMMED: create-sampler (textures/samplers trimmed). + create-bind-group-layout: func(descriptor: gpu-bind-group-layout-descriptor) -> gpu-bind-group-layout; + create-pipeline-layout: func(descriptor: gpu-pipeline-layout-descriptor) -> gpu-pipeline-layout; + create-bind-group: func(descriptor: gpu-bind-group-descriptor) -> gpu-bind-group; + create-shader-module: func(descriptor: gpu-shader-module-descriptor) -> gpu-shader-module; + create-compute-pipeline: func(descriptor: gpu-compute-pipeline-descriptor) -> gpu-compute-pipeline; + // TRIMMED: create-render-pipeline, create-compute-pipeline-async, + // create-render-pipeline-async (no render path; sync compute + // pipeline creation is enough for the matmul demo). + create-command-encoder: func(descriptor: option) -> gpu-command-encoder; + // TRIMMED: create-render-bundle-encoder, create-query-set + // (no rendering, no GPU timing queries). + label: func() -> string; + set-label: func(label: string); + // TRIMMED: lost, push-error-scope, pop-error-scope, + // onuncapturederror-subscribe (error-scope / device-lost flow not + // exposed; failures surface via the per-call result<_, *-error>). + // TRIMMED: connect-graphics-context (no presentation surface). + } + resource gpu-buffer { + size: func() -> gpu-size64-out; + usage: func() -> gpu-flags-constant; + map-state: func() -> gpu-buffer-map-state; + map-async: func(mode: gpu-map-mode-flags, offset: option, size: option) -> result<_, map-async-error>; + get-mapped-range-get-with-copy: func(offset: option, size: option) -> result, get-mapped-range-error>; + unmap: func() -> result<_, unmap-error>; + destroy: func(); + label: func() -> string; + set-label: func(label: string); + get-mapped-range-set-with-copy: func(data: list, offset: option, size: option) -> result<_, get-mapped-range-error>; + } + enum gpu-buffer-map-state { + unmapped, + pending, + mapped, + } + type gpu-buffer-usage-flags = u32; + resource gpu-buffer-usage { + MAP-READ: static func() -> gpu-flags-constant; + MAP-WRITE: static func() -> gpu-flags-constant; + COPY-SRC: static func() -> gpu-flags-constant; + COPY-DST: static func() -> gpu-flags-constant; + INDEX: static func() -> gpu-flags-constant; + VERTEX: static func() -> gpu-flags-constant; + UNIFORM: static func() -> gpu-flags-constant; + STORAGE: static func() -> gpu-flags-constant; + INDIRECT: static func() -> gpu-flags-constant; + QUERY-RESOLVE: static func() -> gpu-flags-constant; + } + type gpu-map-mode-flags = u32; + resource gpu-map-mode { + READ: static func() -> gpu-flags-constant; + WRITE: static func() -> gpu-flags-constant; + } + // TRIMMED resource gpu-texture and its descriptors/views/usage/dimension/ + // aspect/format enums (no textures in the compute-only subset). + // TRIMMED enum gpu-texture-dimension, gpu-texture-view-dimension, + // gpu-texture-aspect, gpu-texture-format (96 variants). + // TRIMMED type gpu-texture-usage-flags, resource gpu-texture-usage, + // resource gpu-texture-view. + // TRIMMED resource gpu-sampler and its descriptor/binding/layout records + // plus gpu-address-mode, gpu-filter-mode, gpu-mipmap-filter-mode, + // gpu-compare-function, gpu-sampler-binding-type, + // gpu-texture-sample-type, gpu-storage-texture-access. + resource gpu-bind-group-layout { + label: func() -> string; + set-label: func(label: string); + } + type gpu-shader-stage-flags = u32; + resource gpu-shader-stage { + VERTEX: static func() -> gpu-flags-constant; + FRAGMENT: static func() -> gpu-flags-constant; + COMPUTE: static func() -> gpu-flags-constant; + } + enum gpu-buffer-binding-type { + uniform, + storage, + read-only-storage, + } + // TRIMMED record gpu-sampler-binding-layout, gpu-texture-binding-layout, + // gpu-storage-texture-binding-layout (only buffer bindings used). + resource gpu-bind-group { + label: func() -> string; + set-label: func(label: string); + } + resource gpu-pipeline-layout { + label: func() -> string; + set-label: func(label: string); + } + record gpu-pipeline-layout-descriptor { + bind-group-layouts: list>>, + label: option, + } + resource gpu-shader-module { + label: func() -> string; + set-label: func(label: string); + // TRIMMED: get-compilation-info (no compile-time diagnostic surfacing; + // shader errors trap during create-compute-pipeline instead). + } + // TRIMMED resource gpu-compilation-message, gpu-compilation-info, + // enum gpu-compilation-message-type (paired with get-compilation-info). + enum gpu-pipeline-error-reason { + validation, + internal, + } + variant gpu-layout-mode { + specific(borrow), + auto, + } + record gpu-shader-module-compilation-hint { + entry-point: string, + layout: option, + } + record gpu-shader-module-descriptor { + code: string, + compilation-hints: option>, + label: option, + } + resource record-gpu-pipeline-constant-value { + constructor(); + add: func(key: string, value: gpu-pipeline-constant-value); + get: func(key: string) -> option; + has: func(key: string) -> bool; + remove: func(key: string); + keys: func() -> list; + values: func() -> list; + entries: func() -> list>; + } + record gpu-programmable-stage { + module: borrow, + entry-point: option, + constants: option, + } + type gpu-pipeline-constant-value = f64; + resource gpu-compute-pipeline { + label: func() -> string; + set-label: func(label: string); + get-bind-group-layout: func(index: u32) -> gpu-bind-group-layout; + } + record gpu-compute-pipeline-descriptor { + compute: gpu-programmable-stage, + layout: gpu-layout-mode, + label: option, + } + // TRIMMED resource gpu-render-pipeline plus the entire render pipeline + // descriptor tree: gpu-render-pipeline-descriptor, gpu-vertex-state, + // gpu-vertex-buffer-layout, gpu-vertex-attribute, gpu-fragment-state, + // gpu-color-target-state, gpu-blend-state, gpu-blend-component, + // gpu-primitive-state, gpu-depth-stencil-state, gpu-stencil-face-state, + // gpu-multisample-state; and their supporting enums + // (gpu-primitive-topology, gpu-front-face, gpu-cull-mode, + // gpu-blend-factor, gpu-blend-operation, gpu-stencil-operation, + // gpu-index-format, gpu-vertex-format, gpu-vertex-step-mode); + // and gpu-color-write flags resource / type. + resource gpu-command-buffer { + label: func() -> string; + set-label: func(label: string); + } + record gpu-command-buffer-descriptor { + label: option, + } + resource gpu-command-encoder { + // TRIMMED: begin-render-pass (no render passes). + begin-compute-pass: func(descriptor: option) -> gpu-compute-pass-encoder; + copy-buffer-to-buffer: func(source: borrow, source-offset: gpu-size64, destination: borrow, destination-offset: gpu-size64, size: gpu-size64); + // TRIMMED: copy-buffer-to-texture, copy-texture-to-buffer, + // copy-texture-to-texture (no textures in this subset). + // TRIMMED: clear-buffer (not used; write-buffer-with-copy serves init). + // TRIMMED: resolve-query-set (no query sets). + finish: func(descriptor: option) -> gpu-command-buffer; + label: func() -> string; + set-label: func(label: string); + // TRIMMED: push-debug-group, pop-debug-group, insert-debug-marker + // (debug markers unused; can be re-added without functional impact). + } + record gpu-command-encoder-descriptor { + label: option, + } + resource gpu-compute-pass-encoder { + set-pipeline: func(pipeline: borrow); + dispatch-workgroups: func(workgroup-count-x: gpu-size32, workgroup-count-y: option, workgroup-count-z: option); + // TRIMMED: dispatch-workgroups-indirect (no indirect buffers). + end: func(); + label: func() -> string; + set-label: func(label: string); + // TRIMMED: push-debug-group, pop-debug-group, insert-debug-marker. + set-bind-group: func(index: gpu-index32, bind-group: option>, dynamic-offsets-data: option>, dynamic-offsets-data-start: option, dynamic-offsets-data-length: option) -> result<_, set-bind-group-error>; + } + // TRIMMED resource gpu-render-pass-encoder and all render-pass primitives + // (set-viewport, set-scissor-rect, set-blend-constant, + // set-stencil-reference, begin/end-occlusion-query, execute-bundles, + // set-index-buffer, set-vertex-buffer, draw / draw-indexed / + // draw-indirect / draw-indexed-indirect). + // TRIMMED enum gpu-load-op, gpu-store-op (only used by render-pass attachments). + // TRIMMED resource gpu-render-bundle, gpu-render-bundle-encoder and their + // descriptors gpu-render-bundle-descriptor, + // gpu-render-bundle-encoder-descriptor. + record gpu-queue-descriptor { + label: option, + } + record gpu-device-descriptor { + required-features: option>, + required-limits: option, + default-queue: option, + label: option, + } + resource gpu-queue { + submit: func(command-buffers: list>); + // TRIMMED: on-submitted-work-done (no completion pollables in this subset). + write-buffer-with-copy: func(buffer: borrow, buffer-offset: gpu-size64, data: list, data-offset: option, size: option) -> result<_, write-buffer-error>; + // TRIMMED: write-texture-with-copy (no textures). + label: func() -> string; + set-label: func(label: string); + } + // TRIMMED resource gpu-query-set, enum gpu-query-type, + // record gpu-query-set-descriptor (no GPU-side timestamping queries). + // TRIMMED resource gpu-canvas-context and all canvas presentation records: + // gpu-canvas-configuration, gpu-canvas-configuration-owned, + // gpu-canvas-tone-mapping, gpu-canvas-alpha-mode, + // gpu-canvas-tone-mapping-mode, predefined-color-space. + // TRIMMED enum gpu-device-lost-reason, resource gpu-device-lost-info + // (device.lost trimmed). + // TRIMMED resource gpu-error, enum gpu-error-filter, variant gpu-error-kind, + // resource gpu-uncaptured-error-event, record pop-error-scope-error, + // variant pop-error-scope-error-kind (error-scope flow trimmed). + type gpu-buffer-dynamic-offset = u32; + // TRIMMED type gpu-stencil-value, gpu-sample-mask, gpu-depth-bias, + // gpu-signed-offset32, gpu-integer-coordinate, gpu-integer-coordinate-out + // (only referenced by render / texture / query primitives). + // TRIMMED record gpu-render-pass-depth-stencil-attachment, + // gpu-render-pass-color-attachment, gpu-render-pass-descriptor, + // gpu-render-pass-timestamp-writes. + type gpu-size64 = u64; + record gpu-buffer-descriptor { + size: gpu-size64, + usage: gpu-buffer-usage-flags, + mapped-at-creation: option, + label: option, + } + record gpu-buffer-binding-layout { + %type: option, + has-dynamic-offset: option, + min-binding-size: option, + } + record gpu-buffer-binding { + buffer: borrow, + offset: option, + size: option, + } + variant gpu-binding-resource { + gpu-buffer-binding(gpu-buffer-binding), + // TRIMMED: gpu-sampler(borrow), + // gpu-texture-view(borrow) - resources trimmed. + } + // TRIMMED record gpu-texture-view-descriptor, gpu-texture-descriptor, + // gpu-texel-copy-buffer-layout, gpu-texel-copy-buffer-info, + // gpu-texel-copy-texture-info, gpu-origin3-d, gpu-extent3-d. + type gpu-index32 = u32; + record gpu-bind-group-layout-entry { + binding: gpu-index32, + visibility: gpu-shader-stage-flags, + buffer: option, + // TRIMMED: sampler: option, + // texture: option, + // storage-texture: option + // - non-buffer binding kinds not used. + } + record gpu-bind-group-layout-descriptor { + entries: list, + label: option, + } + record gpu-bind-group-entry { + binding: gpu-index32, + %resource: gpu-binding-resource, + } + record gpu-bind-group-descriptor { + layout: borrow, + entries: list, + label: option, + } + // TRIMMED record gpu-vertex-attribute, gpu-vertex-buffer-layout, + // gpu-vertex-state, gpu-multisample-state, + // gpu-render-pipeline-descriptor (render path). + // TRIMMED record gpu-compute-pass-timestamp-writes (no query sets to write to). + record gpu-compute-pass-descriptor { + // TRIMMED: timestamp-writes: option + // - referenced gpu-query-set which is trimmed. + label: option, + } + type gpu-size32 = u32; + type gpu-size64-out = u64; + type gpu-size32-out = u32; + type gpu-flags-constant = u32; + // TRIMMED record gpu-color (only used by render-pass blend constants). + get-gpu: func() -> gpu; + variant request-device-error-kind { + type-error, + operation-error, + } + record request-device-error { + kind: request-device-error-kind, + message: string, + } + variant create-pipeline-error-kind { + gpu-pipeline-error(gpu-pipeline-error-reason), + } + record create-pipeline-error { + kind: create-pipeline-error-kind, + message: string, + } + // TRIMMED variant create-query-set-error-kind, record create-query-set-error + // (no create-query-set). + variant map-async-error-kind { + operation-error, + range-error, + abort-error, + } + record map-async-error { + kind: map-async-error-kind, + message: string, + } + variant get-mapped-range-error-kind { + operation-error, + range-error, + type-error, + } + record get-mapped-range-error { + kind: get-mapped-range-error-kind, + message: string, + } + variant unmap-error-kind { + abort-error, + } + record unmap-error { + kind: unmap-error-kind, + message: string, + } + variant set-bind-group-error-kind { + range-error, + } + record set-bind-group-error { + kind: set-bind-group-error-kind, + message: string, + } + variant write-buffer-error-kind { + operation-error, + } + record write-buffer-error { + kind: write-buffer-error-kind, + message: string, + } +} diff --git a/services/ws-wasi-runner/wit/world.wit b/services/ws-wasi-runner/wit/world.wit new file mode 100644 index 0000000..77c6c05 --- /dev/null +++ b/services/ws-wasi-runner/wit/world.wit @@ -0,0 +1,61 @@ +package et:ws-wasi@0.1.0; + +/// WebSocket client bound to ws-server. The host owns the socket; the guest +/// drives it through these calls. `recv` returns at most one inbound text +/// message per call, returning `none` after `timeout-ms` if nothing arrived. +interface ws { + type ws-error = string; + + enum state { + connecting, + connected, + closing, + closed, + } + + connect: func() -> result<_, ws-error>; + get-state: func() -> state; + agent-id: func() -> string; + send-event: func(category: string, kind: string, body-json: string) -> result<_, ws-error>; + send-text: func(text: string) -> result<_, ws-error>; + recv: func(timeout-ms: u32) -> result, ws-error>; + disconnect: func(); +} + +/// The export every WASI module must implement: a single async-equivalent +/// entry point. Returning `err` aborts the runner non-zero. +interface entry { + run: func() -> result<_, string>; +} + +/// What the runner's own `wasmtime::component::bindgen!` consumes. Note +/// `wasi:nn/*` and `wasi:clocks/*` + `wasi:io/poll` are *deliberately absent* +/// from this list: the wasi-nn impls come from `wasmtime-wasi-nn`'s own +/// `add_to_linker` (see `src/host/wasi_nn.rs`), and the clocks + io::poll +/// impls come from `wasmtime_wasi::p2::add_to_linker_async`. The trimmed +/// subset of WebAssembly/wasi-gfx (`wasi:webgpu`) lives under +/// `wit/deps/wasi-webgpu/`; host impls in `src/host/wasi_webgpu.rs` are +/// wgpu-backed for the matmul subset and trap on everything else. +world runner { + import wasi:logging/logging@0.1.0-draft; + import wasi:keyvalue/store@0.2.0-draft; + import ws; + import wasi:webgpu/webgpu@0.0.1; + export entry; +} + +/// What guest WASI modules running under `et-ws-wasi-runner` target. +/// Mirrors `runner` and additionally pulls in the standardised WASI Preview 2 +/// clocks + io::poll (wired by `wasmtime_wasi::p2::add_to_linker_async`) and +/// the wasi-nn interfaces (wired through `wasmtime-wasi-nn`). componentize-py +/// generates Python bindings for every import here. +world module { + include runner; + import wasi:clocks/wall-clock@0.2.6; + import wasi:clocks/monotonic-clock@0.2.6; + import wasi:io/poll@0.2.6; + import wasi:nn/tensor@0.2.0-rc-2024-10-28; + import wasi:nn/graph@0.2.0-rc-2024-10-28; + import wasi:nn/inference@0.2.0-rc-2024-10-28; + import wasi:nn/errors@0.2.0-rc-2024-10-28; +} diff --git a/services/ws-wasm-agent/Cargo.toml b/services/ws-wasm-agent/Cargo.toml index 21bdd8c..31646e8 100644 --- a/services/ws-wasm-agent/Cargo.toml +++ b/services/ws-wasm-agent/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] chrono.workspace = true -edge-toolkit = { path = "../../libs/edge-toolkit" } +edge-toolkit.workspace = true js-sys = "0.3" serde.workspace = true serde_json.workspace = true diff --git a/services/ws-wasm-agent/src/lib.rs b/services/ws-wasm-agent/src/lib.rs index 914e729..264dffc 100644 --- a/services/ws-wasm-agent/src/lib.rs +++ b/services/ws-wasm-agent/src/lib.rs @@ -401,9 +401,9 @@ impl WsClient { } } - /// Get the client ID + /// Get the agent ID assigned by the server on connect. #[wasm_bindgen] - pub fn get_client_id(&self) -> String { + pub fn get_agent_id(&self) -> String { self.agent_id.borrow().clone().unwrap_or_default() } diff --git a/services/ws/Cargo.toml b/services/ws/Cargo.toml index 7ff1ef6..2bcb301 100644 --- a/services/ws/Cargo.toml +++ b/services/ws/Cargo.toml @@ -9,9 +9,9 @@ repository.workspace = true actix-web = "4" actix-ws = "0.3" chrono = { version = "0.4", features = ["serde"] } -edge-toolkit = { path = "../../libs/edge-toolkit" } +edge-toolkit.workspace = true futures-util = "0.3" -opentelemetry = "0.31" +opentelemetry.workspace = true serde.workspace = true serde_json.workspace = true serde_yaml = "0.9" diff --git a/utilities/cli/Cargo.toml b/utilities/cli/Cargo.toml index f649b67..7676cf9 100644 --- a/utilities/cli/Cargo.toml +++ b/utilities/cli/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true [dependencies] anyhow.workspace = true clap.workspace = true -edge-toolkit = { path = "../../libs/edge-toolkit" } +edge-toolkit.workspace = true serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true diff --git a/utilities/cli/src/lib.rs b/utilities/cli/src/lib.rs index 530cf20..23615db 100644 --- a/utilities/cli/src/lib.rs +++ b/utilities/cli/src/lib.rs @@ -306,8 +306,10 @@ fn generated_readme(cluster: &ClusterInput, module_names: &[String], output_type .collect::>() .join(", "); format!( - "This directory contains generated deployment configs for the `{}` scenario.\n\ -Files: {}.", + concat!( + "This directory contains generated deployment configs for the `{}` scenario.\n", + "Files: {}.", + ), cluster.cluster_name, output_files ) }; @@ -318,10 +320,12 @@ Files: {}.", .join("\n"); format!( - "# {name}\n\n\ -{output_summary}\n\n\ -{module_summary}\n\n\ -{run_instructions}", + concat!( + "# {name}\n\n", + "{output_summary}\n\n", + "{module_summary}\n\n", + "{run_instructions}", + ), name = cluster.cluster_name, output_summary = output_summary, module_summary = module_summary, diff --git a/utilities/cli/src/module_package_json/mod.rs b/utilities/cli/src/module_package_json/mod.rs index a0dde01..3f9cb07 100644 --- a/utilities/cli/src/module_package_json/mod.rs +++ b/utilities/cli/src/module_package_json/mod.rs @@ -12,49 +12,78 @@ struct Project { version: String, description: Option, license: Option, + #[serde(default)] + urls: BTreeMap, } -#[derive(Deserialize)] +/// Shared shape of `[tool.ws-module]` (pyproject.toml) and +/// `[package.metadata.ws-module]` (Cargo.toml). +#[derive(Deserialize, Default)] struct WsModule { - #[serde(rename = "js-main")] - js_main: String, + /// Override for the resolved entry file (relative to `pkg/`). When + /// `None`, the entry is derived from the package name — see + /// [`resolve_main`]. + #[serde(default)] + main: Option, #[serde(default)] dependencies: BTreeMap, } #[derive(Deserialize)] struct Tool { - #[serde(rename = "ws-module")] + #[serde(rename = "ws-module", default)] ws_module: WsModule, } #[derive(Deserialize)] struct Pyproject { project: Project, - tool: Tool, + #[serde(default)] + tool: Option, } #[derive(Deserialize)] -struct CargoPackage { - package: CargoPackageMetadata, +struct CargoToml { + package: Option, + workspace: Option, } #[derive(Deserialize)] struct CargoPackageMetadata { name: String, + version: Option, + repository: Option, metadata: Option, } #[derive(Deserialize)] struct CargoMetadata { #[serde(rename = "ws-module")] - ws_module: Option, + ws_module: Option, } #[derive(Deserialize)] -struct CargoWsModule { - #[serde(default)] - dependencies: BTreeMap, +struct CargoWorkspace { + package: Option, +} + +#[derive(Deserialize, Default)] +struct WorkspacePackage { + version: Option, + repository: Option, +} + +/// A Cargo `[package]` field that can be either a literal value +/// (`version = "0.1.0"`) or inherited from the workspace +/// (`version.workspace = true`). +#[derive(Deserialize)] +#[serde(untagged)] +enum MaybeInherited { + Direct(String), + Workspace { + #[allow(dead_code)] + workspace: bool, + }, } pub fn generate_module_package_json(module_dir: &Path) -> Result { @@ -85,36 +114,85 @@ fn package_json_from_pyproject(module_dir: &Path) -> Result { let pyproject_path = module_dir.join("pyproject.toml"); let pyproject: Pyproject = read_toml(&pyproject_path)?; let p = &pyproject.project; + let ws_module = pyproject.tool.map(|t| t.ws_module).unwrap_or_default(); + + let pkg_dir = module_dir.join("pkg"); + let kind = detect_python_kind(module_dir); + let main = resolve_main(&pkg_dir, &p.name, kind, ws_module.main.as_deref())?; + let mut pkg = Map::from_iter([ ("name".to_string(), json!(p.name)), ("type".to_string(), json!("module")), ("description".to_string(), json!(p.description.as_deref().unwrap_or(""))), ("version".to_string(), json!(p.version)), ("license".to_string(), json!(p.license.as_deref().unwrap_or(""))), - ("main".to_string(), json!(pyproject.tool.ws_module.js_main)), + ("main".to_string(), json!(main)), ]); - if !pyproject.tool.ws_module.dependencies.is_empty() { - pkg.insert("dependencies".to_string(), json!(pyproject.tool.ws_module.dependencies)); + if let Some(repo) = project_repository(&p.urls) { + pkg.insert("repository".to_string(), repository_json(repo)); + } + if !ws_module.dependencies.is_empty() { + pkg.insert("dependencies".to_string(), json!(ws_module.dependencies)); } Ok(Value::Object(pkg)) } +/// PEP 621 `[project.urls]` is a free-form map keyed by display name. The +/// PyPI-recommended convention is to call the source-of-truth URL one of +/// these (case-sensitive); we accept all of them. +fn project_repository(urls: &BTreeMap) -> Option<&str> { + ["Repository", "repository", "Source", "source"] + .iter() + .find_map(|key| urls.get(*key)) + .map(String::as_str) +} + fn package_json_from_cargo(module_dir: &Path, out_path: &Path) -> Result { - let cargo_toml: CargoPackage = read_toml(&module_dir.join("Cargo.toml"))?; + let cargo_toml_path = module_dir.join("Cargo.toml"); + let cargo_toml_src = fs::read_to_string(&cargo_toml_path) + .with_context(|| format!("Failed to read {}", cargo_toml_path.display()))?; + let cargo_toml: CargoToml = + toml::from_str(&cargo_toml_src).with_context(|| format!("Failed to parse {}", cargo_toml_path.display()))?; + let package = cargo_toml + .package + .ok_or_else(|| anyhow!("{} has no [package] section", cargo_toml_path.display()))?; + let crate_name = package.name; + let kind = detect_cargo_kind(&cargo_toml_src); + let workspace = find_workspace_package(module_dir)?; + let mut pkg = read_package_json(out_path)?.unwrap_or_else(|| { let mut pkg = Map::new(); - pkg.insert("name".to_string(), json!(cargo_toml.package.name)); + pkg.insert("name".to_string(), json!(crate_name)); pkg.insert("type".to_string(), json!("module")); pkg }); if !pkg.contains_key("name") { - pkg.insert("name".to_string(), json!(cargo_toml.package.name)); + pkg.insert("name".to_string(), json!(crate_name)); + } + let ws_version = workspace.as_ref().and_then(|w| w.version.as_deref()); + let ws_repository = workspace.as_ref().and_then(|w| w.repository.as_deref()); + if !pkg.contains_key("version") { + if let Some(version) = resolve_inherited(package.version.as_ref(), ws_version) { + pkg.insert("version".to_string(), json!(version)); + } + } + if !pkg.contains_key("repository") { + if let Some(repo) = resolve_inherited(package.repository.as_ref(), ws_repository) { + pkg.insert("repository".to_string(), repository_json(&repo)); + } } - let Some(ws_module) = cargo_toml.package.metadata.and_then(|metadata| metadata.ws_module) else { - return Ok(Value::Object(pkg)); - }; + let ws_module = package + .metadata + .and_then(|metadata| metadata.ws_module) + .unwrap_or_default(); + + let existing_main = pkg.get("main").and_then(|v| v.as_str()).map(str::to_string); + let main_override = ws_module.main.as_deref().or(existing_main.as_deref()); + let pkg_dir = module_dir.join("pkg"); + let main = resolve_main(&pkg_dir, &crate_name, kind, main_override)?; + pkg.insert("main".to_string(), json!(main)); if !ws_module.dependencies.is_empty() { let dependencies = pkg @@ -131,6 +209,115 @@ fn package_json_from_cargo(module_dir: &Path, out_path: &Path) -> Result Ok(Value::Object(pkg)) } +/// Resolve a `[package]` field that may be inherited from the workspace. +/// `direct` is the value read from the crate's Cargo.toml; `workspace` +/// is the corresponding `[workspace.package]` value (if any). Returns +/// the literal direct value, or the workspace value when the crate +/// declares `field.workspace = true`. +fn resolve_inherited(direct: Option<&MaybeInherited>, workspace: Option<&str>) -> Option { + match direct { + Some(MaybeInherited::Direct(s)) => Some(s.clone()), + Some(MaybeInherited::Workspace { .. }) => workspace.map(str::to_string), + None => None, + } +} + +/// Walk parents of `start` looking for a Cargo.toml containing a +/// `[workspace]` table; return its `[workspace.package]` if present. +fn find_workspace_package(start: &Path) -> Result> { + for dir in start.ancestors().skip(1) { + let cargo = dir.join("Cargo.toml"); + if !cargo.is_file() { + continue; + } + let toml: CargoToml = read_toml(&cargo)?; + if let Some(ws) = toml.workspace { + return Ok(ws.package); + } + } + Ok(None) +} + +/// npm's `repository` field accepts either a bare URL string or the +/// object form. The object form matches what wasm-pack emits, so we use +/// that for visual consistency across generated package.json files. +fn repository_json(url: &str) -> Value { + json!({ "type": "git", "url": url }) +} + +/// Whether a module is built as a WASI Preview 2 component or as JS that +/// browser/Pyodide loads. +#[derive(Clone, Copy)] +enum ModuleKind { + Wasi, + Js, +} + +impl ModuleKind { + fn extension(self) -> &'static str { + match self { + ModuleKind::Wasi => "wasm", + ModuleKind::Js => "js", + } + } +} + +/// `componentize-py bindings` writes a `wit_world/` package next to the +/// module's `pyproject.toml`. Its presence is what tells us this is a +/// WASI Python module rather than a Pyodide module. +fn detect_python_kind(module_dir: &Path) -> ModuleKind { + if module_dir.join("wit_world").is_dir() { + ModuleKind::Wasi + } else { + ModuleKind::Js + } +} + +/// WASI Rust modules use `wit-bindgen` to generate component bindings; +/// wasm-pack browser modules don't. The substring check covers +/// `[dependencies]`, `[target.*.dependencies]`, and workspace-dep lines +/// alike without needing to model Cargo.toml's full dependency tree. +fn detect_cargo_kind(cargo_toml_src: &str) -> ModuleKind { + if cargo_toml_src.contains("wit-bindgen") { + ModuleKind::Wasi + } else { + ModuleKind::Js + } +} + +/// Resolve the `main` entry file in `pkg_dir`. +/// +/// If `main_override` is set, that filename is used. Otherwise the entry +/// is derived from `name` by trying both its `_` and `-` variants with the +/// extension dictated by `kind` (`.wasm` for WASI, `.js` for browser/Pyodide). +/// The resolved file must exist in `pkg_dir`; this errors otherwise. +fn resolve_main(pkg_dir: &Path, name: &str, kind: ModuleKind, main_override: Option<&str>) -> Result { + if let Some(main) = main_override { + if !pkg_dir.join(main).is_file() { + return Err(anyhow!("main = {:?} does not exist in {}", main, pkg_dir.display())); + } + return Ok(main.to_string()); + } + let underscored = name.replace('-', "_"); + let hyphenated = name.replace('_', "-"); + let stems: &[&str] = if underscored == hyphenated { + &[underscored.as_str()] + } else { + &[underscored.as_str(), hyphenated.as_str()] + }; + let ext = kind.extension(); + for stem in stems { + let candidate = format!("{stem}.{ext}"); + if pkg_dir.join(&candidate).is_file() { + return Ok(candidate); + } + } + Err(anyhow!( + "No main file in {}; expected {underscored}.{ext} or {hyphenated}.{ext} (override with [ws-module] main)", + pkg_dir.display() + )) +} + fn read_toml(path: &Path) -> Result where T: for<'de> Deserialize<'de>, diff --git a/utilities/cli/tests/module_package_json.rs b/utilities/cli/tests/module_package_json.rs index 46ba949..ac13ead 100644 --- a/utilities/cli/tests/module_package_json.rs +++ b/utilities/cli/tests/module_package_json.rs @@ -10,6 +10,9 @@ use tempfile::tempdir; fn module_package_json_generates_from_pyproject_metadata() { let test_root = tempdir().unwrap(); let module_dir = test_root.path(); + let package_dir = module_dir.join("pkg"); + fs::create_dir_all(&package_dir).unwrap(); + fs::write(package_dir.join("et_ws_python_module.js"), "").unwrap(); fs::write( module_dir.join("pyproject.toml"), r#"[project] @@ -18,9 +21,6 @@ version = "0.1.0" description = "Python module" license = "Apache-2.0" -[tool.ws-module] -js-main = "python_module.js" - [tool.ws-module.dependencies] et-model-face1 = "*" "#, @@ -35,16 +35,67 @@ et-model-face1 = "*" assert_eq!(package["description"], "Python module"); assert_eq!(package["version"], "0.1.0"); assert_eq!(package["license"], "Apache-2.0"); - assert_eq!(package["main"], "python_module.js"); + assert_eq!(package["main"], "et_ws_python_module.js"); assert_eq!(package["dependencies"]["et-model-face1"], "*"); } +#[test] +fn module_package_json_derives_wasi_main_from_crate_name() { + let test_root = tempdir().unwrap(); + let module_dir = test_root.path(); + let package_dir = module_dir.join("pkg"); + fs::create_dir_all(&package_dir).unwrap(); + fs::write(package_dir.join("et_ws_wasi_demo.wasm"), "").unwrap(); + fs::write( + module_dir.join("Cargo.toml"), + r#"[package] +name = "et-ws-wasi-demo" +version = "0.1.0" +edition = "2024" + +[dependencies] +wit-bindgen = "0.57" +"#, + ) + .unwrap(); + + let output_path = generate_module_package_json(module_dir).unwrap(); + let package: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap(); + + assert_eq!(package["main"], "et_ws_wasi_demo.wasm"); +} + +#[test] +fn module_package_json_derives_wasi_main_from_pyproject() { + let test_root = tempdir().unwrap(); + let module_dir = test_root.path(); + let package_dir = module_dir.join("pkg"); + fs::create_dir_all(&package_dir).unwrap(); + fs::create_dir_all(module_dir.join("wit_world")).unwrap(); + fs::write(package_dir.join("et_ws_wasi_pydemo.wasm"), "").unwrap(); + fs::write( + module_dir.join("pyproject.toml"), + r#"[project] +name = "et-ws-wasi-pydemo" +version = "0.1.0" +description = "WASI Python demo" +"#, + ) + .unwrap(); + + let output_path = generate_module_package_json(module_dir).unwrap(); + let package: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap(); + + assert_eq!(package["main"], "et_ws_wasi_pydemo.wasm"); +} + #[test] fn module_package_json_merges_cargo_ws_module_dependencies() { let test_root = tempdir().unwrap(); let module_dir = test_root.path(); let package_dir = module_dir.join("pkg"); fs::create_dir_all(&package_dir).unwrap(); + fs::write(package_dir.join("et_ws_rust_module.js"), "").unwrap(); fs::write( module_dir.join("Cargo.toml"), r#"[package] @@ -61,6 +112,7 @@ et-model-har-motion1 = "*" package_dir.join("package.json"), r#"{ "type": "module", + "main": "et_ws_rust_module.js", "dependencies": { "existing-package": "1.0.0" } @@ -74,6 +126,52 @@ et-model-har-motion1 = "*" assert_eq!(package["name"], "et-ws-rust-module"); assert_eq!(package["type"], "module"); + assert_eq!(package["main"], "et_ws_rust_module.js"); assert_eq!(package["dependencies"]["existing-package"], "1.0.0"); assert_eq!(package["dependencies"]["et-model-har-motion1"], "*"); } + +#[test] +fn module_package_json_fails_when_main_missing() { + let test_root = tempdir().unwrap(); + let module_dir = test_root.path(); + fs::create_dir_all(module_dir.join("pkg")).unwrap(); + fs::write( + module_dir.join("Cargo.toml"), + r#"[package] +name = "et-ws-missing-module" +version = "0.1.0" +edition = "2024" +"#, + ) + .unwrap(); + + let error = generate_module_package_json(module_dir).unwrap_err(); + assert!(error.to_string().contains("No main file"), "unexpected error: {error}"); +} + +#[test] +fn module_package_json_respects_main_override() { + let test_root = tempdir().unwrap(); + let module_dir = test_root.path(); + let package_dir = module_dir.join("pkg"); + fs::create_dir_all(&package_dir).unwrap(); + fs::write(package_dir.join("custom_entry.wasm"), "").unwrap(); + fs::write( + module_dir.join("Cargo.toml"), + r#"[package] +name = "et-ws-override-module" +version = "0.1.0" +edition = "2024" + +[package.metadata.ws-module] +main = "custom_entry.wasm" +"#, + ) + .unwrap(); + + let output_path = generate_module_package_json(module_dir).unwrap(); + let package: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap(); + + assert_eq!(package["main"], "custom_entry.wasm"); +}