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
2 changes: 1 addition & 1 deletion .mise/config.rust.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ shell = "{{ vars.task_shell }}"
description = "Build the no_std crates for a bare-metal target"
run = """
features=et-ws-service/picoserve-hub,et-ws-server/picoserve-server
set -- -p et-org -p edge-toolkit -p et-ws-service -p et-ws-server
set -- -p et-org -p edge-toolkit -p et-ws-service -p et-ws-server -p et-ws-wasi-runner
cargo build --lib --target thumbv8m.main-none-eabihf --no-default-features --features "$features" "$@"
"""
shell = "{{ vars.task_shell }}"
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Changelog 1.1.0.
from the same `MODULES_*` variables, so every board's firmware embeds modules the same way. Its `board` writes what
the board a target picks bakes in -- the Wi-Fi network, and the Pico 2 W's radio blobs and memory layout -- and
returns the directives that link it.
- `et-ws-wasi-runner` builds as `no_std` + `alloc` without its new default `std` feature, and its `board` module
runs a guest on a microcontroller: compiled ahead of time with `engine::precompile` for the board's Pulley target,
and hosted through the firmware's `board::Platform` and `board::Transport`.
- An opt-in `et-recovery-supervisor` service observes application progress and coordinates bounded Kubernetes recovery.
- The Python SDK provides recovery lifecycle messages and integrity-checked checkpoint storage helpers.
- A scenario can list `machines:` to span several hosts. The first is the server, running the hub and the collector,
Expand Down Expand Up @@ -100,6 +103,11 @@ Changelog 1.1.0.
- **Breaking:** `et-ws-server`'s actix server, its binary and its environment config are behind the new default `std`
feature. A build with `default-features = false` and none of the features that imply `std` -- `modules`, `otlp`,
`qr`, `storage`, `tls`, `websockify` -- now gets the `no_std` library and no binary; add `std` to keep the server.
- **Breaking:** `et-ws-wasi-runner`'s host runner, its binary and `run_module` are behind the new default `std`
feature, which `nn`, `webgpu` and `coverage` imply. A build with `default-features = false` and none of them now
gets the `no_std` board runner and no binary; add `std` to keep the host runner. `server_message_to_wit` moved from
`host::ws` to `convert`, and `Bucket` from `host::wasi_keyvalue` and `kv_not_implemented` from `host` to
`keyvalue`.
- **Breaking:** `et-cli` refuses a scenario whose `module_paths:` entry, or a module directory found under one, has a
name a shell would need quoted or that some filesystem could not hold, such as `my mod`, `mod$(id)` or `con`; rename
the directory. Generated deployments no longer escape these paths. The check is the new `et-sanitise` crate, which
Expand Down
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,14 @@ wasm-bindgen-test = "0.3"
# RUSTSEC-2026-0378 / RUSTSEC-2026-0379 only from 49.0.4, and RUSTSEC-2026-0420 and RUSTSEC-2026-0422 through
# RUSTSEC-2026-0424 only from 49.0.5, on the 49 line. The major is held at 49 by wasi-webgpu-wasmtime, whose 0.4
# line requires wasmtime ^49.
wasmtime = { version = "49.0.5", features = ["async", "component-model"] }
# Without its default features, which need std, so a board's runner can depend on it; a host build turns them on.
wasmtime = { version = "49.0.5", default-features = false, features = [
"async",
"component-model",
"component-model-async",
"pulley",
"runtime",
] }
# The generator behind wasmtime's `bindgen!` macro, driven directly by et-int-gen.
# Emitting the runner's host bindings as a checked-in source file keeps ws-wasi-runner self-contained: the
# macro read WIT from `../../generated/specs/wit`, which `cargo package` cannot include, so the crate failed
Expand Down
34 changes: 34 additions & 0 deletions config/deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,40 @@ ignore = [
# newest release. It runs at compile time inside that macro and reports its errors; none of it is in the firmware.
# Drop this when pio-proc leaves it.
{ id = "RUSTSEC-2026-0173", reason = "expires 2026-11-13" },
# The npm: AsyncAPI generator tools -- the entries below share one setting, so it is described once, here.
# @asyncapi/generator 3.5.0 and @asyncapi/markdown-template 2.0.0 are the newest releases, and every fix is a major
# version past a range one of them or their dependencies requires, so no reinstall reaches any of them. They run
# only in `gen:ws-md`, a developer-time render of generated/specs/ws.yaml -- a spec this repo generates from its
# own Rust types -- through the template's local install path, so every input is ours, nothing is fetched or
# cloned, and neither tool is shipped or reachable from a deployment.
#
# @babel/core 7.12.9 reads any source map named by a sourceMappingURL in the code it compiles, fixed in 7.29.6.
# @asyncapi/generator-react-sdk 1.1.3 pins it by equality. It compiles only the template's own JSX.
{ id = "GHSA-4x5r-pxfx-6jf8", reason = "expires 2026-11-13" },
# simple-git 3.36.0 and its @simple-git/argv-parser 1.1.1 have gaps in their unsafe-operations guard.
# It misses `VISUAL`, abbreviated long options, `include.path` and `trailer.<token>.cmd`, fixed in simple-git 4,
# and the generator requires ^3.36.0. All four need attacker-controlled git arguments or config, and the generator
# reaches git only to clone a template named by a remote URL, which `gen:ws-md` never passes.
{ id = "GHSA-858h-whjf-mvg5", reason = "expires 2026-11-13" },
{ id = "GHSA-g4wm-2vf7-vfgr", reason = "expires 2026-11-13" },
{ id = "GHSA-v5rq-49vh-5v5c", reason = "expires 2026-11-13" },
{ id = "GHSA-x6jw-m9v5-85vh", reason = "expires 2026-11-13" },
# ajv 5.5.2 and 6.5.2: ReDoS through a `$data` pattern, and prototype pollution from a crafted schema.
# Pulled by json-schema-migrate 0.2.0 and webapi-parser 0.5.0 under the generator's parser stack; both need an
# attacker-supplied schema or instance, and the only schema validated here is our own spec.
{ id = "GHSA-2g4f-4pwh-qvx6", reason = "expires 2026-11-13" },
{ id = "GHSA-v88g-cgmw-v5xw", reason = "expires 2026-11-13" },
# jsonpath-plus 7.2.0 code execution through a crafted JSONPath expression, fixed in 10.3.0.
# Pulled by the parser 1.x and 2.x copies @asyncapi/multi-parser bundles for old spec versions. The expressions
# come from the parser's own rulesets and our spec, never from an untrusted source.
{ id = "GHSA-hw8r-x6gr-5gjp", reason = "expires 2026-11-13" },
{ id = "GHSA-pppg-cpfq-h7wr", reason = "expires 2026-11-13" },
# glob 10.5.0 / 13.0.6 and minimatch 3.1.5 / 9.0.9 / 10.2.6 can be made to do excess work.
# glob keeps walking after its AbortSignal fires, and minimatch amplifies a crafted pattern. Both are fixed only in
# new majors that @npmcli and @stoplight require below. Each costs time only, and only for a caller that aborts a
# walk or matches an attacker's pattern; the patterns here are fixed.
{ id = "GHSA-3g3m-2fjr-6pw4", reason = "expires 2026-11-13" },
{ id = "GHSA-g3hv-75wg-m249", reason = "expires 2026-11-13" },
]

[licenses]
Expand Down
5 changes: 5 additions & 0 deletions config/jscpd-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"03e4e07acff327e2": 1,
"04199950b57990a0": 1,
"04daef50421ed235": 1,
"05f89321b4dbba3a": 1,
"0690b4ae1dc710a4": 1,
"07378241fe562817": 1,
"075c84a635548077": 1,
Expand Down Expand Up @@ -42,6 +43,7 @@
"1a1e1611e4db4656": 1,
"1a46175d923bf850": 3,
"1a97e5c72a30152e": 1,
"1b8894933d283827": 1,
"1bd6f5989d1961e5": 1,
"1c64414236b8476c": 1,
"1c8cf13e10b67513": 1,
Expand Down Expand Up @@ -161,6 +163,7 @@
"71d4c22b912832b9": 1,
"72295cb3aacc3d6c": 1,
"726487c2d82f5f69": 1,
"727e7a5304bdc471": 1,
"728c97d5724db2c3": 1,
"73b4d0b9adadfa1f": 1,
"73ffadbd8a93c577": 1,
Expand Down Expand Up @@ -319,6 +322,7 @@
"dd162080de996936": 1,
"dd41701c3663b9f3": 1,
"ddca47a4caded0fd": 1,
"de1a3233f93e1695": 1,
"dfab26504ac7abe4": 1,
"e152738c60f43b0e": 1,
"e25456ad9cc7f9f9": 1,
Expand All @@ -333,6 +337,7 @@
"eb95988e62f801fd": 1,
"ebb8f005bcebfcee": 1,
"ec5c408a8f2c368d": 1,
"ec83e5f612762cae": 1,
"edfb18c73a139873": 1,
"ef69568ef92f1b79": 1,
"ef6dd6376769473d": 1,
Expand Down
11 changes: 11 additions & 0 deletions config/osv-scanner.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
# AUTO-GENERATED from config/deny.toml by `mise run gen:osv-scanner`.
# The list spans every language, so "has unused ignores" warnings are expected: agents must ignore them.
IgnoredVulns = [
{ id = "GHSA-2g4f-4pwh-qvx6", ignoreUntil = 2026-11-13 },
{ id = "GHSA-3g3m-2fjr-6pw4", ignoreUntil = 2026-11-13 },
{ id = "GHSA-4x5r-pxfx-6jf8", ignoreUntil = 2026-11-13 },
{ id = "GHSA-5j98-2g5x-46v6", ignoreUntil = 2026-11-13 },
{ id = "GHSA-6f2x-v7q7-m7m5", ignoreUntil = 2026-11-13 },
{ id = "GHSA-858h-whjf-mvg5", ignoreUntil = 2026-11-13 },
{ id = "GHSA-g3hv-75wg-m249", ignoreUntil = 2026-11-13 },
{ id = "GHSA-g4wm-2vf7-vfgr", ignoreUntil = 2026-11-13 },
{ id = "GHSA-hw8r-x6gr-5gjp", ignoreUntil = 2026-11-13 },
{ id = "GHSA-pppg-cpfq-h7wr", ignoreUntil = 2026-11-13 },
{ id = "GHSA-qwgh-2vcv-g2f7", ignoreUntil = 2026-11-13 },
{ id = "GHSA-v5rq-49vh-5v5c", ignoreUntil = 2026-11-13 },
{ id = "GHSA-v88g-cgmw-v5xw", ignoreUntil = 2026-11-13 },
{ id = "GHSA-x6jw-m9v5-85vh", ignoreUntil = 2026-11-13 },
{ id = "RUSTSEC-2023-0071", ignoreUntil = 2026-11-13 },
{ id = "RUSTSEC-2024-0436", ignoreUntil = 2026-11-13 },
{ id = "RUSTSEC-2024-0449", ignoreUntil = 2026-11-13 },
Expand Down
95 changes: 74 additions & 21 deletions services/ws-wasi-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,69 @@ path = "src/lib.rs"
doctest = false
name = "et-ws-wasi-runner"
path = "src/main.rs"
required-features = ["std"]

# The tests of the `std` runner, and of the board's, which compiles its guests as only a host can.
# `server_message_to_wit`, the test of the conversions a board shares, runs with `--no-default-features` as well.
[[test]]
name = "board"
required-features = ["std"]

[[test]]
name = "modules"
required-features = ["std"]

[[test]]
name = "o2_ingest_search"
required-features = ["std"]

[[test]]
name = "otel_propagation"
required-features = ["std"]

[[test]]
name = "vector_otlp_relay"
required-features = ["std"]

[[test]]
name = "ws_backend"
required-features = ["std"]

[features]
#! Each standardised WASI interface hosted beyond `et:ws-wasi` and WASI Preview 2 is a feature, all on by default.
#! A guest that imports an interface the build left out fails to instantiate, naming the import.

default = ["nn", "webgpu"]
default = ["nn", "std", "webgpu"]
## The runner for a host with an operating system.
## It fetches a component from the hub, compiles it, and hosts WASI Preview 2 through `wasmtime-wasi`. Without it the
## crate is `no_std`, and runs a component compiled ahead of time for a board through its `board` module.
std = [
"chrono/std",
"dep:et-otlp",
"dep:et-rest-client",
"dep:et-ws-runner-common",
"dep:fs-err",
"dep:futures-util",
"dep:libc",
"dep:reqwest",
"dep:tokio",
"dep:tokio-tungstenite",
"dep:tracing",
"dep:wasmtime-wasi",
"edge-toolkit/std",
"precompile",
"serde_json/std",
"thiserror/std",
"wasmtime/default",
]
## Compiles a component ahead of time for a board's engine, as a firmware's build script does.
precompile = ["wasmtime/cranelift", "wasmtime/std"]
## Hosts `wasi:nn` (graph, tensor, inference) on ONNX Runtime through `wasmtime-wasi-nn`.
## Guests can then load an ONNX model and run inference on it.
nn = ["dep:ort", "dep:wasmtime-wasi-nn"]
nn = ["dep:ort", "dep:wasmtime-wasi-nn", "std"]
## Hosts the full upstream `wasi:webgpu` interface through `wasi-webgpu-wasmtime`.
## A wgpu instance on every native backend gives guests real GPU compute and rendering.
webgpu = ["dep:wasi-webgpu-wasmtime", "dep:wgpu-core", "dep:wgpu-types"]
webgpu = ["dep:wasi-webgpu-wasmtime", "dep:wgpu-core", "dep:wgpu-types", "std"]
## Dispatches `wasi:nn` inference on `ExecutionTarget::Gpu` to ONNX Runtime's `CUDAExecutionProvider`.
## Without it such a request warns and falls back to the CPU. It enables wasmtime-wasi-nn's `onnx-cuda`, and
## through it `ort/cuda`, so `ort/download-binaries` pulls the CUDA-flavoured ONNX Runtime prebuilt -- which only
Expand All @@ -44,32 +95,34 @@ cuda = ["nn", "wasmtime-wasi-nn/onnx-cuda"]
## Internal: compiles in the `/cov` preopen for the repo's coverage workflow.
## Mapped to `target/wasi-cov`, it is where instrumented WASI guests dump their minicov `.profraw`.
## `ET_TEST_COVERAGE` still gates the preopen at runtime.
coverage = ["et-ws-runner-common/coverage"]
coverage = ["et-ws-runner-common?/coverage", "std"]
## Internal: renders this feature list into the crate documentation, as docs.rs builds it.
docs = ["dep:document-features"]

# What a board's `no_std` runner needs as much as the host's: the engine, and the message types and their JSON.
[dependencies]
chrono = { workspace = true, features = ["std"] }
chrono = { workspace = true, features = ["alloc"] }
document-features = { workspace = true, optional = true }
edge-toolkit = { workspace = true, features = ["std"] }
et-otlp.workspace = true
et-rest-client = { workspace = true, features = ["tracing"] }
et-ws-runner-common.workspace = true
fs-err.workspace = true
futures-util.workspace = true
edge-toolkit.workspace = true
serde_json = { workspace = true, features = ["alloc"] }
serde_path_to_error.workspace = true
thiserror.workspace = true
wasmtime.workspace = true
# The rest is the `std` runner's.
et-otlp = { workspace = true, optional = true }
et-rest-client = { workspace = true, features = ["tracing"], optional = true }
et-ws-runner-common = { workspace = true, optional = true }
fs-err = { workspace = true, optional = true }
futures-util = { workspace = true, optional = true }
# libc::_exit for the macOS test-only fast exit (see main.rs).
# std::process::exit still runs atexit handlers, which is exactly the path that races libc++ during
# ORT teardown -- _exit skips them.
libc.workspace = true
reqwest = { workspace = true, features = ["json", "rustls", "stream"] }
serde_json = { workspace = true, features = ["std"] }
serde_path_to_error.workspace = true
thiserror = { workspace = true, features = ["std"] }
tokio = { workspace = true, features = ["macros", "net", "rt-multi-thread", "sync", "time"] }
tokio-tungstenite = { workspace = true, features = ["connect"] }
tracing = { workspace = true, features = ["std"] }
wasmtime.workspace = true
wasmtime-wasi.workspace = true
libc = { workspace = true, optional = true }
reqwest = { workspace = true, features = ["json", "rustls", "stream"], optional = true }
tokio = { workspace = true, features = ["macros", "net", "rt-multi-thread", "sync", "time"], optional = true }
tokio-tungstenite = { workspace = true, features = ["connect"], optional = true }
tracing = { workspace = true, features = ["std"], optional = true }
wasmtime-wasi = { workspace = true, optional = true }
# wasi-nn standardised ML inference.
# The workspace pin drops `openvino` + `winml` (which we don't use) and keeps `onnx`.
wasmtime-wasi-nn = { workspace = true, optional = true }
Expand Down
16 changes: 16 additions & 0 deletions services/ws-wasi-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,19 @@ Plus, attached to the same `Linker` but defined by external WIT packages, each b
wasmtime-wasi-nn 48 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. Inference runs on the CPU
unless the `cuda` feature is on.

## On a board

Without its default `std` feature the crate is `no_std` + `alloc`, and its `board` module runs a guest on a
microcontroller with no operating system. A board cannot compile WebAssembly, so its engine runs Pulley bytecode
instead: a firmware's build script compiles the guest ahead of time with `engine::precompile` (the `precompile`
feature) for the board's Pulley target, `pulley32` on a 32-bit board, and builds the result in.

`board::run` hosts the `runner` world on what the firmware provides through two traits: a `Platform`, the board's
clock and log, and a `Transport`, its websocket to the hub. Guest buckets live in the board's memory. Of WASI
Preview 2 it hosts only what the ws-module guests import -- their standard streams, an empty environment, the
monotonic clock and polling -- since `wasmtime-wasi` needs `std`; `wasi:nn` and `wasi:webgpu` are host-only.

wasmtime asks the firmware for one thing more, a thread-local pointer through `wasmtime_tls_get` and
`wasmtime_tls_set`, which on a single core is a static. A guest's linear memory is its declared size, about 1 MiB
for the Rust guests, so a board needs external RAM such as PSRAM to hold it.
2 changes: 1 addition & 1 deletion services/ws-wasi-runner/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
A wasmtime bump that emits a fifth fails clippy until this list grows, which is the loud direction."
)]
#[doc(hidden)]
pub use super::host::wasi_keyvalue::Bucket as __with_name0;
pub use super::keyvalue::Bucket as __with_name0;
/// Auto-generated bindings for a pre-instantiated version of a
/// component which implements the world `runner`.
///
Expand Down
Loading
Loading