name: WAI conformance # WAI's load-bearing claim is bit-exact reconstruction on every machine, # no tolerance parameter. This workflow enforces it mechanically: every # cargo-class corpus — and the integer-exact neural decoders, which make the # same promise (the registry's NeuralIntegerExact class: i64-only, no float # ONNX) — is re-derived on every OS/arch leg of the matrix below (x86-64 and # arm64) and must reproduce its frozen BLAKE3 hashes; and the integer # byte-equality goldens (rANS / int_synth / JPEG-AI mANS, generated once on # a reference machine) must decode identically everywhere. All of it is pure # Rust (no C codec chain, no ONNX Runtime), so the matrix needs no system # dependencies. on: push: branches: [main] paths: - 'wai/wai-rs/**' # wai-rs path-depends on wai-quantum, and the quantum corpora below are # verified through it: a change there changes what this workflow checks. - 'wai/wai-quantum/**' - 'wai/*-conformance/**' - 'wai/byte-exact-conformance/**' # The registry export and the two library tests that read files outside # wai-rs (the codec glossary, SPEC.md) must fire this workflow too: a # hand edit of an exported file, of SPEC §5 or of the glossary is checked # here. vocab/v1's and v2's pin terms are checked by a library test. - 'wai/registry/**' - 'wai/vocab/**' - 'wai/glossary/**' - 'wai/SPEC.md' - 'wai/REGISTRY.md' - 'wai/wai-web/**' - '.github/workflows/wai-conformance.yml' pull_request: paths: - 'wai/wai-rs/**' # wai-rs path-depends on wai-quantum, and the quantum corpora below are # verified through it: a change there changes what this workflow checks. - 'wai/wai-quantum/**' - 'wai/*-conformance/**' - 'wai/byte-exact-conformance/**' # The registry export and the two library tests that read files outside # wai-rs (the codec glossary, SPEC.md) must fire this workflow too: a # hand edit of an exported file, of SPEC §5 or of the glossary is checked # here. vocab/v1's and v2's pin terms are checked by a library test. - 'wai/registry/**' - 'wai/vocab/**' - 'wai/glossary/**' - 'wai/SPEC.md' - 'wai/REGISTRY.md' - 'wai/wai-web/**' - '.github/workflows/wai-conformance.yml' # No global `shell:` default. The Windows runner is a host executor whose # `bash` is the WSL stub (a different OS), so shell-specific steps below pick # their shell per-OS: bash on unix, pwsh on Windows. Bare `cargo …` steps take # each runner's native default, which runs cargo identically either way. The # Windows legs are out of the matrices for now (see the comment on each matrix); # their pwsh steps stay, inactive, so restoring a leg is a one-line change. jobs: cargo-classes: name: cargo-class corpora · ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: # Self-hosted fleet labels. Both architectures: x86_64 by linux, arm64 # by linux-arm64 + macos — what matters for the Fx integer floor is the # arch, not the OS. No Intel-Mac runner exists in the fleet, and it # would add no arch coverage anyway. # Windows x86_64 leg removed 2026-09-30: runner offline. Restore by # re-adding `windows` here once the runner is back online. os: [linux, linux-arm64, macos] steps: - uses: actions/checkout@v5 # unix: the docker runners (linux / linux-arm64) ship no Rust, so rustup # is bootstrapped; the macOS `host` runner has it but launchd's PATH omits # ~/.cargo/bin. dtolnay/rust-toolchain is unusable — it calls a `rustup` # the docker image lacks. - name: Install Rust 1.98.0 (unix) if: matrix.os != 'windows' shell: bash run: | export PATH="$HOME/.cargo/bin:$PATH" command -v rustup >/dev/null 2>&1 || \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 rustup toolchain install 1.98.0 --profile minimal rustup default 1.98.0 rustup component add --toolchain 1.98.0 clippy echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" # Windows: a host executor with rustup already installed. Only ensure the # pinned toolchain is present + default. pwsh, never bash (that is WSL here). - name: Install Rust 1.98.0 (Windows) if: matrix.os == 'windows' shell: pwsh run: | rustup toolchain install 1.98.0 --profile minimal rustup default 1.98.0 - name: Build the cargo classes (pure Rust, no C codec chain) working-directory: wai/wai-rs # neural_int joins the pure-Rust set: the integer-synthesis decoder is # i64-only (no ONNX, no float), so it carries the same bit-exact promise # and is checkable on this matrix with no ONNX Runtime present. receipt_claims # is in the set because the reference meters (wai_meter, wai_world_meter) write a # measurement report by default and require it: without it they would drop out # of this build on every leg of this matrix, without a word. run: cargo build --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,receipt_claims - name: Library unit tests (the fixed-point determinism floors) working-directory: wai/wai-rs run: cargo test --lib --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,oer,receipt_claims,key_discovery # The JPEG AI exact decoder reproduces across platforms only if no platform math # library is on its path. Two guards, each failing the step: clippy's # disallowed-methods (wai-rs/clippy.toml), which each module on the decode path # denies, rejects a listed float method however it is spelled; and # tests/jpegai_decode_symbols.rs lists the linked decoder's symbols with `nm` and # rejects any C math-library function it imports, whatever made the call, and any # it defines other than the round-to-integral ones (see its doc). `-A clippy::all` # keeps the output to that guard. Unix legs only: `nm` is not on the Windows # runner, and for clippy the unix legs already cover both architectures. - name: JPEG AI exact decode calls no platform math library (unix) if: matrix.os != 'windows' shell: bash working-directory: wai/wai-rs run: | set -euo pipefail cargo clippy --lib --no-default-features --features neural_int -- -A clippy::all cargo test --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film --test jpegai_decode_symbols # The integer decoders run fast kernels (src/int_kernels.rs) that must give # the same integer as the scalar reference kernels for every output element. # The feature list above leaves out audio_int, video_int, world_int and # splat_int, so without these two steps int_audio's kernel-equivalence tests # and tests/int_kernels_vectors.rs (every committed integer-decoder vector # decoded under the reference and under each fast policy, against its frozen # hash) would run nowhere. `--lib` also carries the other kernels' equivalence # modules; tests/int_kernels_geometry.rs runs edge geometry (strides up to # usize::MAX, padding far beyond the kernel) through the public decoders # under every policy. Both profiles, because the overflow-parity tests assert # different things in each: with overflow checks on (dev) that the fast path panics # where the reference panics, with them off (release) that it returns the # same integers. All pure Rust, so every leg runs them. # tests/int_mlicpp.rs decodes the committed wai.neural.int_mlicpp vector against # its pinned hashes (image, every scale bucket, every symbol) under every kernel # policy and thread count, and checks that damaged payloads are refused rather than # overflowing — which is why it runs in both profiles too. # tests/int_mlicpp_preflight.rs (its own binary: it installs a counting global # allocator) checks that a header the model cannot fit is refused before the # decode allocates anything sized from it. # tests/int_audio.rs and tests/int_splat.rs decode the committed wai.audio.int_codec and # wai.splat.int_codec vectors against the hashes the Python integer reference computed at # export, and check each vector's metadata. Cargo.toml declares them, and the other # feature-gated targets named here, with required-features, so if this step ever loses a # feature, cargo stops with an error instead of compiling an empty binary that reports ok. # Each committed vector's decoded output is also checked against the SHA-256 in # its sha256.json sidecar (SPEC §7 *Digests*), and # int_kernels_vectors::every_decoded_output_states_a_sha256 requires a sidecar, # or a named exclusion, for every corpus directory. tests/generate.rs checks the # wai.generate.spectral vector, which `generate` enables. - name: Fast integer kernels ≡ reference kernels (overflow checks on) working-directory: wai/wai-rs run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate - name: Fast integer kernels ≡ reference kernels (release, overflow checks off) working-directory: wai/wai-rs run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate # carriage: every registered capability round-trips byte-exact. cli_wrap_intent and # cli_registry run the `wai` binary, which builds without the codecs feature: the # intent rule of `wai wrap`, its refusal of candidate capabilities (SPEC §5), its media # class, and `wai registry export --check` on the tree and on a drifted copy. # tests/manifest_builders.rs: the manifest types are #[non_exhaustive], and the # builders write the JSON a struct literal wrote; members the types keep only # as `extra` (and `security`) survive a round trip. - name: Carriage conformance and the wai CLI (wrap, registry export) working-directory: wai/wai-rs run: cargo test --no-default-features --test carriage --test manifest_builders --test cli_wrap_intent --test cli_registry # The machine-readable registry (wai/registry/, wai/vocab/v2.*, the table in # wai/REGISTRY.md) is generated from wai-rs/src/codecs/registry.rs. Regenerate it in # memory and fail on any difference; the message says how to regenerate. One leg: the # export is the same text on every platform. - name: The registry export is up to date (linux) if: matrix.os == 'linux' working-directory: wai/wai-rs run: cargo run -q --no-default-features --bin wai -- registry export --check # The C ABI's envelope surface builds with no codec library (feature `ffi`): # unpack returns each manifest as the envelope carries it, the WAI2 table carries # every component's identity and selectability, wai_envelope_select follows # SPEC §7.1 and never picks a candidate, and the packers refuse a candidate # (SPEC §5). Cargo.toml declares the target with required-features. - name: C ABI envelope surface (no codec library) working-directory: wai/wai-rs run: cargo test --no-default-features --features ffi --test ffi_envelope # The browser crate wraps the same envelope types (wai::container), so role, # companion_of, component identity and the pin reach JS, and its pick is SPEC # §7.1's. Its tests target the pure-Rust half (`core`), which runs natively; one # leg suffices, the logic is architecture-independent. The JS surface itself runs # under wasm32 in wai-web-wasm.yml. - name: wai-web envelope surface (native tests) if: matrix.os == 'linux' working-directory: wai/wai-web run: cargo test --locked # jwp-receipts §7.1, §7.8: no `did:web`/`did:key` literal in a tracked .rs, .json or # .js file under wai/ names a real web origin — every key the tree signs with has a # published seed. Ungated: it reads the tree through `git ls-files` and classifies # with `signer_id`, which builds with no feature. - name: Every signer id in the tree is a label working-directory: wai/wai-rs run: cargo test --no-default-features --test signer_ids_are_labels # jwp-receipts §7: `wai-keys` builds a key log from operator-supplied seeds, appends # revocations and rotations, signs statuses and module grants, verifies a log as # §7.3 anchors one, and checks a DID document against it. The test drives the binary. - name: wai-keys — build, check and publish an origin key log working-directory: wai/wai-rs run: cargo test --no-default-features --features key_discovery --test wai_keys_cli # key_log has no I/O, clock or threads, so a browser sink could run it before it # instantiates a module (§7.9); no browser sink does yet. This step checks that it # builds for wasm32 on the unix legs (the target is added to the pinned toolchain # here; the Windows host runner is not set up for it). - name: key_log builds for wasm32 (unix) if: matrix.os != 'windows' shell: bash working-directory: wai/wai-rs run: | set -euo pipefail rustup target add --toolchain 1.98.0 wasm32-unknown-unknown cargo check --lib --target wasm32-unknown-unknown --no-default-features --features key_discovery # The corpus-verify loop is genuinely shell-specific (a helper that groups # log output and propagates the first non-zero exit), so it ships once per # shell family. The command it runs is identical; only the wrapper differs. - name: Every cargo-class corpus reproduces its frozen hashes (unix) if: matrix.os != 'windows' shell: bash working-directory: wai/wai-rs run: | set -euo pipefail F=world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery v() { echo "::group::$1"; cargo run -q --no-default-features --features "$F" --bin "$2" -- verify "$3"; echo "::endgroup::"; } v worlds wai_world_vectors ../world-conformance v spatial-audio wai_audio_vectors ../audio-conformance v haptics wai_haptic_vectors ../haptic-conformance v state-feeds wai_feed_vectors ../feed-conformance v avatars wai_avatar_vectors ../avatar-conformance v volumetric wai_splat4d_vectors ../volumetric-conformance v score wai_score_vectors ../score-conformance v int-synth wai_int_synth_vectors ../neural-conformance v film wai_film_vectors ../film-conformance v quantum wai_quantum_vectors ../quantum-conformance v quantum-ops wai_quantum_ops_vectors ../quantum-ops-conformance v oer wai_oer_vectors ../oer-conformance v jwp wai_jwp_vectors ../jwp-conformance v claims wai_claim_vectors ../claim-conformance v pin wai_pin_vectors ../pin-conformance v signers wai_signer_vectors ../signer-conformance # The pin corpus's second implementation: Python standard library only, sharing no # code with wai-rs. One leg is enough — it checks digests and listing rules, which # do not depend on the architecture — and the macOS host has python3. - name: The pin corpus checks against an independent verifier (macOS) if: matrix.os == 'macos' shell: bash working-directory: wai run: python3 pin-conformance/pin_verify.py pin-conformance # OER's second implementation is portable C, not Rust. Rebuild it from source and # require its output to match the pinned corpus byte for byte: the kernel is # deterministic, and wai_oer_vectors above has already verified the pinned copy, # so equality carries that verification to what the source produces today. # `cc` with the Makefile's flags, so the step needs no `make` on the runner. - name: OER C kernel reproduces its pinned receipts (unix) if: matrix.os != 'windows' shell: bash working-directory: wai/oer-conformance/settle run: | set -euo pipefail cc -O2 -std=c99 -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare \ -o "$RUNNER_TEMP/settle_host" host_main.c settle.c sha256.c tweetnacl.c -lm "$RUNNER_TEMP/settle_host" "$RUNNER_TEMP/firmware_vectors.json" 400 cmp "$RUNNER_TEMP/firmware_vectors.json" ../firmware_vectors.json - name: Every cargo-class corpus reproduces its frozen hashes (Windows) if: matrix.os == 'windows' shell: pwsh working-directory: wai/wai-rs run: | $F = "world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery" function v($grp, $bin, $path) { Write-Output "::group::$grp" cargo run -q --no-default-features --features $F --bin $bin -- verify $path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } Write-Output "::endgroup::" } v worlds wai_world_vectors ../world-conformance v spatial-audio wai_audio_vectors ../audio-conformance v haptics wai_haptic_vectors ../haptic-conformance v state-feeds wai_feed_vectors ../feed-conformance v avatars wai_avatar_vectors ../avatar-conformance v volumetric wai_splat4d_vectors ../volumetric-conformance v score wai_score_vectors ../score-conformance v int-synth wai_int_synth_vectors ../neural-conformance v film wai_film_vectors ../film-conformance v quantum wai_quantum_vectors ../quantum-conformance v quantum-ops wai_quantum_ops_vectors ../quantum-ops-conformance v oer wai_oer_vectors ../oer-conformance v jwp wai_jwp_vectors ../jwp-conformance v claims wai_claim_vectors ../claim-conformance v pin wai_pin_vectors ../pin-conformance v signers wai_signer_vectors ../signer-conformance # The generated v2 vocabulary (wai/vocab/v2.*) is checked as a consumer uses it: # sample manifests, typed and untyped, WAI1 and WAI2, are expanded through the # JSON-LD context exactly as written on the wire and validated against the SHACL # shapes, each with the result it must give (a candidate in any place is # rejected; a media value no revision lists is accepted). One leg: RDF tooling, # not arithmetic. The tool versions are pinned. vocab-shapes: name: v2 vocabulary · manifests validate as stated runs-on: linux steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install the RDF tools shell: bash run: python -m pip install rdflib==7.6.0 PyLD==3.3.0 pyshacl==0.40.1 - name: Sample manifests against vocab/v2.shapes.ttl shell: bash run: python wai/vocab/check_shapes.py # The corpora above are generated and verified by wai-rs, one implementation. These are # the second implementations, written from the extension text in Python and sharing no # code with wai-rs: claim_verify.py (every claim vector, the canonical fixture, and the # bracket arithmetic of energy-measurement §6.2 recomputed with Python's integers), # differential.py (both verifiers' verdicts on generated inputs the corpus does not hold, # under a fixed seed, so a run is reproducible), jwp_verify.py (every JWP vector), and # signer_verify.py (every key-discovery vector and the canonical fixture, with its # verdicts required to equal wai_signer_vectors' line for line). # Each exits non-zero on any failure or any difference, and the step fails with it. # One platform: what they check is the reading of the text, not the architecture. independent-verifiers: name: independent verifiers · claims, JWP and key discovery (Python) runs-on: linux steps: - uses: actions/checkout@v5 - name: Install Rust 1.98.0 shell: bash run: | export PATH="$HOME/.cargo/bin:$PATH" command -v rustup >/dev/null 2>&1 || \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 rustup toolchain install 1.98.0 --profile minimal rustup default 1.98.0 echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - uses: actions/setup-python@v5 with: python-version: '3.11' # The verifiers check Ed25519's equation with the `cryptography` package and make the # rest of jwp-receipts §3.1 step 3 themselves. - name: Install the Ed25519 library shell: bash run: python -m pip install cryptography # differential.py drives wai_claim_vectors' `verdicts` mode; the key-discovery # verdicts are compared with wai_signer_vectors'. - name: Build the corpus verifiers wai-rs ships working-directory: wai/wai-rs shell: bash run: cargo build --no-default-features --features key_discovery --bin wai_claim_vectors --bin wai_jwp_vectors --bin wai_signer_vectors - name: claim_verify.py, differential.py, jwp_verify.py, signer_verify.py working-directory: wai shell: bash run: | set -euo pipefail python3 claim-conformance/claim_verify.py claim-conformance/vectors.json python3 claim-conformance/differential.py "$GITHUB_WORKSPACE/wai/wai-rs/target/debug/wai_claim_vectors" --random 2000 --seed 1 python3 jwp-conformance/jwp_verify.py jwp-conformance/vectors.json python3 signer-conformance/signer_verify.py signer-conformance/vectors.json python3 signer-conformance/signer_verify.py verdicts signer-conformance/vectors.json > "$RUNNER_TEMP/signer-py.tsv" "$GITHUB_WORKSPACE/wai/wai-rs/target/debug/wai_signer_vectors" verdicts signer-conformance/vectors.json > "$RUNNER_TEMP/signer-rs.tsv" diff "$RUNNER_TEMP/signer-py.tsv" "$RUNNER_TEMP/signer-rs.tsv" byte-exact: name: cross-arch byte-equality · ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: # Self-hosted fleet labels. Both architectures: x86_64 by linux, arm64 # by linux-arm64 + macos — what matters for the Fx integer floor is the # arch, not the OS. No Intel-Mac runner exists in the fleet, and it # would add no arch coverage anyway. # Windows x86_64 leg removed 2026-09-30: runner offline. Restore by # re-adding `windows` here once the runner is back online. os: [linux, linux-arm64, macos] steps: - uses: actions/checkout@v5 - name: Install Rust 1.98.0 (unix) if: matrix.os != 'windows' shell: bash run: | export PATH="$HOME/.cargo/bin:$PATH" command -v rustup >/dev/null 2>&1 || \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 rustup toolchain install 1.98.0 --profile minimal rustup default 1.98.0 echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Install Rust 1.98.0 (Windows) if: matrix.os == 'windows' shell: pwsh run: | rustup toolchain install 1.98.0 --profile minimal rustup default 1.98.0 - name: Integer rANS + int_synth + JPEG-AI mANS goldens (one machine → every machine) working-directory: wai/byte-exact-conformance run: cargo test --release # The C2PA emitter is gated behind an off-by-default feature, so the four-platform # job above — which builds a fixed feature list — never compiles it. A feature no # workflow enables has no CI coverage at all: the suite passes, and the tests in it # simply do not run. # # One platform, deliberately. Everything the matrix above exists to prove is about # byte-identical reconstruction across architectures; a signed manifest makes no # such claim and would not become more true for being emitted on four machines. What # this job proves is that the manifest is still well-formed, still carries the three # assertions, and still validates — which is architecture-independent. c2pa-emitter: name: C2PA manifest emission · signed, read back, validated runs-on: linux steps: - uses: actions/checkout@v4 - name: Install Rust 1.98.0 shell: bash run: | export PATH="$HOME/.cargo/bin:$PATH" command -v rustup >/dev/null 2>&1 || \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 rustup toolchain install 1.98.0 --profile minimal rustup default 1.98.0 echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" # `rust_native_crypto` rather than the SDK's default openssl backend, so this # job needs no system libraries — the same property the matrix above depends on. # Asserted, not assumed: an openssl-linked build would still pass the tests here # while quietly making the crate undistributable to a bare runner. - name: The emitter pulls in no system crypto working-directory: wai/wai-rs shell: bash run: | for dep in openssl openssl-sys; do if cargo tree --no-default-features --features c2pa_emit -i "$dep" >/dev/null 2>&1; then echo "::error::$dep reached the c2pa_emit dependency graph" exit 1 fi done echo "no system crypto in the graph" - name: Emit, sign, read back, validate working-directory: wai/wai-rs run: cargo test --lib --no-default-features --features c2pa_emit c2pa_emit # The feature must stay optional. If `provenance` ever pulls c2pa in, every # consumer that wanted only the binding types inherits a large dependency tree # they did not ask for, and the "off by default" claim in the extension is false. - name: The feature stays optional working-directory: wai/wai-rs shell: bash run: | if cargo tree --no-default-features --features provenance -i c2pa >/dev/null 2>&1; then echo "::error::c2pa is reachable from the provenance feature; it must stay opt-in" exit 1 fi cargo build --no-default-features --features provenance echo "provenance builds without c2pa"