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: 2 additions & 11 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,9 @@ env:
MISE_ENV: dart,dotnet,java,js,kotlin,python,r,rust,zig

jobs:
# Each OS runs two lanes: `mise run check`, and the feature powerset (cargo-hack-check) as a lane of its own.
# Each OS runs `mise run check`, and Linux and macOS run the feature powerset (cargo-hack-check) as a lane of its own.
# The powerset is several hundred clippy runs, which no shared timeout fits, and as a separate lane it runs in
# parallel rather than lengthening the check. Running it as a step after `mise run check` also fails on Windows,
# where the next `mise` call dies at once with
# mise: recursive shim invocation detected for mise: C:/Users/runneradmin/AppData/Local/mise/shims/mise
# because the check's install-all rewrites mise's own shim, which install-mise-tools had removed. Observed on
# commit https://github.com/edge-toolkit/core/commit/aa260c59248d8eb227e310f78d553ddb1b9445a2 at
# https://github.com/edge-toolkit/core/actions/runs/37386640989/job/112021398135.
# parallel rather than lengthening the check. Windows runs no powerset lane, at the operator's direction.
# Each lane is named explicitly so the check lanes keep their existing `check (<os>, <timeout>)` names.
check:
name: ${{ matrix.lane }}
Expand Down Expand Up @@ -64,10 +59,6 @@ jobs:
os: macos-latest
task: cargo-hack-check
timeout: 90
- lane: features (windows-latest)
os: windows-latest
task: cargo-hack-check
timeout: 180
# Windows runs the checkers under busybox-w32 ash, same as test.yaml's Windows lane.
# That's the shell mise's `shell = "bash"` tasks run on; Git Bash's MSYS fork-emulation cygheap breaks
# down on long task chains. The path matches the version pin in `.mise/config.windows.toml`'s
Expand Down
30 changes: 17 additions & 13 deletions .github/workflows/no-std.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,7 @@ jobs:
run: |
mise run cargo-clippy-check-pico2w
mise run build-ws-wasm-agent
src=verification/local/input/default.yaml
dst=target/firmware-default
fmt=firmware-pico2
cargo run -q -p et-cli -- generate-deployment --input-file "$src" --output-dir "$dst" --output-type "$fmt"
cd "$dst"
MISE_ENV="$fmt" mise run firmware-build
MISE_ENV="$fmt" mise run firmware-host-test
mise run firmware-scenario verification/local/input/default.yaml firmware-pico2 --host-test

# The host build the step above tested is the one both boards share, so only the board's own build is left.
- name: Lint the ESP32-C6 firmware, then build a generated scenario's
Expand All @@ -88,9 +82,19 @@ jobs:
WIFI_PASSWORD: not-a-real-passphrase
run: |
mise run cargo-clippy-check-esp32c6
src=verification/local/input/default.yaml
dst=target/firmware-default
fmt=firmware-esp32c6
cargo run -q -p et-cli -- generate-deployment --input-file "$src" --output-dir "$dst" --output-type "$fmt"
cd "$dst"
MISE_ENV="$fmt" mise run firmware-build
mise run firmware-scenario verification/local/input/default.yaml firmware-esp32c6

# The board with PSRAM runs wasi runners beside the hub, so its scenario is the two math1 runners.
# Their components have to be built before the firmware compiles them for the board. The host build then runs the
# same guests against the same hub.
- name: Lint the Pico Plus 2 W firmware, then build and serve a generated scenario's runners
timeout-minutes: 45
env:
WIFI_NETWORK: edge-toolkit-ci
WIFI_PASSWORD: not-a-real-passphrase
run: |
mise run cargo-clippy-check-pico-plus2w
mise run build-ws-wasm-agent
mise run build-ws-wasi-math1-module
mise run build-ws-wasi-math1-sender-module
mise run firmware-scenario verification/local/input/wasi-math1.yaml firmware-pico-plus2 --host-test
43 changes: 40 additions & 3 deletions .mise/config.rust.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,52 @@
depends = ["fetch-cyw43-firmware-rclone"]
description = "Build et-ws-server as Raspberry Pi Pico 2 W firmware"
env = { CYW43_FIRMWARE_DIR = "{{ config_root }}/{{ vars.cyw43_dir }}" }
run = "cargo build --release -p et-int-ws-server-firmware --target thumbv8m.main-none-eabihf"
run = "cargo build --release -p et-int-ws-server-firmware --target thumbv8m.main-none-eabihf --features pico2w"

# The firmware's body compiles only for the board, so the workspace's host clippy never reaches it; this does.
# It needs what build-firmware-pico2w needs.
[tasks.cargo-clippy-check-pico2w]
depends = ["fetch-cyw43-firmware-rclone"]
description = "Lint the Raspberry Pi Pico 2 W firmware for its own target"
env = { CYW43_FIRMWARE_DIR = "{{ config_root }}/{{ vars.cyw43_dir }}" }
run = "cargo clippy --release -p et-int-ws-server-firmware --target thumbv8m.main-none-eabihf"
run = "cargo clippy --release -p et-int-ws-server-firmware --target thumbv8m.main-none-eabihf --features pico2w"

# As build-firmware-pico2w, for the Pimoroni Pico Plus 2 W: the same radio on the RP2350B, with PSRAM for the heap.
# It also builds in each runner RUNNER_GUESTS names, compiled for the board from the module MODULES_PATHS provides.
[tasks.build-firmware-pico-plus2w]
depends = ["fetch-cyw43-firmware-rclone"]
description = "Build et-ws-server and its wasi runners as Pimoroni Pico Plus 2 W firmware"
env = { CYW43_FIRMWARE_DIR = "{{ config_root }}/{{ vars.cyw43_dir }}" }
run = "cargo build --release -p et-int-ws-server-firmware --target thumbv8m.main-none-eabihf --features pico-plus2w"

# Generate a scenario's deployment as one board's firmware, then build it, as CI does for every board.
# The deployment lands in target/<format>, and the build reads WIFI_NETWORK and WIFI_PASSWORD as any board
# build does. With --host-test it then serves the same modules, and runs the same runners, from this host.
[tasks.firmware-scenario]
description = "Generate a scenario's firmware deployment under target/ and build it, optionally testing it on the host"
run = """
dst="target/$usage_format"
set -- --input-file "$usage_input" --output-dir "$dst" --output-type "$usage_format"
cargo run -q -p et-cli -- generate-deployment "$@"
cd "$dst"
MISE_ENV="$usage_format" mise run firmware-build
if [ "${usage_host_test:-false}" = true ]; then
MISE_ENV="$usage_format" mise run firmware-host-test
fi
"""
shell = "{{ vars.task_shell }}"
usage = """
arg "<input>" help="The scenario input file"
arg "<format>" help="The board's output type, such as firmware-pico2"
flag "--host-test" help="Also serve the modules and run the runners from this host, checking each"
"""

# As cargo-clippy-check-pico2w, for the Pico Plus 2 W's body.
[tasks.cargo-clippy-check-pico-plus2w]
depends = ["fetch-cyw43-firmware-rclone"]
description = "Lint the Pimoroni Pico Plus 2 W firmware for its own target"
env = { CYW43_FIRMWARE_DIR = "{{ config_root }}/{{ vars.cyw43_dir }}" }
run = "cargo clippy --release -p et-int-ws-server-firmware --target thumbv8m.main-none-eabihf --features pico-plus2w"

# et-ws-server as ESP32-C6 firmware: an ELF that espflash writes to the board, with esp-hal's bootloader before it.
# Its radio's blobs come with esp-radio, so the build needs only the network to join, from WIFI_NETWORK and
Expand Down Expand Up @@ -68,7 +105,7 @@ shell = "{{ vars.task_shell }}"
[tasks.build-no-std]
description = "Build the no_std crates for a bare-metal target"
run = """
features=et-ws-service/picoserve-hub,et-ws-server/picoserve-server
features=et-ws-service/picoserve-hub,et-ws-server/picoserve-server,et-ws-wasi-runner/board
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" "$@"
"""
Expand Down
40 changes: 28 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,34 @@ Changelog 1.1.0.
`GET /ws` over HTTP and WebSocket through picoserve on any socket the embedder accepts, such as an `embassy-net`
`TcpSocket`. It is a local hub, with no backplane, admin API, idle timeout or registry file. A crate awaiting
`Hub::serve` needs `#![recursion_limit = "256"]`.
- `picoserve_hub::Hub::local` joins an agent the embedder runs itself to the hub, with no socket between them: a
`LocalLink` routes what it sends as a connection's frames are, queues what others send it, and disconnects the agent
when dropped. It is how a board's own runner reaches the hub beside it. Its `send_text` and `send_binary` refuse a
frame larger than the hub reads from a socket with `FrameTooLarge`.
- `et-ws-server` builds as `no_std` + `alloc` without its new `std` feature. Its `picoserve-server` feature, on by
default, adds `picoserve_server::serve`: the hub's `/ws` with `/health` and `/favicon.ico` beside it, over picoserve,
and the modules a `picoserve_modules::Modules` builds into the binary under `/modules` and `/`, as the `std` server
serves them from disk. A module listed as served elsewhere is redirected to a base URL instead.
- The `et-ws-server-firmware` firmware runs et-ws-server on a Raspberry Pi Pico 2 W or an ESP32-C6, over the board's
Wi-Fi. `MISE_ENV=rust mise run build-firmware-pico2w` fetches the Pico's radio blobs and builds it for the Pico, and
`build-firmware-esp32c6` for the ESP32-C6, each for the network `WIFI_NETWORK` and `WIFI_PASSWORD` name, embedding
the modules `MODULES_PATHS` names and serving `MODULES_ROOT` at `/`.
- `et-cli generate-deployment --output-type firmware-pico2` writes a `mise.firmware-pico2.toml`, and
`firmware-esp32c6` a `mise.firmware-esp32c6.toml`, whose `firmware-flash` task builds that firmware with the
scenario's modules in it and loads it onto a board, with picotool or espflash. The npm
packages the modules use, such as onnxruntime-web, are redirected to jsDelivr rather than built in. A scenario with
a runner, `machines:` or published artifacts is refused, since the board runs one hub and nothing else. Its
`firmware-host-test` task serves the same modules from the host, as the firmware's host build does, and fetches
every module it lists, with no board needed.
- The `et-ws-server-firmware` firmware runs et-ws-server on a Raspberry Pi Pico 2 W, an ESP32-C6 or a Pimoroni Pico
Plus 2 W, over the board's Wi-Fi. `MISE_ENV=rust mise run build-firmware-pico2w` fetches the Pico's radio blobs and
builds it for the Pico, `build-firmware-esp32c6` for the ESP32-C6, and `build-firmware-pico-plus2w` for the Pico Plus
2 W, each for the network `WIFI_NETWORK` and `WIFI_PASSWORD` name, embedding the modules `MODULES_PATHS` names and
serving `MODULES_ROOT` at `/`. The Pico Plus 2 W makes its 8 MiB of PSRAM the heap, and also runs each wasi runner
`RUNNER_GUESTS` names beside the hub, its guest compiled into the firmware and its websocket a link into the hub.
- `et-cli generate-deployment --output-type firmware-pico2` writes a `mise.firmware-pico2.toml`, `firmware-esp32c6` a
`mise.firmware-esp32c6.toml`, and `firmware-pico-plus2` a `mise.firmware-pico-plus2.toml`, whose `firmware-flash`
task builds that firmware with the scenario's modules in it and loads it onto a board, with picotool or espflash.
The npm packages the modules use, such as onnxruntime-web, are redirected to jsDelivr rather than built in. The
board's firmware decides which runners it runs: the Pico Plus 2 W runs a scenario's `wasi` runners, and every other
runner, or a runner with `limits:`, is refused; the other boards refuse any runner. A scenario with `machines:` or
published artifacts is refused on every board. Its `firmware-host-test` task serves the same modules from the host,
and runs the same runners, as the firmware's host build does, and fetches every module it lists, with no board
needed.
- `et-firmware-modules` generates the source a firmware's build script writes to build ws-modules into its image,
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.
returns the directives that link it. Its `guests` compiles each runner's guest for the board, refusing one that
imports what a board does not host, or guests needing more memory than the board holds for them.
- `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`.
Expand All @@ -51,6 +60,13 @@ Changelog 1.1.0.
- Recovery supervision reconnects after hub interruptions and restores incident context with explicit observation gaps.
Optional dependency checks, fleet action limits, bounded backoff with jitter and sustained-recovery assessment extend
the existing restart policy. Reports distinguish observed failure conditions from reasons for withholding actions.
- `edge-toolkit`'s `board` module lists the interfaces a board's runner hosts, and its `Guest` reads what a component
imports and how much memory it starts with, so a guest can be checked against a board before it is built in.
- `et-ws-wasi-runner` builds as `no_std` + `alloc` without its new default `std` feature, and with its new `board`
feature its `board` module runs a guest on a microcontroller: compiled ahead of time with `engine::precompile` (the
`precompile` feature) for the board's Pulley target, and hosted through the firmware's `board::Platform` and
`board::Transport`. Every guest on a board shares its storage in `board::Buckets`, and draws its memories and
tables from one `board::MemoryBudget`, which refuses growth past what the board holds for them.
- 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
11 changes: 9 additions & 2 deletions CHECKS.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Checks

- **Generated:** 2026-11-02
- **Checks:** 81
- **Checks:** 82
- **Tools:** 48
- **Shell-script checks:** 10
- **Checks with a repair pass:** 22
- **Rules:** 360 custom local rules, or non-default strict linter settings
- **Rules:** 361 custom local rules, or non-default strict linter settings
- **Meta rules:** 20 of those rules are rules about rules

Every check is listed across every `MISE_ENV`, filed under the tool it runs. Each entry names the env whose config
Expand Down Expand Up @@ -212,6 +212,11 @@ Rules, from `config/ast-grep/rules`:
repository root instead -- `edge_toolkit::config::get_project_root()` (runtime) or
`et_path::find_project_root_from_manifest()` (build scripts) -- and join the remainder onto it. (Char literals like
`'.'` for splitting are unaffected.)
- `no-repeated-wildcard-pattern` -- Two or more `_` side by side in a tuple or tuple-struct pattern each ignore one
field where `..` ignores them all. Write the run as `..`: `(outcome, _, _, _)` becomes `(outcome, ..)`, `(_, _, path)`
becomes `(.., path)`, and `(outcome, _, _, closed)` becomes `(outcome, .., closed)`. The pattern then says which
fields matter rather than counting the ones that do not, and it keeps compiling when one is added. This mirrors
`DeepSource RS-W1213`; clippy has no equivalent.
- `no-result-alias` -- Defining a `type Result<...>` alias shadows `std::result::Result` at file scope and makes every
`Result<T>` ambiguous to readers. Spell the error type at each call site instead (`Result<T, my::Error>`).
- `no-result-string-err` -- `Result<_, String>` flattens any typed error to a free-form string at the point of failure
Expand Down Expand Up @@ -334,6 +339,8 @@ Rules, from `config/ast-grep/rules`:
`config/clippy.toml`.
- `cargo-clippy-check-esp32c6` (`rust` env; not run by `mise run check`) -- Lint the ESP32-C6 firmware for its own
target. Config: `Cargo.toml`, `config/clippy.toml`.
- `cargo-clippy-check-pico-plus2w` (`rust` env; not run by `mise run check`) -- Lint the Pimoroni Pico Plus 2 W firmware
for its own target. Config: `Cargo.toml`, `config/clippy.toml`.
- `cargo-clippy-check-pico2w` (`rust` env; not run by `mise run check`) -- Lint the Raspberry Pi Pico 2 W firmware for
its own target. Config: `Cargo.toml`, `config/clippy.toml`.
- `cargo-clippy-check-pkg` (`default` env; not run by `mise run check`) -- cargo clippy on one or more workspace
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ et-ws-runner-common = { path = "libs/ws-runner-common", version = "0.3.0" }
et-ws-server = { path = "services/ws-server", version = "0.3.0", default-features = false }
et-ws-service = { path = "services/ws", version = "0.2.0", default-features = false }
et-ws-test-server = { path = "services/ws-test-server", version = "0.2.0" }
et-ws-wasi-runner = { path = "services/ws-wasi-runner", version = "0.3.0", default-features = false }
et-ws-wasm-agent = { path = "services/ws-wasm-agent", version = "0.3.0" }
fake = "5"
fs-err = "3"
Expand Down Expand Up @@ -301,17 +302,19 @@ wasi-webgpu-wasmtime = "0.4"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
wasm-bindgen-test = "0.3"
# Held to the release wasmtime parses with, so a guest it reads as importable is one wasmtime loads the same way.
wasmparser = { version = "0.258", default-features = false, features = ["component-model", "std"] }
# Keep all three on the same major for ABI compat -- wasmtime-wasi-nn tracks wasmtime's version exactly.
# 49.0.5 is the floor, not a preference: RUSTSEC-2026-0352 through RUSTSEC-2026-0364 are fixed only from 49.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.
# Without its default features, which need std, so a board's runner can depend on it; a host build turns them on.
# Pulley likewise, which only a board's runner and the compiler for it need, so ws-wasi-runner's features turn it 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.
Expand All @@ -322,6 +325,8 @@ wasmtime = { version = "49.0.5", default-features = false, features = [
wasmtime-internal-wit-bindgen = { version = "49.0.5", features = ["async", "component-model-async"] }
wasmtime-wasi = "49.0.5"
wasmtime-wasi-nn = { version = "49", default-features = false, features = ["onnx"] }
# Writes the components the tests read from WebAssembly text.
wat = "1"
web-sys = "0.3"
# wgpu-core/wgpu-types come in via wasi-webgpu-wasmtime's re-exports; only the instance handle is ours.
wgpu-core = { version = "29", default-features = false }
Expand Down
Loading
Loading