Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.wasm
*.onnx
target/
.DS_Store
services/ws-wasm-agent/pkg/
Expand Down
87 changes: 82 additions & 5 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[tools]
action-validator = "latest"
"cargo:ast-grep" = "latest"
cargo-binstall = "latest"
"cargo:aube" = "latest"
"cargo:taplo-cli" = "latest"
Expand All @@ -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"

Expand Down Expand Up @@ -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",
Expand All @@ -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"

Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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",
]
Expand Down Expand Up @@ -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"
Expand Down
52 changes: 52 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand All @@ -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 <pkg> -o pkg/<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]`)
Expand Down
Loading
Loading