diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 1013a4cd..22a8a601 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -202,12 +202,22 @@ jobs: # # Every upload names its language and lane in `flags`, so Codecov can report each on its own. The two # DeepSource uploads carry `runner.os == 'Linux'`, for the one-report-per-key reason given on the job. + # + # `disable_search` keeps each upload to the one report it names. With search on, the action also sends every + # coverage-looking file in the workspace, including the unfiltered intermediates the coverage tasks leave + # under target/ (`agent-cov/all.lcov`, `agent-cov/cov-server.lcov`, `pic-viewer-cov/all.lcov`, + # `wasi-cov/wasi.lcov`). Their records for files the tasks deliberately drop land on the real sources, which + # showed as hits and misses on blank and doc-comment lines of services/ws-web-runner/src/runtime.rs at commit + # https://github.com/edge-toolkit/core/commit/e2f650920e0179f4ff877fc2faf704e0b7a02684, upload log + # `Found 10 coverage files to report` at + # https://github.com/edge-toolkit/core/actions/runs/39376279480/job/118475035669. - name: Upload Rust coverage to Codecov (OIDC) if: ${{ !cancelled() && hashFiles('lcov.info') != '' }} uses: codecov/codecov-action@v7 with: use_oidc: true files: lcov.info + disable_search: true flags: rust,${{ matrix.os_flag }} fail_ci_if_error: true @@ -217,6 +227,7 @@ jobs: with: use_oidc: true files: coverage-python.xml + disable_search: true flags: python,${{ matrix.os_flag }} fail_ci_if_error: true diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e3c1fb7d..88b75f02 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -219,6 +219,19 @@ jobs: uses: $/.github/actions/install-mise-tools timeout-minutes: 60 + # Restore the portable MSVC + Windows SDK that prefetch:msvc stages, so a hit never reaches Microsoft. + # The stage is a pure function of config.msvc.toml -- the pinned script and its version flags live there -- + # so that file's hash is the key and any edit to it restages. Only target/msvc/msvc is cached: the task + # renders msvc.env and the two junctions on every run. actions/cache saves only from a successful job, so a + # partial stage is never stored. The licence does not allow republishing these files, which is why this is + # a runner cache and not an upstream-cache mirror. + - name: Cache portable MSVC + if: matrix.compiler == 'msvc' + uses: actions/cache@v6 + with: + path: target/msvc/msvc + key: portable-msvc-${{ hashFiles('.mise/config.msvc.toml') }} + - name: Prefetch dependencies timeout-minutes: 20 env: diff --git a/.mise/config.msvc.toml b/.mise/config.msvc.toml index 81ec1afd..9b0f91c6 100644 --- a/.mise/config.msvc.toml +++ b/.mise/config.msvc.toml @@ -31,9 +31,18 @@ version = "{{ vars.rust_nightly }}-x86_64-pc-windows-gnullvm" [vars] # portable-msvc script pin: gist id + revision (immutable content address) + sha256 of that revision. # Bump all three together after reviewing a new upstream revision. +# The floor is the revision that matches SDK payload names with either path separator. The Visual Studio +# manifest moved them from `Installers\\` to `Installers/`, so older revisions find no SDK package, download +# none, and die at their `versions` step: +# +# sdkv = first((OUTPUT / "Windows Kits/10/bin").glob("*")).name +# AttributeError: 'NoneType' object has no attribute 'name' +# +# Seen on commit https://github.com/edge-toolkit/core/commit/fc2e5639cb63d541269501663973a94d3138ea5f at +# https://github.com/edge-toolkit/core/actions/runs/39527031634/job/118983994353. a_pmsvc_gist = "7f3162ec2988e81e56d5c4e22cde9977" -a_pmsvc_rev = "ba48b0a3d9ea05c6caa5e583eda60fa7510da4bc" -a_pmsvc_sha = "295a6d7e3e6a7132199ec51224216b07c54af29df9c8ba9bf01166ce12451c0a" +a_pmsvc_rev = "49b58850aa5e5c1e65f090d4651ee76607883e11" +a_pmsvc_sha = "1791cccca594ff083ce8d3e0a139e6156d2c312e24332a909e608010f0230a51" # Clear config.windows.toml's et-ws-web-runner exclusion. cargo_ws_excludes = "" # et-cli lands under the msvc target dir once CARGO_BUILD_TARGET below is flipped. @@ -65,11 +74,13 @@ run = """ base="{{ vars.msvc_base }}" coreutils mkdir -p "$base" script="$base/portable-msvc.py" -if [ ! -f "$script" ]; then +sha() { coreutils sha256sum -b "$script" | coreutils cut -c1-64; } +# Fetched when absent or left over from a previous pin, so a bump takes effect on an existing checkout. +if [ ! -f "$script" ] || [ "$(sha)" != "{{ vars.a_pmsvc_sha }}" ]; then src=":http:mmozeiko/{{ vars.a_pmsvc_gist }}/raw/{{ vars.a_pmsvc_rev }}/portable-msvc.py" rclone copyto "$src" "$script" --http-url https://gist.githubusercontent.com --progress fi -actual=$(coreutils sha256sum -b "$script" | coreutils cut -c1-64) +actual=$(sha) [ "$actual" = "{{ vars.a_pmsvc_sha }}" ] || { echo "portable-msvc.py checksum mismatch: $actual" >&2 exit 1 @@ -78,7 +89,10 @@ actual=$(coreutils sha256sum -b "$script" | coreutils cut -c1-64) # The rendered msvc.env tracks whatever is installed, so a bump only needs these two flags changed and a # re-run after deleting target/msvc/msvc. py="{{ vars.py3_win_fwd }}" -if [ ! -d "$base/msvc/VC" ]; then +# A stage counts as complete only with both halves, so one missing its SDK is wiped and staged again. +# The script writes the MSVC half before it unpacks the SDK, so a run that fails in between leaves `VC` behind. +if [ ! -d "$base/msvc/VC" ] || [ ! -d "$base/msvc/Windows Kits/10/bin" ]; then + coreutils rm -rf "$base/msvc" (cd "$base" && "$py" portable-msvc.py --accept-license --msvc-version 14.51 --sdk-version 26100) fi "$py" - "$base/msvc" "$base/msvc.env" <<'PY' diff --git a/.mise/config.rust.toml b/.mise/config.rust.toml index 09870eb0..7619f7c4 100644 --- a/.mise/config.rust.toml +++ b/.mise/config.rust.toml @@ -10,7 +10,7 @@ # Browser wasm-pack builds are coverage-instrumented when ET_TEST_COVERAGE is set. # The web_cov_wrapper RUSTC_WORKSPACE_WRAPPER (built by build-wasm-cov-wrapper) instruments only workspace -# crates -- never the dependency cdylibs (tracing-wasm, wasm-streams) that don't link minicov's +# crates -- never dependency cdylibs such as wasm-streams that don't link minicov's # __llvm_profile_runtime -- while web_cov_feat enables et-web/coverage to supply minicov's runtime. et-web's # __et_capture_coverage export (folded by wasm-bindgen into every module's glue) lets the web-runner pull each # module's .profraw for wasm-cov. diff --git a/.mise/mise.lock b/.mise/mise.lock index 681e3f9f..f50c909d 100644 --- a/.mise/mise.lock +++ b/.mise/mise.lock @@ -1047,6 +1047,7 @@ url = "https://github.com/microsoft/onnxruntime/releases/download/v1.22.0/onnxru url_api = "https://api.github.com/repos/microsoft/onnxruntime/releases/assets/253300385" [tools."github:microsoft/onnxruntime"."platforms.macos-arm64"] +checksum = "blake3:31837329e25a47978b9c7e68f17b71d117ae208c3da65302925796f04245436b" url = "https://github.com/microsoft/onnxruntime/releases/download/v1.22.0/onnxruntime-osx-arm64-1.22.0.tgz" url_api = "https://api.github.com/repos/microsoft/onnxruntime/releases/assets/253300410" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0947a0cd..4d064a12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Changelog 1.1.0. ### Changed - The workspace requires actix-web 4.17. +- **Breaking:** the minimum supported Rust version is 1.99.0, up from 1.91.1, for every published crate. - **Breaking:** an agent's IP address is always known. `AgentSummary::last_known_ip` and `AgentRecord::last_known_ip` are `IpAddr` rather than `Option`, and `AgentRegistry::connect_agent` takes an `IpAddr`. On the wire, `last_known_ip` in an `et-list-agents-response` is a required IP string, never null, diff --git a/CHECKS.md b/CHECKS.md index 237e7bdd..d9610da6 100644 --- a/CHECKS.md +++ b/CHECKS.md @@ -4,7 +4,7 @@ - **Tools:** 47 - **Shell-script checks:** 10 - **Checks with a repair pass:** 21 -- **Rules:** 342 custom local rules, or non-default strict linter settings +- **Rules:** 343 custom local rules, or non-default strict linter settings Every check is listed across every `MISE_ENV`, filed under the tool it runs. Each entry names the env whose config declares it, the aggregate that runs it as part of `mise run check`, the `fix` or `fmt` pass that repairs what it @@ -42,6 +42,9 @@ Rules, from `config/ast-grep/rules`: status -- a raw `std` `Command`'s `.status()` / `.output()` / `.spawn()` errors carry none of that. If a call genuinely needs the raw outcome (e.g. a test that asserts a non-zero exit, or an exit-code proxy), fully-qualify `std::process::Command` at the call site instead of importing it, so this rule does not apply. +- `const-at-top` -- Declare module-level `const` and `static` items at the top of the file, after the `use` lines and + before any type, trait, `impl` or function. A reader then meets the module's fixed values before the code that uses + them. - `derive-attribute-order` -- Put `#[derive(...)]` before every inert or helper attribute on the same item -- `#[non_exhaustive]`, `#[repr]`, `#[cfg_attr]`, and derive helpers such as `#[serde]`, `#[command]`, `#[strum]` or `#[asyncapi]`. Only doc comments, `#[cfg]`, `#[expect]` and attribute macros that must see the item before the derive diff --git a/Cargo.lock b/Cargo.lock index d60bc1f5..fffb30aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1792,11 +1792,11 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d504c8c7ba3f3accaf8e98b7096beb98e1a271404357744c7e9e730c6865ff" +checksum = "c9c587da2696fbaa58e7a2fa2e5bfdd365d0928219f36992304ac1f15e5bc254" dependencies = [ - "cranelift-assembler-x64-meta 0.136.3", + "cranelift-assembler-x64-meta 0.136.4", ] [[package]] @@ -1807,9 +1807,9 @@ checksum = "c7496a6e92b5cee48c5d772b0443df58816dee30fed6ba19b2a28e78037ecedf" [[package]] name = "cranelift-assembler-x64-meta" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c065757905596bc06e82969ebd7b92d691b7fd710a7e9e8181ae0c30dab588d0" +checksum = "f28a60027b7556b680b051651291053ec381edf4ffbc01784997ae51a64269bb" dependencies = [ "cranelift-srcgen", ] @@ -1825,11 +1825,11 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca7162cd4d4fc5f9f2f88d1459d939882203d93a8c5a19ef2c405b2e82ef13f" +checksum = "62bfdb846acebfd591d601994ea283c98d525ac0bc9a84ca3f2c36bcf5b69ff1" dependencies = [ - "cranelift-entity 0.136.3", + "cranelift-entity 0.136.4", "wasmtime-internal-core", ] @@ -1841,9 +1841,9 @@ checksum = "573c641174c40ef31021ae4a5a3ad78974e280633502d0dfc6e362385e0c100f" [[package]] name = "cranelift-bitset" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec2ba2d4f3d959ec4023a10266c3dcfb28381d1938d507a9a5e1abb78bbcc88" +checksum = "aa9f8cc1080739d5f72b4ee7e00aff7d589f1284beaf0a19472c07878be7f299" dependencies = [ "serde", "serde_derive", @@ -1877,19 +1877,19 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51a64cb821d5fed56bdd6053acd36fa928cd82b70bf6b19b8b14b8b27d047b9a" +checksum = "d85417d493ecec833b5134546b4a252c78cc0bd5be11729a35d25dcff9e4e70b" dependencies = [ "bumpalo", - "cranelift-assembler-x64 0.136.3", - "cranelift-bforest 0.136.3", - "cranelift-bitset 0.136.3", - "cranelift-codegen-meta 0.136.3", - "cranelift-codegen-shared 0.136.3", - "cranelift-control 0.136.3", - "cranelift-entity 0.136.3", - "cranelift-isle 0.136.3", + "cranelift-assembler-x64 0.136.4", + "cranelift-bforest 0.136.4", + "cranelift-bitset 0.136.4", + "cranelift-codegen-meta 0.136.4", + "cranelift-codegen-shared 0.136.4", + "cranelift-control 0.136.4", + "cranelift-entity 0.136.4", + "cranelift-isle 0.136.4", "gimli 0.33.0", "hashbrown 0.17.1", "libm", @@ -1918,12 +1918,12 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142be1e267a7497ba3c2b5a829eaf5387b92b9bcdba6e0a6020f1ffccae7f2b8" +checksum = "d01c8ed0098360bae69ad6b82952d4247516799ce8050d45ad168d6689f4eae6" dependencies = [ - "cranelift-assembler-x64-meta 0.136.3", - "cranelift-codegen-shared 0.136.3", + "cranelift-assembler-x64-meta 0.136.4", + "cranelift-codegen-shared 0.136.4", "cranelift-srcgen", "heck", "pulley-interpreter", @@ -1937,9 +1937,9 @@ checksum = "0f4ff8d2e1235f2d6e7fc3c6738be6954ba972cd295f09079ebffeca2f864e22" [[package]] name = "cranelift-codegen-shared" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e41f75410da6cce8601330137d73718f679db81cb790a33de3871b5c90a0de3" +checksum = "04f9b86d809cec9c4e88b201f1b3ed3b0347674c9ccfbb8c355228669e34be81" [[package]] name = "cranelift-control" @@ -1952,9 +1952,9 @@ dependencies = [ [[package]] name = "cranelift-control" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2616c6d01c14921ac9289ef65eb7702d9c760dfdc4537a8ee5bf38ea2832c6" +checksum = "ce182038fb10b719841e65fdf5deeee258c684f63439f5be680c2be059274b58" dependencies = [ "arbitrary", ] @@ -1970,11 +1970,11 @@ dependencies = [ [[package]] name = "cranelift-entity" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a03e8b44c4877cf2fb21e5788d514af6fd291a066ed9ae37adf114a25fecb4aa" +checksum = "7bca812b03a7b942796391f25bc02aa70a1a007fee3ce2756490c788429e9c39" dependencies = [ - "cranelift-bitset 0.136.3", + "cranelift-bitset 0.136.4", "serde", "serde_derive", "wasmtime-internal-core", @@ -1994,11 +1994,11 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ff7cb014bd08752210eba55a650969f8db9b52670d69d2ea0b23e1e9e3edf" +checksum = "a0c30563d2aef95e8d1a00a920199c99117b350339599f0e9487a7a1cf5d59de" dependencies = [ - "cranelift-codegen 0.136.3", + "cranelift-codegen 0.136.4", "hashbrown 0.17.1", "log", "smallvec 1.16.1", @@ -2013,9 +2013,9 @@ checksum = "f900e0a3847d51eed0321f0777947fb852ccfce0da7fb070100357f69a2f37fc" [[package]] name = "cranelift-isle" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea6526ebf9064fff8ce3b1e26afad7027f9e6bb0e45d2b9200e4cbab8343345" +checksum = "9c54184333a275698edb1d3bdff226131a25d47133d792aa716e47e32b8b7f05" [[package]] name = "cranelift-module" @@ -2041,20 +2041,20 @@ dependencies = [ [[package]] name = "cranelift-native" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b44d323f3e5108a8a5bc61f3acd227285ece066e19afe9aece211d66b34a64ea" +checksum = "eb67a4c8a3180b5f1d77dffcd6317c05386ced42e5a31c7004523a9d99dc85c1" dependencies = [ - "cranelift-codegen 0.136.3", + "cranelift-codegen 0.136.4", "libc", "target-lexicon", ] [[package]] name = "cranelift-srcgen" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc173168f291ff3b95b5d0769d6a05b1eaef5e6d74c2600affab8396b4e7b420" +checksum = "b533db1679b4b728568bd490da6533cf50974c48a4d7534e78f1e0e02bc39769" [[package]] name = "crc-fast" @@ -4741,9 +4741,9 @@ dependencies = [ "et-ws-wasm-agent", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4757,9 +4757,9 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4772,9 +4772,9 @@ dependencies = [ "et-ws-wasm-agent", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4790,9 +4790,9 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4803,9 +4803,9 @@ dependencies = [ "et-web", "et-ws-wasm-agent", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4819,10 +4819,10 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", + "wasm-tracing", "web-sys", ] @@ -4836,9 +4836,9 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4852,9 +4852,9 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4868,10 +4868,10 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", + "wasm-tracing", "web-sys", ] @@ -4885,9 +4885,9 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4902,9 +4902,9 @@ dependencies = [ "serde", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4917,9 +4917,9 @@ dependencies = [ "et-web", "et-ws-wasm-agent", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4933,9 +4933,9 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -4952,10 +4952,10 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", + "wasm-tracing", "web-sys", ] @@ -5016,9 +5016,9 @@ dependencies = [ "et-ws-wasm-agent", "js-sys", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -5094,9 +5094,9 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -5135,9 +5135,9 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-tracing", "web-sys", ] @@ -5226,10 +5226,10 @@ dependencies = [ "js-sys", "serde_json", "tracing", - "tracing-wasm", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", + "wasm-tracing", "web-sys", ] @@ -8992,11 +8992,11 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def59f28ccaf2cb4f8dfaac83197fd7d56a4470a70e4f2589454c731370101c5" +checksum = "5cd3de43f4f4cff536de8902097edbdf749321b97dda548f36a37918eb85bba3" dependencies = [ - "cranelift-bitset 0.136.3", + "cranelift-bitset 0.136.4", "log", "pulley-macros", "wasmtime-internal-core", @@ -9004,9 +9004,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1834d3b3ef994f266a0c9ed29a8214f2c2faaa9b1c44babfa57566bb05af83" +checksum = "8319a8fa5269b4895957f8795861c42822bd26218e495f70060a963323d94847" dependencies = [ "proc-macro2", "quote", @@ -9882,7 +9882,7 @@ dependencies = [ "rustls-webpki", "security-framework 3.7.0", "security-framework-sys", - "webpki-root-certs 1.0.9", + "webpki-root-certs 1.0.10", "windows-sys 0.61.2", ] @@ -9907,9 +9907,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -12023,17 +12023,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tracing-wasm" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" -dependencies = [ - "tracing", - "tracing-subscriber", - "wasm-bindgen", -] - [[package]] name = "tree-sitter" version = "0.27.0" @@ -12684,6 +12673,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-tracing" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ab253baf6d3772bbdb37a0966b67d37ab80657ccd1a084b4d7b3de3232375d" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + [[package]] name = "wasm_dep_analyzer" version = "0.4.0" @@ -12732,9 +12732,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbdd39f5c66f63ba6b5b31391b324b7ade7237d29c866cdb561330118ef436e" +checksum = "36abb25de5518707e5ba96b586366b09db168d2d4976cb9dac57265a317497ea" dependencies = [ "addr2line", "async-trait", @@ -12785,15 +12785,15 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e915c27826de4f7b4411b61fd6db32ca448657a5136b8c8e8874a15e90cecbf4" +checksum = "92f16a4d09fb1fbf2dcfeddb9818e1517a258e13b505b3bfdadcf47a19bf31ca" dependencies = [ "anyhow", "cpp_demangle", - "cranelift-bforest 0.136.3", - "cranelift-bitset 0.136.3", - "cranelift-entity 0.136.3", + "cranelift-bforest 0.136.4", + "cranelift-bitset 0.136.4", + "cranelift-entity 0.136.4", "gimli 0.33.0", "hashbrown 0.17.1", "indexmap", @@ -12816,9 +12816,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952086551d2041430a159fe4bda3471cfea3d52846b93c825829cea9dd01954d" +checksum = "96e4469e885ac50f8dc06337245f9905b22c68d55416267f9afd07f7f76687be" dependencies = [ "base64 0.22.1", "directories-next", @@ -12836,9 +12836,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95f531a0b809fd9fd158be6133c97bd2fc628ac47f5812aff3e3b5624eb02c2" +checksum = "d4fca03bd4425856574c0807d1dee16fad43f947feaf4e9ae1d2129596ee4de4" dependencies = [ "anyhow", "proc-macro2", @@ -12851,15 +12851,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e754a3ff5690c34eb4713bd7c65f60dd607ead4e39042cbf52ac8011de14455b" +checksum = "4006009fc3598af4e895ab18c6c33d2c6b7c657b1bd0a755ca7ee1abef6f0c16" [[package]] name = "wasmtime-internal-core" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba00c5db77adb0749ee059068e5a417bfac3d8969f5a3479d9bd1ca8c1ba248d" +checksum = "947a732ad659f42c10ec639706fc8cb1f068dd9a55eebde8a1f24dfd06dfa525" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -12869,15 +12869,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ceeb4325712a253f463e50a69b5a547fbbecd52e45612360c543eaa25a03ff" +checksum = "fbfb8b455d524939e03df42e3e749f8e26e26ba2acd8b08fa00b3bda55686750" dependencies = [ - "cranelift-codegen 0.136.3", - "cranelift-control 0.136.3", - "cranelift-entity 0.136.3", - "cranelift-frontend 0.136.3", - "cranelift-native 0.136.3", + "cranelift-codegen 0.136.4", + "cranelift-control 0.136.4", + "cranelift-entity 0.136.4", + "cranelift-frontend 0.136.4", + "cranelift-native 0.136.4", "gimli 0.33.0", "itertools 0.14.0", "log", @@ -12895,9 +12895,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb3f21bc32b5382ea95f83ce9a5f2f62b68f1b25a37d0802a41a65236eae5ae" +checksum = "bd8aa8711f68b2cdd94c2b85ba5c8317edaa1ac4ee0a0534b957123680239307" dependencies = [ "cc", "libc", @@ -12909,9 +12909,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e786f9925126e0d2e466c5308e572e8b6226f4047596228a3d63d99528a78949" +checksum = "e873e4504faf50aa8ce255d6ea7210d87b39d5f379c2486782e2c5f179c70ca8" dependencies = [ "cc", "object 0.40.0", @@ -12921,9 +12921,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92cb1dd5da78f5b21d65e91063bdd8c2eb343c1f077a1dcbc29003d4a794abb0" +checksum = "197ab1fda5da958a5c0b8a97eb241beafc48ac568b82ef4f12f38d6866c74173" dependencies = [ "libc", "wasmtime-internal-core", @@ -12932,11 +12932,11 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0243d244f6dd07ccd114e6bb099e8dd1197b47de5c054576adc9483f80f65d8" +checksum = "5577f3a5a979a7c3b1b23c1e9f13e70a8583acc8828471ec56f282117fc4952b" dependencies = [ - "cranelift-codegen 0.136.3", + "cranelift-codegen 0.136.4", "log", "object 0.40.0", "wasmtime-environ", @@ -12944,9 +12944,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20fa5616cd875ec78f5b92020fa47ced7c8bb923079185c8475751098b8c4a69" +checksum = "f6399c9c4050ed431cf66e21c05fadd6f9731db44476f452f8c7142b4597b883" dependencies = [ "proc-macro2", "quote", @@ -12955,9 +12955,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad5e8cef56790d4741b62f7b6c971af53b2ad4c2d1603f8ffe86872763147aa" +checksum = "b4eaf45d4da7681d234bb6efd846b4a528b08a75384a7fc23f125d0145364b06" dependencies = [ "anyhow", "bitflags 2.13.2", @@ -12969,9 +12969,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284555904d42017e049fd57c05810b7975075c322481c045f99b07385fb7a5ba" +checksum = "821e45765c35555d3b0bdf9277cb99478f7d1f29a17b3315ba76e01220f02a38" dependencies = [ "async-trait", "bitflags 2.13.2", @@ -12994,9 +12994,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-io" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebaf573aae85636595bdb976b6c76e9277e1f81db067aff6764cb256e0c17dff" +checksum = "8cdff38d1c730bdaf4fcfbe0eedf3265fb9c7a4deb119deecbfc1c1778950716" dependencies = [ "async-trait", "bytes", @@ -13007,9 +13007,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-nn" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c48bbb53c7b0940f700103c391ccdb52d190c94a09e2ca22027adcfdcff6b3" +checksum = "01dad9a43e8dc1e9a4c7b073faf5d7814d926d1939ee7d27c976288eedefd21e" dependencies = [ "ort", "thiserror 2.0.20", @@ -13100,14 +13100,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.9", + "webpki-root-certs 1.0.10", ] [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb" dependencies = [ "rustls-pki-types", ] @@ -13118,14 +13118,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.9", + "webpki-roots 1.0.10", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" dependencies = [ "rustls-pki-types", ] @@ -13321,9 +13321,9 @@ checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "wiggle" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad3dcdc2980b91d0e7db6975caee4f553c6e37fd8e1464a38b09baceed3fe568" +checksum = "af3872869836d404f722e2361f869c09630e43a73fcc7060f7892a929b74b6a3" dependencies = [ "bitflags 2.13.2", "thiserror 2.0.20", @@ -13335,9 +13335,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4765433ad668e225812b956e0f598011e5cdab4d1a57b2d295ab6863e36ff231" +checksum = "cf7e31dec6de37e7e848aecbad54777dd246cd440a1a547ab1a1e31c27f55e49" dependencies = [ "heck", "proc-macro2", @@ -13349,9 +13349,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94c6623099d11a3fee39986492e803dd0db6676c9d40ab9553a7edd29f317e5" +checksum = "a2af323f33b28ea84b24e2bccef19e51c7a75aa1d2929280ad1a4865f2877dcc" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0bafb8d4..e2d974c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0 or MIT" repository = "https://github.com/edge-toolkit/core" -rust-version = "1.91.1" +rust-version = "1.99.0" [workspace] members = [ @@ -250,7 +250,8 @@ tracing-actix-web = { version = "0.7", default-features = false, features = ["em tracing-log = "0.2" tracing-opentelemetry = "0.33" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tracing-wasm = "0.2" +# Maintained fork; keep the dependency name so browser modules retain the same setup API. +tracing-wasm = { package = "wasm-tracing", version = "2.1" } tree-sitter = "0.27" tree-sitter-zig = "1" # Already resolved transitively (object_store, reqwest, ...), so this adds no new crate or license to review. @@ -263,16 +264,17 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" wasm-bindgen-test = "0.3" # Keep all three on the same major for ABI compat -- wasmtime-wasi-nn tracks wasmtime's version exactly. -# 49.0.3 is the floor, not a preference: RUSTSEC-2026-0352 through RUSTSEC-2026-0364 are fixed only from 49.0.3 -# 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.3", features = ["async", "component-model"] } +# 49.0.4 is the floor, not a preference: RUSTSEC-2026-0352 through RUSTSEC-2026-0364 are fixed only from 49.0.3, +# and RUSTSEC-2026-0378 / RUSTSEC-2026-0379 only from 49.0.4, 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.4", features = ["async", "component-model"] } # 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 # to build from its own tarball. Named `internal` upstream, so treat its API as unstable and pinned to the # wasmtime version above; the drift check catches any change in what it emits. -wasmtime-internal-wit-bindgen = { version = "49.0.3", features = ["async", "component-model-async"] } -wasmtime-wasi = "49.0.3" +wasmtime-internal-wit-bindgen = { version = "49.0.4", features = ["async", "component-model-async"] } +wasmtime-wasi = "49.0.4" wasmtime-wasi-nn = { version = "49", default-features = false, features = ["onnx"] } web-sys = "0.3" # wgpu-core/wgpu-types come in via wasi-webgpu-wasmtime's re-exports; only the instance handle is ours. diff --git a/config/ast-grep/rules/const-at-top.yaml b/config/ast-grep/rules/const-at-top.yaml new file mode 100644 index 00000000..b403662e --- /dev/null +++ b/config/ast-grep/rules/const-at-top.yaml @@ -0,0 +1,36 @@ +id: const-at-top +language: Rust +severity: error +message: | + Declare module-level `const` and `static` items at the top of the file, after the `use` lines and before any + type, trait, `impl` or function. A reader then meets the module's fixed values before the code that uses them. +# Match a top-level const/static with a `use` below it, or a type, trait, impl or fn above it, among its siblings. +# `mod` is deliberately not placed: a trailing `#[cfg(test)] mod test_x;` legitimately follows every const. The +# anonymous `const _: () = { ... };` is exempt: it is an item-level assertion and belongs beside the item it checks. +rule: + all: + - any: + - kind: const_item + - kind: static_item + - inside: + kind: source_file + - not: + regex: '^(pub(\([^)]*\))?\s+)?const\s+_\s*:' + - any: + - precedes: + kind: use_declaration + stopBy: end + - follows: + any: + - kind: enum_item + - kind: function_item + - kind: impl_item + - kind: struct_item + - kind: trait_item + - kind: type_item + - kind: union_item + stopBy: end +ignores: + - generated/** + # Generated; refer to int-gen bindings.rs for details. + - services/ws-wasi-runner/src/bindings.rs diff --git a/config/ast-grep/tests/const-at-top.yaml b/config/ast-grep/tests/const-at-top.yaml new file mode 100644 index 00000000..0ee28167 --- /dev/null +++ b/config/ast-grep/tests/const-at-top.yaml @@ -0,0 +1,89 @@ +id: const-at-top +valid: + - | + use std::time::Duration; + + mod inner; + + const LIMIT: usize = 4; + pub static NAME: &str = "et"; + + fn run() -> usize { + LIMIT + } + - | + fn run() -> usize { + const LOCAL: usize = 4; + LOCAL + } + - | + struct Runner; + + impl Runner { + const STEP: usize = 1; + } + - | + struct Runner; + + const _: () = assert!(size_of::() == 0); + - | + const fn limit() -> usize { + 4 + } + + fn run() -> usize { + limit() + } + - | + use std::time::Duration; + + const LIMIT: usize = 4; + + fn run() -> usize { + LIMIT + } + + #[cfg(test)] + mod test_run; + - | + const LIMIT: usize = 4; + + fn run() -> usize { + use std::cmp::min; + min(LIMIT, 2) + } +invalid: + - | + const LIMIT: usize = 4; + + use std::time::Duration; + - | + pub static NAME: &str = "et"; + + pub use crate::inner::Runner; + - | + fn run() -> usize { + LIMIT + } + + const LIMIT: usize = 4; + - | + struct Runner; + + pub(crate) static NAME: &str = "et"; + - | + enum Mode { + Fast, + } + + /// Documented. + #[cfg(test)] + const LIMIT: usize = 4; + - | + impl Default for Runner { + fn default() -> Self { + Self + } + } + + pub const LIMIT: usize = 4; diff --git a/config/conftest/policy/jscpd/jscpd.rego b/config/conftest/policy/jscpd/jscpd.rego index 7068bc1c..ffbc477a 100644 --- a/config/conftest/policy/jscpd/jscpd.rego +++ b/config/conftest/policy/jscpd/jscpd.rego @@ -24,7 +24,7 @@ package jscpd # Ceiling on the clone fingerprints recorded in config/jscpd-baseline.json. # May only ever be lowered. See the header for what raising it implies. -declared_clones := 323 +declared_clones := 324 # How far under the ceiling the baseline may sit before the ceiling has to be banked downward. max_slack := 20 diff --git a/config/deny.toml b/config/deny.toml index f4e03c5e..c1e87c6c 100644 --- a/config/deny.toml +++ b/config/deny.toml @@ -23,17 +23,17 @@ ignore = [ # newest release. Unmaintained != vulnerable. The advisory covers every version of the crate, 2.x and 3.0.0 # included, so only dropping bincode clears it -- which deno_core did at 0.411.0. deno_runtime 0.267.0 is the # first release built on it (deno_core ^0.412.0); drop this entry with the deno_runtime bump that reaches it. - { id = "RUSTSEC-2025-0141", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2025-0141", reason = "expires 2026-10-31" }, # paste 1.0.15 (unmaintained). # Proc-macro pulled by v8 (rusty_v8) via deno_core / serde_v8; expands at compile time only, so there is no # runtime exposure. Every published v8 release through 152.2.0 declares it as a non-optional dependency, so no # upgrade clears this -- only a rusty_v8 move to the maintained pastey fork. Awaiting that migration. - { id = "RUSTSEC-2024-0436", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2024-0436", reason = "expires 2026-10-31" }, # rustls-pemfile 2.2.0 (unmaintained; final release -- parsing moved into rustls-pki-types). # PEM cert parser for loading the system CA bundle. Unmaintained, not vulnerable. Pulled directly by both # deno_native_certs and deno_tls, each at its newest release, so no bump drops it. rustls-native-certs # migrated at 0.8.1 and our 0.8.4 copy is clean; the affected 0.7.3 copy is pinned by deno_native_certs. - { id = "RUSTSEC-2025-0134", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2025-0134", reason = "expires 2026-10-31" }, # rustls 0.23.40 -- the four rustls entries below share one blocker, so it is described once, here. # deno_tls pins `rustls = "=0.23.40"` by equality through 0.246.0, its newest release, and because a lockfile # holds one rustls 0.23.x that pin also holds ws-server's own TLS listener and every reqwest client at @@ -46,50 +46,57 @@ ignore = [ # for example a plaintext `EncryptedExtensions` packed into the same record as the `ServerHello`. The transcript # is still authenticated, so an attacker cannot alter or complete a handshake with it. CVSS 5.3, client and # server roles alike, and live code here. - { id = "RUSTSEC-2026-0285", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2026-0285", reason = "expires 2026-10-31" }, # `Debug` on client session values prints the TLS 1.2 master secret or TLS 1.3 resumption PSK, fixed 0.23.46. # Reached only through a custom `ClientSessionStore` that formats those values; nothing here implements one, and # the built-in session cache's `Debug` output is redacted. - { id = "RUSTSEC-2026-0346", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2026-0346", reason = "expires 2026-10-31" }, # Brotli certificate decompression can allocate 1 GiB before authentication, fixed in 0.23.46. # Needs rustls's non-default `brotli` feature, which our resolution does not enable -- `cargo tree -i rustls -e # features` reports aws-lc-rs,default,log,logging,prefer-post-quantum,std,tls12 -- so the decompressor is absent. - { id = "RUSTSEC-2026-0348", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2026-0348", reason = "expires 2026-10-31" }, # An unauthenticated ServerHello can steer later client connections to classical-only key exchange, fixed 0.23.46. # Client role, and live here: `prefer-post-quantum` is on, so the downgrade is possible. The cost is losing # post-quantum confidentiality on the affected connections, not a key compromise or an authentication bypass, # and it needs a network attacker who can answer one connection attempt to the same server name. - { id = "RUSTSEC-2026-0349", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2026-0349", reason = "expires 2026-10-31" }, + # webpki-roots / webpki-root-certs 0.26.11 bundle the distrusted emSign ECC Root CA - G3, fixed only in 1.0.10. + # Both 0.26.11 releases are thin re-export shims over `^1` of the same crate, and the lockfile resolves that to + # 1.0.10, so the roots they hand out are the patched set; the advisories match on the shim's own version + # number. deno_tls (`webpki-roots = "^0.26"`) and deno_node (`webpki-root-certs = "^0.26.5"`) require the shims + # through their newest releases, 0.246.0 and 0.197.0, so no bump drops them. Drop both when Deno moves to ^1. + { id = "RUSTSEC-2026-0381", reason = "expires 2026-10-31" }, + { id = "RUSTSEC-2026-0383", reason = "expires 2026-10-31" }, # smartstring 1.0.1 (unmaintained; repo archived 2026-05-03). # Pulled only by the swc stack (swc_ecma_lexer/parser 26/27) under deno_ast -> deno_runtime. Maintenance # notice, not a vulnerability. Upstream swc migrated to compact_str at swc_ecma_lexer 41.0.0 and # swc_ecma_parser 42.0.0, but deno_ast pins both by equality (=26.0.0 / =27.0.7) through its newest release, # so no bump on our side reaches them. Drop when a deno_runtime/deno_ast bump pulls the migrated swc. - { id = "RUSTSEC-2026-0249", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2026-0249", reason = "expires 2026-10-31" }, # hickory-proto 0.25.2 NSEC3 closest-encloser proof unbounded loop (DoS on DNSSEC validation). # Pulled by deno_net for resolver. The advisory scopes reachability to builds carrying dnssec-ring or # dnssec-aws-lc-rs, and our resolution enables neither -- cargo tree -i hickory-proto -f "{p} [{f}]" reports # default,futures-io,serde,std,tokio -- so DnssecDnsHandle is never compiled in. (Cargo.lock listing ring # under hickory-proto is not evidence otherwise; it records optional deps regardless of activation.) The fix # landed in 0.26.0-beta.1, but deno_net requires ^0.25.2 through its newest release, so it is out of reach. - { id = "RUSTSEC-2026-0118", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2026-0118", reason = "expires 2026-10-31" }, # hickory-proto 0.25.2 O(n^2) name compression during message encode. # Encoder side, hit only when we *send* a DNS message. The runner uses the OS resolver via hickory. It # never crafts outbound DNS frames with attacker-controlled name lists. - { id = "RUSTSEC-2026-0119", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2026-0119", reason = "expires 2026-10-31" }, # hickory-resolver 0.25.2 lookup() / lookup_ip() return Ok for a response DNSSEC validation found bogus. # Pulled by deno_net and deno_fetch for the web runner's resolver. The advisory needs a resolver with DNSSEC # validation enabled, which needs a dnssec-* feature, and our resolution enables none -- cargo tree -i # hickory-resolver -e features reports default,serde,system-config,tokio -- so no response is ever validated and # none can be passed off as valid. The fix is 0.26.2, but deno_net and deno_fetch require ^0.25.2 through their # newest releases (0.251.0 / 0.283.0), so it is out of reach. - { id = "GHSA-5j98-2g5x-46v6", reason = "expires 2026-10-23" }, + { id = "GHSA-5j98-2g5x-46v6", reason = "expires 2026-10-31" }, # hickory-resolver 0.25.2 follows CNAME records outside the answer's chain, sending queries it does not need. # Same deno_net / deno_fetch path and the same unreachable 0.26.2 fix as the entry above. Unlike that one, no # feature gates it, so the code is live: a response carrying stray CNAMEs costs extra queries. The cost is query # amplification, not a wrong answer, and the runner resolves only the ws-server host it is configured with, so # the responses come from the operator's own DNS rather than one an attacker chooses. - { id = "GHSA-6f2x-v7q7-m7m5", reason = "expires 2026-10-23" }, + { id = "GHSA-6f2x-v7q7-m7m5", reason = "expires 2026-10-31" }, # rand 0.8.5 unsound ThreadRng reseed when a custom `log` logger calls rand::rng() (UB via aliased &mut). # Transitive via deno_crypto / deno_fs / deno_node (web-runner), used for non-cryptographic randomness # (jitter, salt scratchpads). The advisory's first precondition is the `log` feature, which our resolution @@ -97,7 +104,7 @@ ignore = [ # Fixed in the 0.8 line at 0.8.6, which deleted the log optional dep -- but deno_crypto pins rand "=0.8.5" # by equality through its newest release, so neither a lockfile bump nor a deno bump moves it. cargo-deny # does not match this id and warns advisory-not-detected; the entry stays because osv-scanner does flag it. - { id = "RUSTSEC-2026-0097", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2026-0097", reason = "expires 2026-10-31" }, # rsa 0.9.10 Marvin Attack timing sidechannel on RSA decryption. # No patched version exists -- the advisory lists none, upstream is still migrating to a constant-time # implementation, and 0.9.10 is the newest stable (the 0.10 line is release candidates, equally affected), @@ -105,7 +112,7 @@ ignore = [ # RSA keys or decrypt RSA payloads. Only TLS server cert verification, which uses *signature* checks, no # decryption. Not reachable unless guest JS explicitly imports crypto.subtle and performs RSA-OAEP decrypt # with a private key. Our modules don't. - { id = "RUSTSEC-2023-0071", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2023-0071", reason = "expires 2026-10-31" }, # h2 0.3.27 unbounded empty DATA frames (low-severity HTTP/2 DoS: undrained streams grow memory). # Accepted for the 0.3 copy only. hyperium patched the 0.4 line (>= 0.4.16, which Cargo.lock now pins for the # hyper/reqwest path) and shipped no 0.3 backport, so 0.3.27 is the last release of a dead line. It arrives @@ -115,14 +122,14 @@ ignore = [ # this when it ships one built on h2 0.4. cargo-deny 0.20 ignores by id only (no version scoping), so this # also mutes the 0.4 line -- Cargo.lock pins 0.4.16 there, and a bump below that would have to come back # through this entry, so re-check both copies when touching it. - { id = "RUSTSEC-2026-0258", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2026-0258", reason = "expires 2026-10-31" }, # unsafe-libyaml 0.2.11 (unmaintained; repository archived 2024-03-25, so no version is patched). # A c2rust translation of libyaml, reached through serde_yaml, itself deprecated. Two things pull serde_yaml: # our own crates, for registry, scenario and generated-deployment YAML, and progenitor-macro, which parses the # OpenAPI document at compile time. Unmaintained, not vulnerable. Moving our crates to a safe-Rust parser # (serde-saphyr is the advisory's suggestion) is in our hands, but progenitor-macro would still carry it, so # drop this only once both have moved. - { id = "RUSTSEC-2024-0449", reason = "expires 2026-10-23" }, + { id = "RUSTSEC-2024-0449", reason = "expires 2026-10-31" }, ] [licenses] diff --git a/config/generated-trees.toml b/config/generated-trees.toml index 41fb3c56..04606d0d 100644 --- a/config/generated-trees.toml +++ b/config/generated-trees.toml @@ -42,6 +42,7 @@ regenerated_by = "mise run gen-specs-all" required_in = [ "config/ast-grep/rules/command-requires-command-error.yaml", + "config/ast-grep/rules/const-at-top.yaml", "config/ast-grep/rules/derive-attribute-order.yaml", "config/ast-grep/rules/doc-summary-ends-with-period.yaml", "config/ast-grep/rules/inherent-impl-first.yaml", diff --git a/config/osv-scanner.toml b/config/osv-scanner.toml index b75ad0ed..98fa975c 100644 --- a/config/osv-scanner.toml +++ b/config/osv-scanner.toml @@ -1,20 +1,22 @@ # 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-5j98-2g5x-46v6", ignoreUntil = 2026-10-23 }, - { id = "GHSA-6f2x-v7q7-m7m5", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2023-0071", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2024-0436", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2024-0449", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2025-0134", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2025-0141", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2026-0097", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2026-0118", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2026-0119", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2026-0249", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2026-0258", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2026-0285", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2026-0346", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2026-0348", ignoreUntil = 2026-10-23 }, - { id = "RUSTSEC-2026-0349", ignoreUntil = 2026-10-23 }, + { id = "GHSA-5j98-2g5x-46v6", ignoreUntil = 2026-10-31 }, + { id = "GHSA-6f2x-v7q7-m7m5", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2023-0071", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2024-0436", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2024-0449", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2025-0134", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2025-0141", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0097", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0118", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0119", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0249", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0258", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0285", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0346", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0348", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0349", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0381", ignoreUntil = 2026-10-31 }, + { id = "RUSTSEC-2026-0383", ignoreUntil = 2026-10-31 }, ] diff --git a/libs/edge-toolkit/src/ports.rs b/libs/edge-toolkit/src/ports.rs index 85106efd..e5fd5846 100644 --- a/libs/edge-toolkit/src/ports.rs +++ b/libs/edge-toolkit/src/ports.rs @@ -1,5 +1,7 @@ //! Port allocation. +use Services::{InsecureWebSocketServer, OtlpCollector, SecureWebSocketServer}; + /// Define each services port. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] @@ -13,8 +15,6 @@ pub enum Services { SecureWebSocketServer, } -use Services::{InsecureWebSocketServer, OtlpCollector, SecureWebSocketServer}; - impl Services { /// Get the allocation port for the service. #[must_use] diff --git a/libs/edge-toolkit/src/ws_server.rs b/libs/edge-toolkit/src/ws_server.rs index a1174f9c..84f457ff 100644 --- a/libs/edge-toolkit/src/ws_server.rs +++ b/libs/edge-toolkit/src/ws_server.rs @@ -8,6 +8,9 @@ use tracing::{info, warn}; use crate::ws::{AgentConnectionState, AgentSummary, ConnectStatus}; +/// Longest agent id an agent may choose for itself; a generated UUID is 36 characters. +pub const MAX_AGENT_ID_LEN: usize = 64; + #[derive(Debug, Error)] #[non_exhaustive] pub enum RegistryError { @@ -60,9 +63,6 @@ fn lock_agents( agents.lock().unwrap_or_else(PoisonError::into_inner) } -/// Longest agent id an agent may choose for itself; a generated UUID is 36 characters. -pub const MAX_AGENT_ID_LEN: usize = 64; - /// Whether `id` is acceptable as an agent id the agent chose itself rather than one the hub generated. /// /// The id becomes the first segment of the agent's storage paths (`/`), so it is held to characters diff --git a/libs/edge-toolkit/tests/http_pyodide.rs b/libs/edge-toolkit/tests/http_pyodide.rs index befc5a5c..58c594ce 100644 --- a/libs/edge-toolkit/tests/http_pyodide.rs +++ b/libs/edge-toolkit/tests/http_pyodide.rs @@ -14,6 +14,17 @@ use std::path::PathBuf; use edge_toolkit::config::{Language, default_modules_folders, mise_env_includes, mise_where}; +/// Lower bound -- the official 0.29.x release ships well over 300 wheels. +/// +/// 100 is conservative enough to survive minor releases dropping a few rarely-used packages without flapping in CI. +const MIN_WHEEL_COUNT: usize = 100; + +/// Wheels we always expect in the full distribution. +/// +/// Each entry is a `-` prefix; the test passes if at least one filename in the install starts with it. These +/// three are the "is this the full release?" canaries: none of them ship with `npm:pyodide`. +const REQUIRED_WHEEL_PREFIXES: &[&str] = &["numpy-", "scipy-", "pandas-"]; + /// Shared resolver -- every test in this file wants the install path. /// /// Returns `Some(path)` when found; `None` (and logs a skip line) when `http:pyodide` is intentionally not installed @@ -36,17 +47,6 @@ fn find_http_pyodide_install() -> Option { ) } -/// Lower bound -- the official 0.29.x release ships well over 300 wheels. -/// -/// 100 is conservative enough to survive minor releases dropping a few rarely-used packages without flapping in CI. -const MIN_WHEEL_COUNT: usize = 100; - -/// Wheels we always expect in the full distribution. -/// -/// Each entry is a `-` prefix; the test passes if at least one filename in the install starts with it. These -/// three are the "is this the full release?" canaries: none of them ship with `npm:pyodide`. -const REQUIRED_WHEEL_PREFIXES: &[&str] = &["numpy-", "scipy-", "pandas-"]; - #[test] fn http_pyodide_install_contains_full_wheel_set() { let Some(install) = find_http_pyodide_install() else { diff --git a/libs/wasi-guest/src/lib.rs b/libs/wasi-guest/src/lib.rs index 041c0d74..5cf6c518 100644 --- a/libs/wasi-guest/src/lib.rs +++ b/libs/wasi-guest/src/lib.rs @@ -43,6 +43,12 @@ const AGENT_ID_POLL_INTERVAL_MS: u64 = 50; /// Nanoseconds per millisecond, for the monotonic clock's duration argument. const NANOS_PER_MILLI: u64 = 1_000_000; +/// The calling guest's log context, set once by [`start`]. +/// +/// Held here rather than passed to every [`info`] call so a guest's logging reads as `info("...")` with no +/// per-guest wrapper function to define -- which is what stops four guests from each carrying an identical one. +static LOG_CONTEXT: OnceLock = OnceLock::new(); + // Lets `?` lift a `ws-error` into `entry-error.ws(...)` so a guest's `run` body stays free of explicit // `.map_err`s, which the workspace bans outside the error.rs files it names anyway. impl From for EntryError { @@ -59,12 +65,6 @@ impl From for EntryError { } } -/// The calling guest's log context, set once by [`start`]. -/// -/// Held here rather than passed to every [`info`] call so a guest's logging reads as `info("...")` with no -/// per-guest wrapper function to define -- which is what stops four guests from each carrying an identical one. -static LOG_CONTEXT: OnceLock = OnceLock::new(); - /// Log at info level under the context [`start`] recorded. /// /// Before `start` has run there is no guest to name, so this falls back to this crate's own name rather than diff --git a/libs/ws-runner-common/src/config.rs b/libs/ws-runner-common/src/config.rs index 98a7d3a8..fce86b3a 100644 --- a/libs/ws-runner-common/src/config.rs +++ b/libs/ws-runner-common/src/config.rs @@ -11,6 +11,9 @@ use serde::Deserialize; use serde_default::DefaultFromSerde; use serde_inline_default::serde_inline_default; +/// Default time [`crate::connect_and_register`] waits for `et-connect-ack`. +pub const DEFAULT_CONNECT_ACK_TIMEOUT: Duration = Duration::from_secs(5); + /// Declare a runner's `Config`, with the fields every runner reads supplied and its own added after. /// /// A macro rather than a shared struct the runners nest or flatten. Nesting changes the variable names @@ -68,9 +71,6 @@ pub struct RunnerConfig { pub timeout: Option, } -/// Default time [`crate::connect_and_register`] waits for `et-connect-ack`. -pub const DEFAULT_CONNECT_ACK_TIMEOUT: Duration = Duration::from_secs(5); - /// Shared `WS_*` settings for both native runners. #[serde_inline_default] #[derive(Clone, Debug, DefaultFromSerde, Deserialize)] diff --git a/libs/ws-runner-common/src/lib.rs b/libs/ws-runner-common/src/lib.rs index a096adc6..5b184b8c 100644 --- a/libs/ws-runner-common/src/lib.rs +++ b/libs/ws-runner-common/src/lib.rs @@ -28,9 +28,6 @@ pub mod pyo3_config; pub mod wasi_config; pub mod web_config; -/// A live websocket to the ws-server that has completed the et-connect handshake. -pub type RegisteredSocket = WebSocketStream>; - /// Lower bound on the connect-ack retry interval. /// /// Stops a small configured timeout from collapsing the backoff to near-zero @@ -52,6 +49,24 @@ const MAX_RETRY_INTERVAL: Duration = Duration::from_secs(5); /// must ping well inside that window. pub const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +/// How long [`fetch_main_field`] keeps retrying a module that the hub is not serving yet. +/// +/// A deployment starts the hub and its runners together, and the hub binds its port before it has finished +/// scanning the module paths -- so a runner can arrive to a connection refused, or to a 404 that becomes a 200 a +/// moment later. Retrying here fixes that race for every deployment shape at once, which a wait bolted onto one +/// generator's tasks cannot: a compose stack gates its runners on the hub's healthcheck, and `/health` answers +/// before the module scan finishes. +/// +/// The cost is that a genuinely missing module now takes this long to report. That is the right trade for a +/// runner whose whole job is to wait for work, and the error names the module either way. +const MODULE_WAIT: Duration = Duration::from_secs(120); + +/// Gap between attempts, short enough to add no noticeable delay once the module is being served. +const MODULE_RETRY_INTERVAL: Duration = Duration::from_millis(500); + +/// A live websocket to the ws-server that has completed the et-connect handshake. +pub type RegisteredSocket = WebSocketStream>; + /// Build a heartbeat ticker firing every [`HEARTBEAT_INTERVAL`]. /// /// The immediate first tick is consumed so the first heartbeat fires one @@ -296,21 +311,6 @@ pub async fn collect_byte_stream(mut stream: et_rest_client::ByteStream) -> Resu /// Returns [`BootstrapError::Rest`] / [`BootstrapError::Stream`] if the fetch /// fails, [`BootstrapError::PackageJsonInvalid`] if the body is not valid JSON, /// or [`BootstrapError::PackageJsonMissingMain`] if it has no `main` field. -/// How long [`fetch_main_field`] keeps retrying a module that the hub is not serving yet. -/// -/// A deployment starts the hub and its runners together, and the hub binds its port before it has finished -/// scanning the module paths -- so a runner can arrive to a connection refused, or to a 404 that becomes a 200 a -/// moment later. Retrying here fixes that race for every deployment shape at once, which a wait bolted onto one -/// generator's tasks cannot: a compose stack gates its runners on the hub's healthcheck, and `/health` answers -/// before the module scan finishes. -/// -/// The cost is that a genuinely missing module now takes this long to report. That is the right trade for a -/// runner whose whole job is to wait for work, and the error names the module either way. -const MODULE_WAIT: Duration = Duration::from_secs(120); - -/// Gap between attempts, short enough to add no noticeable delay once the module is being served. -const MODULE_RETRY_INTERVAL: Duration = Duration::from_millis(500); - #[instrument(name = "fetch_package_json", skip(client), err)] pub async fn fetch_main_field(client: &et_rest_client::Client, module_name: &str) -> Result { let bytes = fetch_package_json_bytes(client, module_name).await?; diff --git a/services/websockify/src/lib.rs b/services/websockify/src/lib.rs index 708a722e..8794e098 100644 --- a/services/websockify/src/lib.rs +++ b/services/websockify/src/lib.rs @@ -11,30 +11,6 @@ use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; use tokio::net::TcpStream; use tracing::{debug, info, warn}; -/// Loopback TCP endpoint every `/websockify` connection is bridged to. -/// -/// Set by the server in [`configure`], never by the client -- this is what keeps the relay from being an open -/// proxy: a browser chooses neither the host nor the port. -#[derive(Clone, Copy)] -struct RelayTarget(SocketAddr); - -/// Register the `/websockify` relay route, bridging every connection to `target`. -/// -/// `target` is expected to be a loopback address -- the ws-server passes its own plain-HTTP port. Register this -/// before any catch-all `Files::new("/")` mount so the explicit route wins. -#[expect( - clippy::literal_string_with_formatting_args, - reason = "`{tail:.*}` is an actix-web route path pattern, not a format string" -)] -pub fn configure(cfg: &mut web::ServiceConfig, target: SocketAddr) { - // Match a trailing path too: Emscripten may append the connect target to the configured WebSocket URL - // (e.g. `/websockify/127.0.0.1:8080`). The suffix is ignored -- the relay always bridges to the fixed - // server-configured target -- so it stays SSRF-safe regardless of what the client puts there. - let _routed = cfg - .app_data(web::Data::new(RelayTarget(target))) - .route("/websockify{tail:.*}", web::get().to(relay_handler)); -} - /// Bytes copied per TCP->WebSocket read. 16 KiB is a typical socket read; large reads just loop. const RELAY_BUF: usize = 16 * 1024; @@ -61,6 +37,30 @@ const SOCKS5_REPLY_OK: [u8; 10] = [SOCKS5_VERSION, 0x00, 0x00, SOCKS5_ATYP_IPV4, /// CONNECT reply, connection not allowed by ruleset (REP=2). const SOCKS5_REPLY_REFUSED: [u8; 10] = [SOCKS5_VERSION, 0x02, 0x00, SOCKS5_ATYP_IPV4, 0, 0, 0, 0, 0, 0]; +/// Loopback TCP endpoint every `/websockify` connection is bridged to. +/// +/// Set by the server in [`configure`], never by the client -- this is what keeps the relay from being an open +/// proxy: a browser chooses neither the host nor the port. +#[derive(Clone, Copy)] +struct RelayTarget(SocketAddr); + +/// Register the `/websockify` relay route, bridging every connection to `target`. +/// +/// `target` is expected to be a loopback address -- the ws-server passes its own plain-HTTP port. Register this +/// before any catch-all `Files::new("/")` mount so the explicit route wins. +#[expect( + clippy::literal_string_with_formatting_args, + reason = "`{tail:.*}` is an actix-web route path pattern, not a format string" +)] +pub fn configure(cfg: &mut web::ServiceConfig, target: SocketAddr) { + // Match a trailing path too: Emscripten may append the connect target to the configured WebSocket URL + // (e.g. `/websockify/127.0.0.1:8080`). The suffix is ignored -- the relay always bridges to the fixed + // server-configured target -- so it stays SSRF-safe regardless of what the client puts there. + let _routed = cfg + .app_data(web::Data::new(RelayTarget(target))) + .route("/websockify{tail:.*}", web::get().to(relay_handler)); +} + /// Render the first [`PREVIEW_BYTES`] of a relayed chunk as an escaped string for logging. /// /// Escaping makes CR/LF and any non-printable bytes visible, so a malformed HTTP request line or header (or an diff --git a/services/ws-pyo3-runner/tests/modules.rs b/services/ws-pyo3-runner/tests/modules.rs index a52865c1..c3ab3340 100644 --- a/services/ws-pyo3-runner/tests/modules.rs +++ b/services/ws-pyo3-runner/tests/modules.rs @@ -30,8 +30,6 @@ use futures_util::{SinkExt as _, StreamExt as _}; use rstest::rstest; use tokio_tungstenite::{connect_async, tungstenite}; -type ControlSocket = tokio_tungstenite::WebSocketStream>; - /// Overall `run_exchange` budget for the torch case. /// /// Covers the whole exchange, the peer wait included -- and the peer wait is the long part, because the case's @@ -54,6 +52,8 @@ const POLL_BACKOFF_MAX: Duration = Duration::from_millis(500); const POLL_DRAIN_WINDOW: Duration = Duration::from_millis(250); const RUST_LOG: &str = "RUST_LOG"; +type ControlSocket = tokio_tungstenite::WebSocketStream>; + /// When a case may not be runnable, the condition under which it self-skips. enum Gate { /// Always runs (needs only the embedded interpreter + bundled module). diff --git a/services/ws-test-server/src/lib.rs b/services/ws-test-server/src/lib.rs index 647e3059..288c872e 100644 --- a/services/ws-test-server/src/lib.rs +++ b/services/ws-test-server/src/lib.rs @@ -28,6 +28,12 @@ use tracing_actix_web::TracingLogger; pub mod math1; +/// Actix workers per test hub; test traffic never needs more, and each worker repeats the module scan at startup. +const TEST_HUB_WORKERS: usize = 2; + +/// How long one [`wait_for_connected_agents`] round reads replies before asking the hub again. +const ROSTER_POLL_INTERVAL: Duration = Duration::from_millis(250); + /// A running test server. The temporary storage directory is cleaned up on drop. #[non_exhaustive] pub struct TestServer { @@ -97,9 +103,6 @@ pub fn start_clustered(nats_url: &str, storage_root: &Path) -> TestServer { ) } -/// Actix workers per test hub; test traffic never needs more, and each worker repeats the module scan at startup. -const TEST_HUB_WORKERS: usize = 2; - /// Run one hub on `port` on its own thread and actix system, returning once it is bound and on its backplane. fn spawn_hub(port: u16, storage_dir: TempDir, storage_config: StorageConfig, ws_config: WsConfig) -> TestServer { // The default search paths, with the page module named outright. The server has no default for which module it @@ -159,9 +162,6 @@ fn spawn_hub(port: u16, storage_dir: TempDir, storage_config: StorageConfig, ws_ } } -/// How long one [`wait_for_connected_agents`] round reads replies before asking the hub again. -const ROSTER_POLL_INTERVAL: Duration = Duration::from_millis(250); - /// Block until `count` agents other than the waiter itself are connected to the hub at `ws_url`. /// /// Returns the connected peer ids from the last roster the hub sent: at least `count` of them once the wait succeeds, diff --git a/services/ws-wasi-runner/src/host/ws.rs b/services/ws-wasi-runner/src/host/ws.rs index 215a8890..89395033 100644 --- a/services/ws-wasi-runner/src/host/ws.rs +++ b/services/ws-wasi-runner/src/host/ws.rs @@ -33,7 +33,7 @@ use crate::bindings::et::ws_messages::messages::{ MessageScope as WitMessageScope, MessageStatusPayload, RelayBinaryPayload, RelayTextPayload, ResponsePayload, ServerMessage as WitServerMessage, }; -use crate::bindings::et::ws_wasi::ws::{Host, State}; +use crate::bindings::et::ws_wasi::ws::{Host, State, WsError}; // The `et:ws-messages/messages` interface only declares types -- no functions. wasmtime-bindgen still requires a `Host` // impl so the linker has somewhere to anchor the interface. The trait body is empty. @@ -41,8 +41,6 @@ impl crate::bindings::et::ws_messages::messages::Host for HostState {} type WsSink = SplitSink>, tungstenite::Message>; -use crate::bindings::et::ws_wasi::ws::WsError; - /// Live state for an open websocket connection. /// /// Owned by `HostState` behind a `Mutex`; replaced on disconnect. diff --git a/services/ws-wasm-agent/tests/client.rs b/services/ws-wasm-agent/tests/client.rs index 5a7e5121..dad7af07 100644 --- a/services/ws-wasm-agent/tests/client.rs +++ b/services/ws-wasm-agent/tests/client.rs @@ -31,18 +31,18 @@ use wasm_bindgen::prelude::Closure; use wasm_bindgen_futures::JsFuture; use wasm_bindgen_test::*; -wasm_bindgen_test_configure!(run_in_browser); - -fn new_client(url: &str) -> WsClient { - WsClient::new(WsClientConfig::new(url.to_string())) -} - /// The live ws-server URL for the connected-path tests. /// /// A backend must be listening here: the `wasm-agent-cov` mise task starts the in-process et-ws-test-server on /// this port, and `ws-e2e-chrome` runs the real ws-server on it too. const SERVER_URL: &str = "ws://127.0.0.1:8080/ws"; +wasm_bindgen_test_configure!(run_in_browser); + +fn new_client(url: &str) -> WsClient { + WsClient::new(WsClientConfig::new(url.to_string())) +} + async fn sleep(millis: i32) { let promise = Promise::new(&mut |resolve, _reject| { let window = web_sys::window().unwrap(); diff --git a/services/ws-web-runner/src/runtime.rs b/services/ws-web-runner/src/runtime.rs index 2d6d3d23..75efb166 100644 --- a/services/ws-web-runner/src/runtime.rs +++ b/services/ws-web-runner/src/runtime.rs @@ -31,6 +31,24 @@ use tracing::error; use crate::error::JsErrExt as _; +/// Browser-environment shim fragments, concatenated in order ahead of the module. +/// +/// Split by concern under `shims/`; each fragment documents itself. +const SHIMS: &[&str] = &[ + include_str!("shims/main.js"), + include_str!("shims/document.js"), + include_str!("shims/events.js"), + include_str!("shims/wasm_streaming.js"), + include_str!("shims/xhr.js"), +]; + +/// Coverage-capture shim fragments, compiled in only under the `coverage` feature. +/// +/// `ws_agent_id` sniffs the module's server-assigned `agent_id` (its storage bucket); `pycov` defines the Pyodide +/// coverage.py helper. Both are inert until `globalThis.__ET_TEST_COVERAGE` is set (see `shim_js`). +#[cfg(feature = "coverage")] +const COVERAGE_SHIMS: &[&str] = &[include_str!("shims/ws_agent_id.js"), include_str!("shims/pycov.js")]; + /// Module loader that fetches JavaScript from the ws-server over HTTP. /// /// Uses the typed REST client's inner `reqwest::Client` so the loader can fetch arbitrary module sub-paths (ES @@ -95,24 +113,6 @@ impl deno_core::ModuleLoader for ServerModuleLoader { } } -/// Browser-environment shim fragments, concatenated in order ahead of the module. -/// -/// Split by concern under `shims/`; each fragment documents itself. -const SHIMS: &[&str] = &[ - include_str!("shims/main.js"), - include_str!("shims/document.js"), - include_str!("shims/events.js"), - include_str!("shims/wasm_streaming.js"), - include_str!("shims/xhr.js"), -]; - -/// Coverage-capture shim fragments, compiled in only under the `coverage` feature. -/// -/// `ws_agent_id` sniffs the module's server-assigned `agent_id` (its storage bucket); `pycov` defines the Pyodide -/// coverage.py helper. Both are inert until `globalThis.__ET_TEST_COVERAGE` is set (see `shim_js`). -#[cfg(feature = "coverage")] -const COVERAGE_SHIMS: &[&str] = &[include_str!("shims/ws_agent_id.js"), include_str!("shims/pycov.js")]; - /// Render the full shim for a run. /// /// Emits the per-run URL globals -- the ws-server's HTTP base (used for `location` and module URL resolution) and the diff --git a/utilities/cli/src/deployment_types/docker_compose.rs b/utilities/cli/src/deployment_types/docker_compose.rs index d44a0aed..dbd5ace2 100644 --- a/utilities/cli/src/deployment_types/docker_compose.rs +++ b/utilities/cli/src/deployment_types/docker_compose.rs @@ -33,6 +33,15 @@ const OFELIA_IMAGE: &str = "ghcr.io/netresearch/ofelia:1.0.1"; /// host would each schedule the other's jobs. const SCENARIO_LABEL: &str = concat!(et_org::org!(), "/scenario"); +/// Characters YAML reserves as indicators, which a plain scalar may not open with. +/// +/// `@` and a backtick are reserved for future use and are a scanner error outright; the rest introduce anchors, tags, +/// flow collections, block scalars and the like. A value starting with any of them has to be quoted to be read back as +/// the string it is. +const YAML_INDICATORS: [char; 18] = [ + '@', '`', '-', '?', ':', ',', '[', ']', '{', '}', '#', '&', '*', '!', '|', '>', '\'', '"', +]; + /// The scheduler service for the machine `machines` names, or for the single host when it names none. /// /// One per machine with a scheduled runner, since Ofelia can only start a container on the Docker daemon it runs on. @@ -550,15 +559,6 @@ enum ComposeValue { WrappedDoubleQuoted(Vec), } -/// Characters YAML reserves as indicators, which a plain scalar may not open with. -/// -/// `@` and a backtick are reserved for future use and are a scanner error outright; the rest introduce anchors, tags, -/// flow collections, block scalars and the like. A value starting with any of them has to be quoted to be read back as -/// the string it is. -const YAML_INDICATORS: [char; 18] = [ - '@', '`', '-', '?', ':', ',', '[', ']', '{', '}', '#', '&', '*', '!', '|', '>', '\'', '"', -]; - fn render_compose_yaml(compose: &ComposeFile) -> String { let mut renderer = ComposeRenderer::default(); renderer.push_line(0, "services:"); diff --git a/utilities/cli/src/deployment_types/mise.rs b/utilities/cli/src/deployment_types/mise.rs index be282f4d..e0cde8a9 100644 --- a/utilities/cli/src/deployment_types/mise.rs +++ b/utilities/cli/src/deployment_types/mise.rs @@ -46,6 +46,15 @@ pub(crate) const HUB_TASK: &str = "ws-server"; /// The mise task that opens the collector's UI in a browser. pub(crate) const OPENER_TASK: &str = "open-o2"; +/// Version every `cargo:` tool the generated deployment declares is requested at. +const LATEST: &str = "latest"; + +/// The page served at `/`, which only a cluster a browser opens has any use for. +/// +/// `static` names the runtimes its page imports at boot, so a deployment that serves the page without it serves one +/// whose first import 404s -- and one that serves neither is a headless cluster that was never going to load either. +const PAGE_MODULE: &str = "static"; + /// The mise task that starts everything one machine of a multi-machine cluster runs. pub(crate) fn machine_task(machine: &str) -> String { format!("machine-{machine}") @@ -59,9 +68,6 @@ pub(crate) fn collector_container(cluster_name: &str) -> String { format!("openobserve-{cluster_name}") } -/// Version every `cargo:` tool the generated deployment declares is requested at. -const LATEST: &str = "latest"; - /// The task that runs one runner agent, from the checkout root for a local scenario. #[expect(clippy::single_call_fn, reason = "the per-runner step of generate_mise_deployment")] fn runner_task(runner: &RunnerInstance, workspace_rel: &str, artifacts: ArtifactSource) -> Table { @@ -294,12 +300,6 @@ fn openobserve_run_body(cluster_name: &str) -> String { ) } -/// The page served at `/`, which only a cluster a browser opens has any use for. -/// -/// `static` names the runtimes its page imports at boot, so a deployment that serves the page without it serves one -/// whose first import 404s -- and one that serves neither is a headless cluster that was never going to load either. -const PAGE_MODULE: &str = "static"; - /// The scenario's modules, the page first when there is one, each named as the hub serves it. /// /// Nothing else is added: whatever a module needs at run time -- the wasm agent included, which it reaches through the diff --git a/utilities/cli/src/deployment_types/scenario_image.rs b/utilities/cli/src/deployment_types/scenario_image.rs index c28d35a1..19b115e2 100644 --- a/utilities/cli/src/deployment_types/scenario_image.rs +++ b/utilities/cli/src/deployment_types/scenario_image.rs @@ -24,6 +24,107 @@ const HUB_IMAGE_MODULES: [&str; 2] = ["/app/services/ws-server/static", "/app/se /// costs nothing -- only the tools named on the `mise install` line are actually fetched. const DEPS_MISE_ENV: &str = "dart,dotnet,java,js,kotlin,python,r,rust,zig"; +/// The fixed head of the deps stage: the base image, the packages it installs, and a verified mise. +/// +/// Held as a const rather than inlined so the rendering function below stays a short composer of named parts. Nothing +/// in this block varies with the cluster, so there is nothing here to parameterise. +const DEPS_BASE_STAGE: &str = concat!( + "# Stage the packages that live outside the repository.\n", + "# These are published packages the modules load at runtime, provisioned by mise rather than built\n", + "# here, so this stage installs them from the repo's own pins and copies each one to the `/app` path\n", + "# the hub serves it from.\n", + "FROM docker.io/library/debian:bookworm-slim AS deps\n", + "\n", + "# The package list is named rather than inlined so a policy can hold it against the repo-root Dockerfile.\n", + "# Every name here is drawn from that file's own COMMON_PACKAGES: the shell, the archive tools mise unpacks\n", + "# downloads with, plus git and curl. Its compiler packages are deliberately absent -- nothing is compiled\n", + "# in this stage -- so the two lists are checked as subset rather than equality.\n", + "# Promoted to ENV because the quoted heredoc below is expanded by bash at run time, which an ARG alone\n", + "# does not reach.\n", + "ARG COMMON_PACKAGES=\"bash bzip2 ca-certificates curl git gzip tar unzip\"\n", + "ENV COMMON_PACKAGES=${COMMON_PACKAGES}\n", + "RUN bash <<'EOF'\n", + "set -euo pipefail\n", + "apt-get update\n", + "# Word-splitting $COMMON_PACKAGES into separate arguments is the intent, so it is deliberately unquoted.\n", + "apt-get install -y --no-install-recommends $COMMON_PACKAGES\n", + "apt-get clean\n", + "rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb\n", + "EOF\n", + "\n", + "# mise is fetched to a file and checksum-verified, never piped from mise.run into a shell.\n", + "# Piping a remote script into an interpreter runs unreviewed code straight off the network, which the\n", + "# repo's no-curl-pipe rule bans. The release's SHASUMS256.txt covers every asset, so `--ignore-missing`\n", + "# checks exactly the archive downloaded here. Pinned rather than installed from mise's apt repository,\n", + "# which carries only the newest version: a floating mise writes lockfile entries an older CI mise\n", + "# then refuses to install.\n", + "ARG MISE_VERSION=v2026.9.17\n", + "ARG TARGETARCH\n", + "ENV MISE_VERSION=${MISE_VERSION}\n", + "ENV TARGETARCH=${TARGETARCH}\n", + "RUN bash <<'EOF'\n", + "set -euo pipefail\n", + "case \"${TARGETARCH}\" in\n", + " amd64) arch=x64 ;;\n", + " arm64) arch=arm64 ;;\n", + " *) echo \"unsupported TARGETARCH ${TARGETARCH}\" >&2; exit 1 ;;\n", + "esac\n", + "asset=\"mise-${MISE_VERSION}-linux-${arch}.tar.gz\"\n", + "base=\"https://github.com/jdx/mise/releases/download/${MISE_VERSION}\"\n", + "cd /tmp\n", + "curl -fsSL -o \"${asset}\" \"${base}/${asset}\"\n", + "curl -fsSL -o SHASUMS256.txt \"${base}/SHASUMS256.txt\"\n", + "sha256sum -c --ignore-missing SHASUMS256.txt\n", + "tar -xzf \"${asset}\"\n", + "install -m 0755 /tmp/mise/bin/mise /usr/local/bin/mise\n", + "rm -rf /tmp/mise \"/tmp/${asset}\" /tmp/SHASUMS256.txt\n", + "EOF\n", + "\n", + "ENV HOME=/root\n", + "ENV PATH=\"/root/.local/share/mise/shims:${PATH}\"\n", + "WORKDIR /workspace\n", + "COPY .miserc.toml .miserc.toml\n", + "COPY .mise/ .mise/\n", + "RUN mise trust\n", +); + +/// The per-tool copy step, driven entirely by the `STAGE_*` variables the loop emits ahead of it. +/// +/// Locates the package rather than assuming where the backend put it. The npm backend nests it under one of several +/// layouts (`lib/node_modules/`, `node_modules/`, or an aube virtual store below a content-hashed directory) that vary +/// by platform, so this searches for the named directory instead. The named directory is tried FIRST and the install +/// root only as a fallback: the npm backend drops its own wrapper manifest (`"name": "mise-npm-install"`) at that root, +/// so a root-first probe stages the wrapper and the module is then served under the wrapper's name. The fallback is +/// what covers an archive-backed `http:` tool, which extracts flat so the root really is the package. `-L` and `cp -L` +/// resolve the symlinks the aube store is built from, so real files land in the image. +const DEPS_STAGE_TOOL_COPY: &str = concat!( + "RUN bash <<'EOF'\n", + "set -euo pipefail\n", + "root=\"$(mise where \"${STAGE_TOOL}\")\"\n", + "src=\"$(find -L \"${root}\" -type d -name \"${STAGE_PACKAGE}\" ", + "-exec test -f '{}/package.json' ';' -print -quit)\"\n", + "if [ -z \"${src}\" ] && [ -f \"${root}/package.json\" ]; then\n", + " src=\"${root}\"\n", + "fi\n", + "if [ -z \"${src}\" ]; then\n", + " echo \"no ${STAGE_PACKAGE} package dir under the ${STAGE_TOOL} install at ${root}\" >&2\n", + " exit 1\n", + "fi\n", + "mkdir -p \"$(dirname \"${STAGE_DEST}\")\"\n", + "cp -rL \"${src}\" \"${STAGE_DEST}\"\n", + "EOF\n", + "\n", +); + +/// Image the released-modules stage runs `npm` from, on the Node major the repository's own `node` tool is pinned to. +const NODE_IMAGE: &str = "docker.io/library/node:22-bookworm-slim"; + +/// Build secret carrying the token GitHub Packages reads are made with, the id the repo-root Dockerfile uses too. +pub(crate) const REGISTRY_TOKEN_SECRET: &str = "gh_token"; + +/// Environment variable the registry token is read from on the host and exposed as inside the fetching step. +pub(crate) const REGISTRY_TOKEN_ENV: &str = "GITHUB_TOKEN"; + /// Where the scenario image's modules come from, split by what each source needs from the build. /// /// Each list is sorted out of resolution order, which moves a module whenever an unrelated one gains a dependency, so @@ -285,107 +386,6 @@ fn render_dockerfile(cluster_name: &str, published: bool, sources: &ImageSources out } -/// The fixed head of the deps stage: the base image, the packages it installs, and a verified mise. -/// -/// Held as a const rather than inlined so the rendering function below stays a short composer of named parts. Nothing -/// in this block varies with the cluster, so there is nothing here to parameterise. -const DEPS_BASE_STAGE: &str = concat!( - "# Stage the packages that live outside the repository.\n", - "# These are published packages the modules load at runtime, provisioned by mise rather than built\n", - "# here, so this stage installs them from the repo's own pins and copies each one to the `/app` path\n", - "# the hub serves it from.\n", - "FROM docker.io/library/debian:bookworm-slim AS deps\n", - "\n", - "# The package list is named rather than inlined so a policy can hold it against the repo-root Dockerfile.\n", - "# Every name here is drawn from that file's own COMMON_PACKAGES: the shell, the archive tools mise unpacks\n", - "# downloads with, plus git and curl. Its compiler packages are deliberately absent -- nothing is compiled\n", - "# in this stage -- so the two lists are checked as subset rather than equality.\n", - "# Promoted to ENV because the quoted heredoc below is expanded by bash at run time, which an ARG alone\n", - "# does not reach.\n", - "ARG COMMON_PACKAGES=\"bash bzip2 ca-certificates curl git gzip tar unzip\"\n", - "ENV COMMON_PACKAGES=${COMMON_PACKAGES}\n", - "RUN bash <<'EOF'\n", - "set -euo pipefail\n", - "apt-get update\n", - "# Word-splitting $COMMON_PACKAGES into separate arguments is the intent, so it is deliberately unquoted.\n", - "apt-get install -y --no-install-recommends $COMMON_PACKAGES\n", - "apt-get clean\n", - "rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb\n", - "EOF\n", - "\n", - "# mise is fetched to a file and checksum-verified, never piped from mise.run into a shell.\n", - "# Piping a remote script into an interpreter runs unreviewed code straight off the network, which the\n", - "# repo's no-curl-pipe rule bans. The release's SHASUMS256.txt covers every asset, so `--ignore-missing`\n", - "# checks exactly the archive downloaded here. Pinned rather than installed from mise's apt repository,\n", - "# which carries only the newest version: a floating mise writes lockfile entries an older CI mise\n", - "# then refuses to install.\n", - "ARG MISE_VERSION=v2026.9.17\n", - "ARG TARGETARCH\n", - "ENV MISE_VERSION=${MISE_VERSION}\n", - "ENV TARGETARCH=${TARGETARCH}\n", - "RUN bash <<'EOF'\n", - "set -euo pipefail\n", - "case \"${TARGETARCH}\" in\n", - " amd64) arch=x64 ;;\n", - " arm64) arch=arm64 ;;\n", - " *) echo \"unsupported TARGETARCH ${TARGETARCH}\" >&2; exit 1 ;;\n", - "esac\n", - "asset=\"mise-${MISE_VERSION}-linux-${arch}.tar.gz\"\n", - "base=\"https://github.com/jdx/mise/releases/download/${MISE_VERSION}\"\n", - "cd /tmp\n", - "curl -fsSL -o \"${asset}\" \"${base}/${asset}\"\n", - "curl -fsSL -o SHASUMS256.txt \"${base}/SHASUMS256.txt\"\n", - "sha256sum -c --ignore-missing SHASUMS256.txt\n", - "tar -xzf \"${asset}\"\n", - "install -m 0755 /tmp/mise/bin/mise /usr/local/bin/mise\n", - "rm -rf /tmp/mise \"/tmp/${asset}\" /tmp/SHASUMS256.txt\n", - "EOF\n", - "\n", - "ENV HOME=/root\n", - "ENV PATH=\"/root/.local/share/mise/shims:${PATH}\"\n", - "WORKDIR /workspace\n", - "COPY .miserc.toml .miserc.toml\n", - "COPY .mise/ .mise/\n", - "RUN mise trust\n", -); - -/// The per-tool copy step, driven entirely by the `STAGE_*` variables the loop emits ahead of it. -/// -/// Locates the package rather than assuming where the backend put it. The npm backend nests it under one of several -/// layouts (`lib/node_modules/`, `node_modules/`, or an aube virtual store below a content-hashed directory) that vary -/// by platform, so this searches for the named directory instead. The named directory is tried FIRST and the install -/// root only as a fallback: the npm backend drops its own wrapper manifest (`"name": "mise-npm-install"`) at that root, -/// so a root-first probe stages the wrapper and the module is then served under the wrapper's name. The fallback is -/// what covers an archive-backed `http:` tool, which extracts flat so the root really is the package. `-L` and `cp -L` -/// resolve the symlinks the aube store is built from, so real files land in the image. -const DEPS_STAGE_TOOL_COPY: &str = concat!( - "RUN bash <<'EOF'\n", - "set -euo pipefail\n", - "root=\"$(mise where \"${STAGE_TOOL}\")\"\n", - "src=\"$(find -L \"${root}\" -type d -name \"${STAGE_PACKAGE}\" ", - "-exec test -f '{}/package.json' ';' -print -quit)\"\n", - "if [ -z \"${src}\" ] && [ -f \"${root}/package.json\" ]; then\n", - " src=\"${root}\"\n", - "fi\n", - "if [ -z \"${src}\" ]; then\n", - " echo \"no ${STAGE_PACKAGE} package dir under the ${STAGE_TOOL} install at ${root}\" >&2\n", - " exit 1\n", - "fi\n", - "mkdir -p \"$(dirname \"${STAGE_DEST}\")\"\n", - "cp -rL \"${src}\" \"${STAGE_DEST}\"\n", - "EOF\n", - "\n", -); - -/// Image the released-modules stage runs `npm` from, on the Node major the repository's own `node` tool is pinned to. -const NODE_IMAGE: &str = "docker.io/library/node:22-bookworm-slim"; - -/// Build secret carrying the token GitHub Packages reads are made with, the id the repo-root Dockerfile uses too. -pub(crate) const REGISTRY_TOKEN_SECRET: &str = "gh_token"; - -/// Environment variable the registry token is read from on the host and exposed as inside the fetching step. -pub(crate) const REGISTRY_TOKEN_ENV: &str = "GITHUB_TOKEN"; - /// Render the stage that fetches this repository's modules from the registry a published scenario consumes them from. /// /// `npm pack` rather than a download by URL, because it checks each tarball against the integrity the registry records diff --git a/utilities/cli/src/input.rs b/utilities/cli/src/input.rs index 1a43b16a..dbc3ec18 100644 --- a/utilities/cli/src/input.rs +++ b/utilities/cli/src/input.rs @@ -15,6 +15,9 @@ use crate::schedule::Schedule; /// process resolved is the one that builds these artifacts, not whether some Rust project is nearby. const REPOSITORY_URL: &str = et_org::REPOSITORY_URL; +/// The name a cluster takes when its input does not choose one. +pub const DEFAULT_CLUSTER_NAME: &str = "default"; + /// The deployment format a scenario is rendered into. /// /// An enum rather than the string it is written as, because the set is closed and every consumer switches on it: as @@ -196,9 +199,6 @@ pub struct Machine { pub agents: Vec, } -/// The name a cluster takes when its input does not choose one. -pub const DEFAULT_CLUSTER_NAME: &str = "default"; - impl Default for ClusterInput { /// The smallest cluster that is still valid: named, no agents, rendered the way an unstated input is. /// diff --git a/utilities/cli/src/lib.rs b/utilities/cli/src/lib.rs index 83ac490e..12dc601c 100644 --- a/utilities/cli/src/lib.rs +++ b/utilities/cli/src/lib.rs @@ -47,6 +47,154 @@ pub use self::module_package_json::generate_module_package_json; pub(crate) use self::scenario_password::{scenario_password, scenario_seed}; pub use self::schedule::Schedule; +/// Where the hub serves pyodide from, whichever distribution was selected. +const PYODIDE_DOCKER_PATH: &str = "/app/node_modules/pyodide"; + +/// Name of the generated env file that carries the scenario's derived credential. +/// +/// The password is derived from the scenario input so a deployment is reproducible from it, which used to mean writing +/// the literal into `mise.toml` and `compose.yaml` -- both committed under `verification/`, where every secret scanner +/// duly found it. Collecting it into one file keeps the deployment reproducible while leaving the rest of the generated +/// output free of anything a scanner reads as a credential. +/// +/// Whether that one file is committed depends on where it was generated, and [`write_deployment_gitignore`] decides: +/// under `verification/` it is a fixture the drift check reads, and anywhere else it is a real credential that the +/// ignore file written beside it lists. +pub(crate) const SECRETS_ENV_FILE: &str = "secrets.env"; + +/// Account the collector is created with, and the one the hub authenticates its OTLP exports as. +/// +/// One constant because the two are the same account seen from either end: the collector is created with it as +/// `ZO_ROOT_USER_EMAIL` and the hub presents it as `OTLP_AUTH_USERNAME`, so a deployment where they disagree comes up +/// healthy and then rejects every export. Written in two files before this existed, with nothing checking that they +/// matched. +pub const COLLECTOR_USERNAME: &str = "root@example.com"; + +/// The collector's non-secret settings, which every generated deployment carries rather than sourcing. +/// +/// Three formats render this -- a `ConfigMap`, a compose `environment:` block, a `docker run` flag list -- so it is +/// one list and a setting cannot reach some deployments and not others. Reading it from a file in this repository +/// instead is what made a generated deployment unable to leave the tree, for the sake of two values neither secret nor +/// scenario-specific. +/// +/// Two things are deliberately absent. The root password is derived per scenario and reaches each format from the +/// generated env file. `ZO_DATA_DIR` is per format rather than shared, because it only means anything alongside the +/// storage that format declares -- a named volume, a claim, or nothing at all. +pub(crate) const COLLECTOR_SETTINGS: [(&str, &str); 2] = + [("RUST_LOG", "warn"), ("ZO_ROOT_USER_EMAIL", COLLECTOR_USERNAME)]; + +/// Registry path the repository's own images are published under. +/// +/// A runner image is the same for every deployment -- nothing in one varies by scenario -- so a scenario that asks for +/// published images names it here and the cluster pulls it, leaving the node with nothing to build or import. The hub +/// image is published alongside them but no manifest names it: it is the base a scenario image is layered onto, so it +/// reaches a deployment as a build context rather than as something a pod runs. +pub(crate) const IMAGE_REGISTRY: &str = et_org::IMAGE_REGISTRY; + +/// Longest `cluster_name` that still leaves a valid Kubernetes namespace after the `et-` prefix. +/// +/// A namespace is an RFC 1123 label, which the API server caps at 63 characters. +const CLUSTER_NAME_MAX: usize = 60; + +/// Longest runner name, which names a Kubernetes `Deployment` as it is: an RFC 1123 label's 63 characters. +const RUNNER_NAME_MAX: usize = 63; + +/// Directories of this repository whose every child holding a module is registered, relative to its root. +const REPO_MODULE_PARENTS: [&str; 2] = ["services/ws-modules", "data/model-modules"]; + +/// Single module directories of this repository, relative to its root, each registered as it is. +const REPO_MODULE_DIRS: [&str; 6] = [ + // Generated Python ws-modules: each generated/python-{ws,rest}/ holds its own pkg/package.json after `mise run + // build-et-{ws,rest-client}- wheel`. They're listed individually because the parent `generated/` also contains non- + // module artifacts (rust-rest, dart-ws, zig-rest, specs, docs). + "generated/python-ws", + "generated/python-rest", + // The Python libraries, likewise listed one by one because `libs/` otherwise holds Rust crates. + "libs/python-org", + "libs/python-edge-toolkit", + // The two the hub serves whatever the scenario asks for: its own page, and the agent that page loads. Registered + // like any other module rather than prepended as bare paths by each generator, so the dependencies they declare are + // resolved too. `static` names the runtimes its page pulls at boot, and a deployment that omits them serves a page + // whose first import 404s. + "services/ws-server/static", + "services/ws-wasm-agent", +]; + +/// Registry scope this project's own module packages carry, and which the hub drops when naming a module. +pub(crate) const MODULE_SCOPE: &str = et_org::NPM_SCOPE; + +/// Environment a runner gets from the deployment rather than from the scenario, and so cannot be overridden. +pub(crate) const DERIVED_RUNNER_ENV: [&str; 2] = ["RUNNER_MODULE", "WS_SERVER_URL"]; + +/// Runner kinds a scenario may name, mapped to the crate that runs them. +/// +/// All three share one deployment shape, which is what lets one generator serve them: each takes the module's +/// published name in `RUNNER_MODULE`, fetches it from the hub named by `WS_SERVER_URL`, and builds from +/// `services/ws--runner/Dockerfile`. Nothing else distinguishes a runner here, so a fourth is this line plus an +/// image. Rejecting a kind by name is what stops a scenario from asking for one and silently getting nothing. +pub(crate) const SUPPORTED_RUNNERS: [(&str, &str); 3] = [ + ("pyo3", "et-ws-pyo3-runner"), + ("wasi", "et-ws-wasi-runner"), + ("web", "et-ws-web-runner"), +]; + +/// Names the generated deployment already uses for its own tasks, services and aliases. +/// +/// A runner is named after the agent that declares it, and both generators key on that name: mise inserts each task +/// into a table and compose writes each service as a mapping key. Either way a collision replaces rather than reports +/// -- an agent called `ws-server` would quietly take the hub's place, and the deployment would come up missing the +/// thing it was meant to talk to. Rejecting the name is the only way that surfaces. +pub(crate) const RESERVED_RUNNER_NAMES: [&str; 7] = [ + "generated-scenario", + "o2", + deployment_types::OPENER_TASK, + deployment_types::COLLECTOR_TASK, + deployment_types::HUB_TASK, + deployment_types::HUB_IMAGE_BUILD, + deployment_types::SCHEDULER_SERVICE, +]; + +/// What the README says about committing the lockfile, whichever way the deployment's artifacts come. +/// +/// A macro rather than a `const`, because `concat!` takes only literals and each section is one literal. +macro_rules! mise_lock_commit_note { + () => { + concat!( + "Commit `mise.lock` alongside the deployment, together with the `.mise/locks/` directory when\n", + "mise writes one -- it holds the dependency graph of each npm tool, which `mise.lock` refers to.\n", + "The two record the version and checksum every `latest` in `mise.toml` resolved to, for each of\n", + "mise's common platforms, so every later install -- on any machine -- fetches the same binaries\n", + "rather than whatever is newest that day. Move to newer releases deliberately, with\n", + "`mise lock --bump`, and commit the result.\n\n" + ) + }; +} + +/// The command that starts a process-compose deployment, naming its file outright. +/// +/// process-compose looks for `compose.yaml` among its default names and takes it first, so in a directory that also +/// holds the Docker Compose file it would load that one instead. Macros rather than `const`s here and below, because +/// `concat!` takes only literals and each README section is one literal. +macro_rules! process_compose_up { + () => { + "process-compose up --no-server -f process-compose.yaml" + }; +} + +/// What the process-compose README section says after its commands, whichever way the deployment's artifacts come. +macro_rules! process_compose_run_notes { + () => { + concat!( + "Each process runs one task of `mise.toml`, in order: `ws-server` waits for OpenObserve to report\n", + "healthy, and every runner waits for `ws-server`. A scheduled agent's runner is started on each\n", + "tick of its schedule and runs its module once; a tick that finds the previous run still going is\n", + "skipped. `--no-server` keeps process-compose's own API off port 8080, where `ws-server` listens.\n", + "Quitting the TUI stops every process. Open the OpenObserve UI, once it is up, by starting the\n", + "`open-o2` process from the TUI.\n" + ) + }; +} + /// The files a deployment of `output_types` writes for `cluster`, in the order the README lists them. fn generated_output_files(cluster: &ClusterInput, output_types: &[OutputType]) -> Vec<&'static str> { let mut files = Vec::new(); @@ -184,50 +332,6 @@ pub(crate) fn module_path_contexts(module_paths: &[PathBuf], base: &Path) -> Vec .collect() } -/// Where the hub serves pyodide from, whichever distribution was selected. -const PYODIDE_DOCKER_PATH: &str = "/app/node_modules/pyodide"; - -/// Name of the generated env file that carries the scenario's derived credential. -/// -/// The password is derived from the scenario input so a deployment is reproducible from it, which used to mean writing -/// the literal into `mise.toml` and `compose.yaml` -- both committed under `verification/`, where every secret scanner -/// duly found it. Collecting it into one file keeps the deployment reproducible while leaving the rest of the generated -/// output free of anything a scanner reads as a credential. -/// -/// Whether that one file is committed depends on where it was generated, and [`write_deployment_gitignore`] decides: -/// under `verification/` it is a fixture the drift check reads, and anywhere else it is a real credential that the -/// ignore file written beside it lists. -pub(crate) const SECRETS_ENV_FILE: &str = "secrets.env"; - -/// Account the collector is created with, and the one the hub authenticates its OTLP exports as. -/// -/// One constant because the two are the same account seen from either end: the collector is created with it as -/// `ZO_ROOT_USER_EMAIL` and the hub presents it as `OTLP_AUTH_USERNAME`, so a deployment where they disagree comes up -/// healthy and then rejects every export. Written in two files before this existed, with nothing checking that they -/// matched. -pub const COLLECTOR_USERNAME: &str = "root@example.com"; - -/// The collector's non-secret settings, which every generated deployment carries rather than sourcing. -/// -/// Three formats render this -- a `ConfigMap`, a compose `environment:` block, a `docker run` flag list -- so it is -/// one list and a setting cannot reach some deployments and not others. Reading it from a file in this repository -/// instead is what made a generated deployment unable to leave the tree, for the sake of two values neither secret nor -/// scenario-specific. -/// -/// Two things are deliberately absent. The root password is derived per scenario and reaches each format from the -/// generated env file. `ZO_DATA_DIR` is per format rather than shared, because it only means anything alongside the -/// storage that format declares -- a named volume, a claim, or nothing at all. -pub(crate) const COLLECTOR_SETTINGS: [(&str, &str); 2] = - [("RUST_LOG", "warn"), ("ZO_ROOT_USER_EMAIL", COLLECTOR_USERNAME)]; - -/// Registry path the repository's own images are published under. -/// -/// A runner image is the same for every deployment -- nothing in one varies by scenario -- so a scenario that asks for -/// published images names it here and the cluster pulls it, leaving the node with nothing to build or import. The hub -/// image is published alongside them but no manifest names it: it is the base a scenario image is layered onto, so it -/// reaches a deployment as a build context rather than as something a pod runs. -pub(crate) const IMAGE_REGISTRY: &str = et_org::IMAGE_REGISTRY; - /// Prefix that turns a bare image name into the one a scenario's `artifact_source` asks for. /// /// A prefix rather than two parallel name builders, because that is the whole of the difference: the tags are @@ -319,14 +423,6 @@ pub(crate) fn load_cluster_input(input_file: &Path) -> Result<(ClusterInput, u64 Ok((cluster, scenario_seed(&content))) } -/// Longest `cluster_name` that still leaves a valid Kubernetes namespace after the `et-` prefix. -/// -/// A namespace is an RFC 1123 label, which the API server caps at 63 characters. -const CLUSTER_NAME_MAX: usize = 60; - -/// Longest runner name, which names a Kubernetes `Deployment` as it is: an RFC 1123 label's 63 characters. -const RUNNER_NAME_MAX: usize = 63; - /// Reject a `cluster_name` that is not an RFC 1123 label. /// /// The name reaches three renderers that each read it as trusted text: it is interpolated into generated comments, @@ -842,22 +938,6 @@ where kinds } -/// What the README says about committing the lockfile, whichever way the deployment's artifacts come. -/// -/// A macro rather than a `const`, because `concat!` takes only literals and each section is one literal. -macro_rules! mise_lock_commit_note { - () => { - concat!( - "Commit `mise.lock` alongside the deployment, together with the `.mise/locks/` directory when\n", - "mise writes one -- it holds the dependency graph of each npm tool, which `mise.lock` refers to.\n", - "The two record the version and checksum every `latest` in `mise.toml` resolved to, for each of\n", - "mise's common platforms, so every later install -- on any machine -- fetches the same binaries\n", - "rather than whatever is newest that day. Move to newer releases deliberately, with\n", - "`mise lock --bump`, and commit the result.\n\n" - ) - }; -} - /// Render the step that locks the tools `mise.toml` names, and for a published deployment installs them. /// /// Every deployment that runs the tasks declares tools there -- at least the UI opener, and a published one its @@ -900,31 +980,6 @@ const fn mise_install_note(artifacts: ArtifactSource) -> &'static str { ) } -/// The command that starts a process-compose deployment, naming its file outright. -/// -/// process-compose looks for `compose.yaml` among its default names and takes it first, so in a directory that also -/// holds the Docker Compose file it would load that one instead. Macros rather than `const`s here and below, because -/// `concat!` takes only literals and each README section is one literal. -macro_rules! process_compose_up { - () => { - "process-compose up --no-server -f process-compose.yaml" - }; -} - -/// What the process-compose README section says after its commands, whichever way the deployment's artifacts come. -macro_rules! process_compose_run_notes { - () => { - concat!( - "Each process runs one task of `mise.toml`, in order: `ws-server` waits for OpenObserve to report\n", - "healthy, and every runner waits for `ws-server`. A scheduled agent's runner is started on each\n", - "tick of its schedule and runs its module once; a tick that finds the previous run still going is\n", - "skipped. `--no-server` keeps process-compose's own API off port 8080, where `ws-server` listens.\n", - "Quitting the TUI stops every process. Open the OpenObserve UI, once it is up, by starting the\n", - "`open-o2` process from the TUI.\n" - ) - }; -} - /// Render the README section that runs the scenario with process-compose. /// /// A local deployment installs process-compose here, since the README's own install step is only written for a @@ -1411,27 +1466,6 @@ fn k3s_apply_instructions(cluster_name: &str) -> String { ) } -/// Directories of this repository whose every child holding a module is registered, relative to its root. -const REPO_MODULE_PARENTS: [&str; 2] = ["services/ws-modules", "data/model-modules"]; - -/// Single module directories of this repository, relative to its root, each registered as it is. -const REPO_MODULE_DIRS: [&str; 6] = [ - // Generated Python ws-modules: each generated/python-{ws,rest}/ holds its own pkg/package.json after `mise run - // build-et-{ws,rest-client}- wheel`. They're listed individually because the parent `generated/` also contains non- - // module artifacts (rust-rest, dart-ws, zig-rest, specs, docs). - "generated/python-ws", - "generated/python-rest", - // The Python libraries, likewise listed one by one because `libs/` otherwise holds Rust crates. - "libs/python-org", - "libs/python-edge-toolkit", - // The two the hub serves whatever the scenario asks for: its own page, and the agent that page loads. Registered - // like any other module rather than prepended as bare paths by each generator, so the dependencies they declare are - // resolved too. `static` names the runtimes its page pulls at boot, and a deployment that omits them serves a page - // whose first import 404s. - "services/ws-server/static", - "services/ws-wasm-agent", -]; - #[must_use] pub(crate) fn module_registry( project_root: &Path, @@ -1603,9 +1637,6 @@ fn register_module_at( register_module(registry, module_path, directory_name, ws_server_dir, docker_path); } -/// Registry scope this project's own module packages carry, and which the hub drops when naming a module. -pub(crate) const MODULE_SCOPE: &str = et_org::NPM_SCOPE; - fn register_module( registry: &mut BTreeMap, module_path: &Path, @@ -1880,9 +1911,6 @@ pub fn npm_module_path(package: &str) -> Result { .ok_or_else(|| CliError::UnresolvedNpmModule(package.to_string())) } -/// Environment a runner gets from the deployment rather than from the scenario, and so cannot be overridden. -pub(crate) const DERIVED_RUNNER_ENV: [&str; 2] = ["RUNNER_MODULE", "WS_SERVER_URL"]; - /// One runner process the generated deployment starts, resolved from an agent that names a `runner:`. #[derive(Clone, Debug)] #[non_exhaustive] @@ -1903,34 +1931,6 @@ pub(crate) struct RunnerInstance { pub schedule: Schedule, } -/// Runner kinds a scenario may name, mapped to the crate that runs them. -/// -/// All three share one deployment shape, which is what lets one generator serve them: each takes the module's -/// published name in `RUNNER_MODULE`, fetches it from the hub named by `WS_SERVER_URL`, and builds from -/// `services/ws--runner/Dockerfile`. Nothing else distinguishes a runner here, so a fourth is this line plus an -/// image. Rejecting a kind by name is what stops a scenario from asking for one and silently getting nothing. -pub(crate) const SUPPORTED_RUNNERS: [(&str, &str); 3] = [ - ("pyo3", "et-ws-pyo3-runner"), - ("wasi", "et-ws-wasi-runner"), - ("web", "et-ws-web-runner"), -]; - -/// Names the generated deployment already uses for its own tasks, services and aliases. -/// -/// A runner is named after the agent that declares it, and both generators key on that name: mise inserts each task -/// into a table and compose writes each service as a mapping key. Either way a collision replaces rather than reports -/// -- an agent called `ws-server` would quietly take the hub's place, and the deployment would come up missing the -/// thing it was meant to talk to. Rejecting the name is the only way that surfaces. -pub(crate) const RESERVED_RUNNER_NAMES: [&str; 7] = [ - "generated-scenario", - "o2", - deployment_types::OPENER_TASK, - deployment_types::COLLECTOR_TASK, - deployment_types::HUB_TASK, - deployment_types::HUB_IMAGE_BUILD, - deployment_types::SCHEDULER_SERVICE, -]; - /// Resolve every agent that names a `runner:` into the processes the deployment has to start. /// /// One process per resource rather than per agent, because a runner hosts exactly one module -- `RUNNER_MODULE` is a diff --git a/utilities/cli/src/module_package_json/generate.rs b/utilities/cli/src/module_package_json/generate.rs index ed816aef..cdf1caf0 100644 --- a/utilities/cli/src/module_package_json/generate.rs +++ b/utilities/cli/src/module_package_json/generate.rs @@ -13,6 +13,14 @@ use serde_json::{Map, Value, json}; use crate::MODULE_SCOPE; use crate::error::{CliError, parse_json, parse_toml, serialize_json_pretty}; +/// Prefixes that mark a dependency as a module of this project's own, and so as one to scope. +/// +/// The crate prefix, or the organisation name itself, as `edge-toolkit` is named like its Rust crate. +const OWN_MODULE_PREFIXES: [&str; 2] = [et_org::CRATE_PREFIX, et_org::ORG]; + +/// The runtime dependency names that identify a WASI component guest. +const WASI_GUEST_MARKERS: [&str; 2] = ["et-wasi-guest", "wit-bindgen"]; + /// The name a module publishes under, given the name its crate or project declares. /// /// Every module carries the owner scope, because a registry that would accept an unscoped one does not exist @@ -28,11 +36,6 @@ fn scoped_npm_package_name(declared: &str) -> String { format!("{MODULE_SCOPE}{declared}") } -/// Prefixes that mark a dependency as a module of this project's own, and so as one to scope. -/// -/// The crate prefix, or the organisation name itself, as `edge-toolkit` is named like its Rust crate. -const OWN_MODULE_PREFIXES: [&str; 2] = [et_org::CRATE_PREFIX, et_org::ORG]; - /// The name a declared dependency is written as in the published manifest. /// /// A module's `dependencies` name the other modules the hub has to serve alongside it, and a deployment @@ -349,9 +352,6 @@ fn detect_python_kind(module_dir: &Path) -> ModuleKind { } } -/// The runtime dependency names that identify a WASI component guest. -const WASI_GUEST_MARKERS: [&str; 2] = ["et-wasi-guest", "wit-bindgen"]; - /// WASI Rust modules reach the component bindings one of two ways; wasm-pack browser modules do neither. /// /// `et-wasi-guest` generates the bindings once and shares them, so a guest that depends on it names no diff --git a/utilities/cli/tests/limits.rs b/utilities/cli/tests/limits.rs index e59c8341..bbc623c8 100644 --- a/utilities/cli/tests/limits.rs +++ b/utilities/cli/tests/limits.rs @@ -16,6 +16,23 @@ use k8s_openapi::api::core::v1::{ConfigMap, PodSpec}; use serde::Deserialize as _; use tempfile::{TempDir, tempdir}; +/// Every limit that can be set together: the quota cap and the share weight rather than their alternatives. +const EVERY_LIMIT: &str = "blkio_weight: 300 +cpu_period: 50000 +cpu_quota: 75000 +cpu_shares: 512 +cpuset: \"0-3\" +memory: \"1.5 GiB\" +memory_reservation: \"256 MiB\" +memswap_limit: \"2 GiB\" +pids: 256 +shm_size: \"128 MiB\" +ulimits: + core: 0 + nofile: + soft: 1024 + hard: 4096"; + /// The math1 pair, with `{twin}` spliced in as the twin's `limits:` block and `{rest}` appended at the top level. fn scenario(twin: &str, rest: &str) -> String { format!( @@ -46,23 +63,6 @@ fn twin_limits(fields: &str) -> String { }) } -/// Every limit that can be set together: the quota cap and the share weight rather than their alternatives. -const EVERY_LIMIT: &str = "blkio_weight: 300 -cpu_period: 50000 -cpu_quota: 75000 -cpu_shares: 512 -cpuset: \"0-3\" -memory: \"1.5 GiB\" -memory_reservation: \"256 MiB\" -memswap_limit: \"2 GiB\" -pids: 256 -shm_size: \"128 MiB\" -ulimits: - core: 0 - nofile: - soft: 1024 - hard: 4096"; - /// Every format generated from `input` into a fresh `verification/` tree, with the root kept alive. fn generated(input: &str) -> (TempDir, PathBuf) { let test_root = tempdir().unwrap(); diff --git a/utilities/cli/tests/scenario_generation.rs b/utilities/cli/tests/scenario_generation.rs index a3c15149..7ed78a06 100644 --- a/utilities/cli/tests/scenario_generation.rs +++ b/utilities/cli/tests/scenario_generation.rs @@ -8,6 +8,21 @@ use fs_err as fs; use serde::Deserialize as _; use tempfile::tempdir; +/// The one input line that switches a scenario from building what it runs to addressing the releases. +const PUBLISHED: &str = "artifact_source: \"published\"\n"; + +/// A scenario whose agent declares extra runner environment, which every format has to carry. +const WITH_RUNNER_ENV: &str = r#"cluster_name: "runner-env" +agents: + - name: "math1-twin" + runner: "wasi" + env: + OTLP_COLLECTOR_URL: "http://host:5080/api/default/v1" + RUST_LOG: debug + resources: + - type: "wasi-math1" +"#; + /// Lay out a `verification/` tree holding one scenario input, and return its root and the output directory. /// /// Both regeneration tests below need the same four paths in the same shape and differ only in the document they write, @@ -460,10 +475,7 @@ deployment_type: "k3s" /// Generate the k3s scenario with `extra` spliced in and return the two files a rendering is judged by. /// -/// The temp root is dropped here rather than handed back, because both files are read before it goes. The one input -/// line that switches a scenario from building what it runs to addressing the releases. -const PUBLISHED: &str = "artifact_source: \"published\"\n"; - +/// The temp root is dropped here rather than handed back, because both files are read before it goes. fn k3s_manifest_and_readme(extra: &str) -> (String, String) { let (_test_root, output_dir) = k3s_scenario_with(extra); let manifest = fs::read_to_string(output_dir.join("k3s.yaml")).unwrap(); @@ -512,18 +524,6 @@ fn the_artifact_source_decides_whether_runner_images_are_pulled() { ); } -/// A scenario whose agent declares extra runner environment, which every format has to carry. -const WITH_RUNNER_ENV: &str = r#"cluster_name: "runner-env" -agents: - - name: "math1-twin" - runner: "wasi" - env: - OTLP_COLLECTOR_URL: "http://host:5080/api/default/v1" - RUST_LOG: debug - resources: - - type: "wasi-math1" -"#; - #[test] fn an_agents_declared_env_reaches_every_deployment_format() { // One declaration in the input, three renderings. A format that dropped it would leave a runner configured in two diff --git a/utilities/cli/tests/schedule.rs b/utilities/cli/tests/schedule.rs index 9930508c..cec48687 100644 --- a/utilities/cli/tests/schedule.rs +++ b/utilities/cli/tests/schedule.rs @@ -13,6 +13,16 @@ use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; use serde::Deserialize; use tempfile::{TempDir, tempdir}; +/// Two machines, with the scheduled trigger on `edge` and the twin on the server. +const MACHINES: &str = r#"machines: + - name: "server" + address: "10.0.0.1" + agents: ["math1-twin"] + - name: "edge" + address: "10.0.0.2" + agents: ["math1-trigger"] +"#; + /// The math1 pair, with `{schedule}` spliced into the trigger. fn scenario(schedule: &str) -> String { format!( @@ -35,16 +45,6 @@ fn every(cron: &str, time_zone: &str) -> String { format!(" schedule:\n cron: \"{cron}\"\n time_zone: \"{time_zone}\"\n") } -/// Two machines, with the scheduled trigger on `edge` and the twin on the server. -const MACHINES: &str = r#"machines: - - name: "server" - address: "10.0.0.1" - agents: ["math1-twin"] - - name: "edge" - address: "10.0.0.2" - agents: ["math1-trigger"] -"#; - /// What generating `input` as `output_type` into a fresh directory returns, and that directory. fn generate(input: &str, output_type: OutputType) -> (TempDir, PathBuf, String) { let test_root = tempdir().unwrap(); diff --git a/utilities/int-gen/src/checks.rs b/utilities/int-gen/src/checks.rs index b4db31f5..c58ce169 100644 --- a/utilities/int-gen/src/checks.rs +++ b/utilities/int-gen/src/checks.rs @@ -86,6 +86,20 @@ const IMPLICIT_CONFIGS: &[(&str, &[&str])] = &[ ("taplo", &["config/taplo.toml"]), ]; +/// The lint groups, which name a set of lints and have no entry of their own in the lint index. +const CLIPPY_GROUPS: &[&str] = &[ + "all", + "cargo", + "complexity", + "correctness", + "nursery", + "pedantic", + "perf", + "restriction", + "style", + "suspicious", +]; + /// For each check, the aggregates that list it in `depends`. type Parents = BTreeMap>; @@ -1099,20 +1113,6 @@ pub fn link_label(name: &str, link: &str) -> String { words.join("_") } -/// The lint groups, which name a set of lints and have no entry of their own in the lint index. -const CLIPPY_GROUPS: &[&str] = &[ - "all", - "cargo", - "complexity", - "correctness", - "nursery", - "pedantic", - "perf", - "restriction", - "style", - "suspicious", -]; - /// The documentation page for one lint of a `[workspace.lints.]` table, or empty when it has none. /// /// A clippy lint links to its entry in the stable lint index, and a clippy group to nothing, since groups have no diff --git a/utilities/int-gen/src/wit/messages.rs b/utilities/int-gen/src/wit/messages.rs index 4afc38e8..14c8bf8b 100644 --- a/utilities/int-gen/src/wit/messages.rs +++ b/utilities/int-gen/src/wit/messages.rs @@ -24,6 +24,9 @@ use wit_encoder::{EnumCase, Field, Ident, Interface, Package, PackageName, Type, use crate::Error; use crate::asyncapi::{WS_DESCRIPTION, WS_VERSION}; +/// The WIT type a `string` field with the schema `format: ip` (a Rust `IpAddr`) maps to. +const IP_ADDRESS: &str = "ip-address"; + /// Convert a wire identifier (`WsConnectAck`, `agent_id`, `et-connect-ack`, ...) to canonical WIT kebab-case. /// /// The results are `ws-connect-ack`, `agent-id`, `connect-ack`. The `et-` prefix is dropped because the @@ -34,9 +37,6 @@ fn to_kebab(input: &str) -> String { type EnumSet = HashSet; -/// The WIT type a `string` field with the schema `format: ip` (a Rust `IpAddr`) maps to. -const IP_ADDRESS: &str = "ip-address"; - /// Define [`IP_ADDRESS`] in the shape `wasi:sockets/network` gives its own: four octets or eight hextets. /// /// Declared here rather than imported, so the package needs no `wasi:sockets` dependency for one type. diff --git a/utilities/int-gen/src/wit/upstream.rs b/utilities/int-gen/src/wit/upstream.rs index bfae15e9..25e9ede1 100644 --- a/utilities/int-gen/src/wit/upstream.rs +++ b/utilities/int-gen/src/wit/upstream.rs @@ -14,23 +14,6 @@ use fs_err as fs; use crate::Error; -/// One file within an upstream package. -struct File { - name: &'static str, -} - -/// An upstream WASI WIT package pinned to a tag or commit SHA. -struct UpstreamPackage { - /// Directory name under `generated/specs/wit/deps/`. - local_dir: &'static str, - /// `WebAssembly/` (always under the WebAssembly org). - repo: &'static str, - /// Tag (e.g. `v0.2.6`) or commit SHA. - /// wasi-logging has no releases, so it pins by SHA; everything else by release tag. - git_ref: &'static str, - files: &'static [File], -} - const PACKAGES: &[UpstreamPackage] = &[ UpstreamPackage { local_dir: "wasi-clocks", @@ -93,6 +76,23 @@ const PACKAGES: &[UpstreamPackage] = &[ }, ]; +/// One file within an upstream package. +struct File { + name: &'static str, +} + +/// An upstream WASI WIT package pinned to a tag or commit SHA. +struct UpstreamPackage { + /// Directory name under `generated/specs/wit/deps/`. + local_dir: &'static str, + /// `WebAssembly/` (always under the WebAssembly org). + repo: &'static str, + /// Tag (e.g. `v0.2.6`) or commit SHA. + /// wasi-logging has no releases, so it pins by SHA; everything else by release tag. + git_ref: &'static str, + files: &'static [File], +} + /// Refresh every upstream WIT package and write them all under `generated/specs/wit/deps//`. /// Triggered by `mise run fetch-wit-deps`. pub fn run(project_root: &Path) -> Result<(), Error> { diff --git a/utilities/wasm-cov-wrapper/src/main.rs b/utilities/wasm-cov-wrapper/src/main.rs index 089b07e7..5f252305 100644 --- a/utilities/wasm-cov-wrapper/src/main.rs +++ b/utilities/wasm-cov-wrapper/src/main.rs @@ -2,17 +2,17 @@ //! //! cargo runs this as `int-wasm-cov-wrapper ` for **workspace** crates only (never registry //! dependencies), so only our own crates are instrumented -- dependencies get neither coverage nor a broken -//! `-Cinstrument-coverage` link (a dependency cdylib such as tracing-wasm or wasm-streams would otherwise want an -//! unresolved `__llvm_profile_runtime`). Real crate compiles (those carrying `--crate-name`) get the wasm coverage -//! flags appended; cargo's `-vV` / `--print` probes pass through untouched. minicov (pulled by `et-web/coverage`) -//! supplies the profiler runtime for the instrumented crates, and the `--emit=llvm-ir` output feeds the wasm-cov -//! task that turns each module's coverage map into lcov. +//! `-Cinstrument-coverage` link (a dependency cdylib such as wasm-streams would otherwise want an unresolved +//! `__llvm_profile_runtime`). Real crate compiles (those carrying `--crate-name`) get the wasm coverage flags appended; +//! cargo's `-vV` / `--print` probes pass through untouched. minicov (pulled by `et-web/coverage`) supplies the profiler +//! runtime for the instrumented crates, and the `--emit=llvm-ir` output feeds the wasm-cov task that turns each +//! module's coverage map into lcov. //! //! Excluded from Codacy analysis (`.codacy.yaml`): Codacy's Rust security rule flags `args_os()` flowing into a -//! subprocess spawn as a command-injection shape and cannot suppress it per line. That is a false positive here -- -//! the args are cargo's own trusted rustc invocation, and forwarding argv to rustc is this file's entire purpose, -//! so no code change removes it. The file is kept minimal and single-purpose so the path exclude is as narrow as -//! possible; every other analyzer the repo runs over Rust still reads it. +//! subprocess spawn as a command-injection shape and cannot suppress it per line. That is a false positive here -- the +//! args are cargo's own trusted rustc invocation, and forwarding argv to rustc is this file's entire purpose, so no +//! code change removes it. The file is kept minimal and single-purpose so the path exclude is as narrow as possible; +//! every other analyzer the repo runs over Rust still reads it. #![expect( clippy::print_stderr, reason = "a build-tool wrapper reports its own startup failure to stderr" @@ -31,10 +31,10 @@ fn main() -> ExitCode { }; let mut rustc_args: Vec = args.collect(); - // Instrument only real crate compiles for the wasm target. `--crate-name` excludes cargo's `rustc -vV` / - // `--print` probes; the `wasm32` target gate excludes host build scripts + proc-macros -- those compile for - // the host, and instrumenting them just litters `.profraw` files in the module dir when they run at build - // time (their coverage is irrelevant to the browser module anyway). + // Instrument only real crate compiles for the wasm target. `--crate-name` excludes cargo's `rustc -vV` / `--print` + // probes; the `wasm32` target gate excludes host build scripts + proc-macros -- those compile for the host, and + // instrumenting them just litters `.profraw` files in the module dir when they run at build time (their coverage is + // irrelevant to the browser module anyway). if is_wasm_crate_compile(&rustc_args) { rustc_args.extend( [ @@ -62,9 +62,9 @@ fn main() -> ExitCode { /// True when these rustc args are a real crate compile (`--crate-name`) targeting wasm32. /// -/// Both gates matter: `--crate-name` skips cargo's `-vV`/`--print` probes, and the wasm32 target skips host -/// build scripts + proc-macros (whose instrumented binaries would drop stray `.profraw` files at build time). -/// Handles both `--target wasm32-...` (two args) and `--target=wasm32-...` (one arg) spellings. +/// Both gates matter: `--crate-name` skips cargo's `-vV`/`--print` probes, and the wasm32 target skips host build +/// scripts + proc-macros (whose instrumented binaries would drop stray `.profraw` files at build time). Handles both +/// `--target wasm32-...` (two args) and `--target=wasm32-...` (one arg) spellings. #[expect( clippy::single_call_fn, reason = "the instrument-or-pass-through decision, named so main reads as that one branch" diff --git a/uv.lock b/uv.lock index 7fb664f0..b810ae0f 100644 --- a/uv.lock +++ b/uv.lock @@ -51,11 +51,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.6.17" +version = "2026.10.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, + { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" }, ] [[package]]