wai: the record made true #201
37 changed files with 941 additions and 88 deletions
15
.github/workflows/wai-conformance.yml
vendored
15
.github/workflows/wai-conformance.yml
vendored
|
|
@ -559,12 +559,25 @@ jobs:
|
|||
|| { echo "::error::no test of $m ran"; exit 1; }
|
||||
done
|
||||
|
||||
# The wai package is built as one codegen unit here. Its lib target is also a cdylib,
|
||||
# and the wasm linker in Rust 1.98 (LLD 22) can crash with SIGSEGV linking that cdylib:
|
||||
# the linker keeps the flags of the first object that names the function table, so
|
||||
# when that object names it without NO_STRIP the table is no garbage-collection root.
|
||||
# Nothing the cdylib keeps calls through the table, but it does keep function
|
||||
# addresses, and writing their element segment then dereferences a table that was
|
||||
# never created. Which object comes first follows the hashed names of the codegen
|
||||
# units, so an unrelated change to the crate, or a different checkout path, can bring
|
||||
# the crash on or off. As one unit, wai's object comes first and takes function
|
||||
# addresses, so it names the table with NO_STRIP. LLD 23 keeps NO_STRIP from any
|
||||
# object. The units change how the crate is split for code generation, not what the
|
||||
# program computes.
|
||||
- name: wai_quantum_vectors verify under wasmtime
|
||||
shell: bash
|
||||
working-directory: wai/wai-rs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo build --locked --target wasm32-wasip2 --no-default-features --features quantum --bin wai_quantum_vectors
|
||||
cargo build --locked --target wasm32-wasip2 --no-default-features --features quantum --bin wai_quantum_vectors \
|
||||
--config 'profile.dev.package.wai.codegen-units=1'
|
||||
wasmtime run --dir ../quantum-conformance::/corpus \
|
||||
target/wasm32-wasip2/debug/wai_quantum_vectors.wasm verify /corpus
|
||||
|
||||
|
|
|
|||
4
.github/workflows/wai-section-refs.yml
vendored
4
.github/workflows/wai-section-refs.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
name: WAI section references
|
||||
|
||||
# Every section that SPEC.md or an extension cites in another WAI text exists:
|
||||
# Every section that SPEC.md, an extension or SECURITY.md cites in a WAI text exists:
|
||||
# a link followed by §N, a link whose text holds §N, or an extension's name (or
|
||||
# SPEC) in prose followed by §N, including references that break across lines.
|
||||
# A citation of a section that was never written — as transparency §6's pointer
|
||||
|
|
@ -34,12 +34,14 @@ on:
|
|||
paths:
|
||||
- 'wai/extensions/**'
|
||||
- 'wai/SPEC.md'
|
||||
- 'wai/SECURITY.md'
|
||||
- 'scripts/check-wai-section-refs.py'
|
||||
- '.github/workflows/wai-section-refs.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'wai/extensions/**'
|
||||
- 'wai/SPEC.md'
|
||||
- 'wai/SECURITY.md'
|
||||
- 'scripts/check-wai-section-refs.py'
|
||||
- '.github/workflows/wai-section-refs.yml'
|
||||
|
||||
|
|
|
|||
18
.github/workflows/wai-web-reach.yml
vendored
18
.github/workflows/wai-web-reach.yml
vendored
|
|
@ -27,7 +27,10 @@ name: wai-web reach builds
|
|||
# 2. on current Node (reach/test/run-modern.mjs): the loader must choose the shipped
|
||||
# build; every rung and profile, forced, must give the shipped build's outputs over
|
||||
# the corpora; an engine stripped to ES5 must be given J and the same outputs; and
|
||||
# the demo verify scripts must pass against shipped, L1, L2 and J;
|
||||
# the demo verify scripts must pass against shipped, L1, L2 and J. On the record leg
|
||||
# those scripts, and the corpora they read, are the record's commit's
|
||||
# (--demo-commit): the package is held to the checks of the code it was built from,
|
||||
# and a check added since meets that code on the tree leg;
|
||||
# 3. on older engine lines, each in its own Node image (reach/test/conformance.js,
|
||||
# which is ECMAScript 5): the loader must choose the rung that line needs, by
|
||||
# feature probes alone, and every workload must equal the shipped build's output
|
||||
|
|
@ -175,6 +178,11 @@ jobs:
|
|||
"$REACH/manifest.json" "$REACH/record"
|
||||
mkdir -p "$RUNNER_TEMP/pkg"
|
||||
cp "$REACH/record/wai_web_bg.wasm" "$REACH/record/wai_web.js" "$RUNNER_TEMP/pkg/"
|
||||
# The commit the package was built from (input.commit): its demo verify scripts
|
||||
# are the ones run-modern.mjs runs on this leg. The checkout is shallow.
|
||||
c=$(node -p 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).input.commit' "$REACH/manifest.json")
|
||||
git fetch --no-tags --depth=1 origin "$c"
|
||||
echo "RECORD_COMMIT=$c" >> "$GITHUB_ENV"
|
||||
# The build and the reference stay in the workspace: the engine images below
|
||||
# mount it, and nothing else.
|
||||
- name: Reach builds (reach/build.sh, pinned binaryen and JS tools)
|
||||
|
|
@ -184,9 +192,15 @@ jobs:
|
|||
if [ "${{ matrix.leg }}" = record ]; then export WAI_REACH_REQUIRE_RECORD=1; fi
|
||||
WAI_REACH_TOOLS="$RUNNER_TEMP/reach-tools" bash "$REACH/build.sh" "$RUNNER_TEMP/pkg" "$DIST"
|
||||
cat "$DIST/manifest.json"
|
||||
# The demo verify scripts: on the record leg, the record's commit's, over that
|
||||
# commit's corpora; on the tree leg, this tree's.
|
||||
- name: Current Node — every rung forced, the demo verify scripts on every full build
|
||||
shell: bash
|
||||
run: node "$REACH/test/run-modern.mjs" --dist "$DIST" --shipped "$RUNNER_TEMP/pkg" --out "$REACH/ci-modern-${{ matrix.leg }}"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
set --
|
||||
if [ "${{ matrix.leg }}" = record ]; then set -- --demo-commit "$RECORD_COMMIT"; fi
|
||||
node "$REACH/test/run-modern.mjs" --dist "$DIST" --shipped "$RUNNER_TEMP/pkg" --out "$REACH/ci-modern-${{ matrix.leg }}" "$@"
|
||||
|
||||
- name: node:0.12 (no WebAssembly) — J chosen and equal to the shipped build
|
||||
uses: docker://node:0.12
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Every section a WAI specification text cites in another WAI text exists.
|
||||
|
||||
Reads wai/SPEC.md and wai/extensions/*.md. In each, a reference to a section of
|
||||
SPEC.md or of an extension is checked against the headings of the file it names. Three
|
||||
spellings are recognised, including where the reference breaks across lines:
|
||||
Reads wai/SPEC.md, wai/extensions/*.md and wai/SECURITY.md. In each, a reference to a
|
||||
section of SPEC.md or of an extension is checked against the headings of the file it
|
||||
names. Three spellings are recognised, including where the reference breaks across
|
||||
lines:
|
||||
|
||||
[text](X.md) §N a link, then the section
|
||||
[text §N](X.md) the section inside the link text
|
||||
|
|
@ -37,6 +38,7 @@ def sections(path):
|
|||
def texts():
|
||||
out = [os.path.join(WAI, "SPEC.md")]
|
||||
out += sorted(os.path.join(EXT, n) for n in os.listdir(EXT) if n.endswith(".md"))
|
||||
out.append(os.path.join(WAI, "SECURITY.md")) # it cites the texts it points readers to
|
||||
return out
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,59 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## v0.4.0 — current
|
||||
|
||||
### The record made true: training, paths, protected content, reader limits, security
|
||||
|
||||
- **`int_synth`'s weights are quantised after float training.** SPEC §5 and
|
||||
[determinism-tiers](extensions/determinism-tiers.md) §5 said the capability decodes
|
||||
"QAT weights". No committed model was trained that way: the trained corpus vector,
|
||||
`photo`, was made by `tools/wai_int_synth_real.py`, float training then post-training
|
||||
quantisation with each layer's shift derived from its measured activation range, and
|
||||
the weights of the synthetic vectors and of the cross-architecture golden come from a
|
||||
seed. Both texts now say so, and so do the tools:
|
||||
`tools/wai_int_synth_qat.py` is marked experimental (it trains against one fixed shift
|
||||
and made no committed model), and `tools/wai_int_synth_export.py` names the working
|
||||
path.
|
||||
- **Stale paths.** The int_synth decoder is `int_synth.rs` at the crate root, not
|
||||
`neural/int_synth.rs`. The quantum-ops engine and the quantum-sim receipt live in
|
||||
`wai-quantum`, whose features `wai-rs` forwards. README and SPEC Appendix A no longer
|
||||
send macOS builders to a `.cargo/config.toml` that no longer exists: `wai-rs/build.rs`
|
||||
finds libFLAC.
|
||||
- **Protected content: two lanes** (SPEC §6.5). Lane A, hardware-protected content,
|
||||
decodes inside the platform's protected media path; WAI's records about it (among
|
||||
them the served manifest, the segment map, the key-release decisions, the ad
|
||||
decisions and impressions of `wai.video.ssai`, the program-as-run windows of
|
||||
`wai.video.channel`, and the content hashes of WAI objects) cover no decoded frame.
|
||||
This revision registers no `security.encryption` value for it. Lane B, end-to-end
|
||||
encryption at the application layer (`mls-sframe`), decrypts in the application, so
|
||||
the tier and the output digests of the sink that holds the key hold as they do for
|
||||
plaintext; staged-measurement counts no encrypted object, and a verifier without the
|
||||
key checks none of a `reconstruct-session` claim's rules that need a payload. Lane B
|
||||
is not a protected-path robustness level. `wai.video.keys` and the confidentiality
|
||||
extension say which lane they belong to.
|
||||
- **Where the exact tier holds** (determinism-tiers §5). `decode-equivalence` is shown
|
||||
on native CPU and on wasm32, on no GPU or NPU; a WebAssembly decoder at the tier uses
|
||||
no relaxed-SIMD instruction and depends on no NaN's bits; and the tier never covers
|
||||
output a sink cannot read.
|
||||
- **Reader limits** (SPEC §2). A reader need accept no manifest longer than 2²⁷ bytes
|
||||
(128 MiB) or nested deeper than 127, and refuses a longer one from `man_len` alone
|
||||
(`manifest_too_large`). The length limit is sized to the largest manifest a
|
||||
conforming staged layer has: a `prev` (staged-delivery §3) naming a different
|
||||
producer for each of the 2²⁰ units a `WIR1` layer may cover (staged-delivery
|
||||
Appendix A.3) is about 94 MB. It bounds what a reader reads, not the memory a parse
|
||||
takes, which in the reference readers is up to nearly 40 times the manifest's length
|
||||
(`SECURITY.md`). `wai-rs`'s readers, its HTTP head parser (`head_malformed`) and the
|
||||
Python verifiers apply the length limit, and the HTTP corpus holds it at its edge;
|
||||
the writers, `wai-web`'s included, refuse to write past it, and refuse a payload whose
|
||||
length does not fit its `u32` field.
|
||||
*Envelopes this makes non-conforming:* none of the layout's; an envelope whose manifest
|
||||
is longer than 128 MiB, or nested deeper than 127, is one a conforming sink may refuse,
|
||||
so a conforming encoder no longer writes it (SPEC §7).
|
||||
- **`SECURITY.md`.** How to report a vulnerability, and where the reference
|
||||
implementation reads untrusted input: the manifest limits and the memory a manifest's
|
||||
parse takes, the native codec libraries and their gaps (unbounded decompression output
|
||||
for `wai.text.zstd` and `wai.text.xz`; no fuzzing in CI), module authorisation, and the
|
||||
fingerprinting risk of `held_sets`. CI checks its section references.
|
||||
|
||||
### Vector graphics (Draft): the extension text of `wai.graphics.vector`, §1–§3
|
||||
|
||||
- **A new extension, [graphics-vector](extensions/graphics-vector.md) (Draft).**
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ cargo check -p wai --no-default-features
|
|||
| [`research/`](research/) | Research notes and dated records: graded, not normative |
|
||||
| [`glossary/`](glossary/) | Reference catalog of media codecs surveyed for capability registration. [`codecs.json`](glossary/codecs.json) is the dataset, linked to the registry; [`SCHEMA.md`](glossary/SCHEMA.md) defines the shape and contribution rules. Rendered at [wai.transaction.science/glossary](https://wai.transaction.science/glossary). |
|
||||
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | How to change the specification, register a capability and add tests CI runs |
|
||||
| [`SECURITY.md`](SECURITY.md) | How to report a vulnerability, and where the reference implementation reads untrusted input: the manifest limits, the native codec libraries, module authorisation, the fingerprinting risk of advertised parameter sets |
|
||||
|
||||
## Quickstart
|
||||
|
||||
|
|
@ -62,10 +63,10 @@ cargo run --no-default-features --bin wai -- capabilities # the registry, one r
|
|||
cargo build --release --lib # with the codecs feature: libwai.dylib/.so/.dll + libwai.a
|
||||
```
|
||||
|
||||
On macOS, libflac lives under the Homebrew "flac" formula prefix that
|
||||
Cargo doesn't probe automatically. The committed `.cargo/config.toml`
|
||||
adds the path; if you build outside the repo set
|
||||
`RUSTFLAGS="-L /opt/homebrew/opt/flac/lib"`.
|
||||
With the `codecs` feature, `wai-rs/build.rs` finds libFLAC through pkg-config
|
||||
and, on macOS without pkg-config, in the Homebrew "flac" formula prefix
|
||||
(`/opt/homebrew/opt/flac/lib` or `/usr/local/opt/flac/lib`). Where neither finds
|
||||
it, set `RUSTFLAGS="-L <the directory holding libFLAC>"`.
|
||||
|
||||
## SDK consumption
|
||||
|
||||
|
|
|
|||
189
wai/SECURITY.md
Normal file
189
wai/SECURITY.md
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
# Security
|
||||
|
||||
This file says where WAI's reference implementation reads untrusted input, what it
|
||||
bounds, and what it does not yet do. The rules a sink must follow are in
|
||||
[`SPEC.md`](SPEC.md) and [`extensions/`](extensions/); this file points at them and
|
||||
adds nothing normative.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Email **[email protected]**. Do not open a public issue or pull request
|
||||
for a vulnerability that is not yet fixed.
|
||||
|
||||
Say which component is affected (a crate and file, a verifier, or a section of the
|
||||
specification), the commit you tested, the input or steps that trigger it, what
|
||||
happened and what you expected. An input file is the most useful thing to send.
|
||||
|
||||
In scope:
|
||||
|
||||
- the specification and its extensions, where a rule lets a conforming sink or
|
||||
verifier be made to misbehave;
|
||||
- the reference implementation: `wai-rs`, `wai-quantum`, `wai-web`, `wai-pack`,
|
||||
`wai-relay`, `wai-verify` and the other crates under this directory;
|
||||
- the conformance verifiers (`*-conformance/*.py`) and their corpora.
|
||||
|
||||
Out of scope: services anyone deploys with this code, which their operators answer for.
|
||||
|
||||
WAI is pre-1.0. Fixes go to `main` of the canonical repository,
|
||||
`https://git.transaction.science/Transaction-Science/open-standards`; no earlier
|
||||
release line is maintained.
|
||||
|
||||
## Manifest parsing
|
||||
|
||||
Every envelope a sink receives is untrusted. Its container and manifest are read by
|
||||
`wai-rs/src/container.rs` (`Wai::from_bytes`, `WaiMulti::from_bytes`,
|
||||
`manifest_bytes`) and, over HTTP, by `wai-rs/src/http_binding/head.rs`
|
||||
(`parse_head`).
|
||||
|
||||
What the readers bound:
|
||||
|
||||
| what | bound | where it is specified |
|
||||
|---|---|---|
|
||||
| every length field (`man_len`, `payload_len`, each rendition's offset and length) | checked against the bytes in hand before anything is sliced; sums formed so they cannot wrap (`saturating_add`; the `WAI2` payload total in `u64`) | SPEC §2, §7.1 |
|
||||
| manifest length | at most 134 217 728 bytes (2²⁷), sized to the largest manifest a conforming staged layer has. A longer one is refused from `man_len` alone, before it is read or, over HTTP, requested (`manifest_too_large`; `head_malformed` from `parse_head`) | SPEC §2, *Reader limits* |
|
||||
| manifest nesting | at most 127 nested arrays and objects, the manifest object counting as one; 128 are refused | SPEC §2, *Reader limits* |
|
||||
| member names | a member named twice in one object, at any depth, is refused | SPEC §2 |
|
||||
| `WAI2` rendition table | at most 65 535 rows (`u16`), as many as the manifest's `renditions`, packed contiguously from offset 0 | SPEC §7.1 |
|
||||
| an HTTP object | exactly one envelope; any other length is refused (`object_length_mismatch`) | http-delivery §5.1 |
|
||||
| a head proof | at most 64 chaining values; `covered_len` is the object's length, or shorter and ending on a chunk boundary of the hashed input | http-delivery §6 |
|
||||
|
||||
A reader copies a payload only after its bounds are checked, so a payload never makes
|
||||
it allocate more than it was given. A `WAI1` reader ignores bytes after the payload; an
|
||||
HTTP object that carries any is refused ([http-delivery](extensions/http-delivery.md)
|
||||
§5.1).
|
||||
|
||||
**Parsing a manifest takes more memory than its length.** The reference readers parse
|
||||
the manifest into a JSON tree (`serde_json::Value`, for `target`, `refines`,
|
||||
`derived_from` and every member this revision does not define), and a parsed value
|
||||
takes more memory than its text: about seven times the length of a 94 MB `prev` that
|
||||
names a producer for each of 2²⁰ units, the largest manifest a conforming layer has,
|
||||
and nearly 40 times the length of a manifest of many small values, such as one long
|
||||
array of zeros. A manifest at the length limit can take some 5 GB and several seconds
|
||||
to parse, and a reader may not refuse it for its length (SPEC §2). The length limit
|
||||
bounds what a reader must read, not what parsing it costs; the reference sets no limit
|
||||
of its own on the parse.
|
||||
|
||||
The library tests in `container.rs` and `http_binding/tests.rs` check the manifest
|
||||
limits at their edges: from its first eight bytes, every reader refuses a manifest one
|
||||
byte past the length limit as too large and one at the limit only as truncated; a
|
||||
2 MiB manifest is read and written back byte for byte; and 127 levels are read where
|
||||
128 are refused. The writers (`Wai::to_bytes`, `WaiMulti::to_bytes`, and `pack` and
|
||||
`packMulti` in `wai-web`) refuse a manifest past the length limit, and a payload whose
|
||||
length does not fit its `u32` field. The Rust writers stop serialising as soon as the
|
||||
manifest passes the limit; the test runs both writers at limits equal to an envelope's
|
||||
manifest and payload lengths and one byte below them, since serialising 128 MiB takes a
|
||||
debug build many seconds. `verify_pack.mjs` checks the browser writers at the real
|
||||
limit.
|
||||
|
||||
The stdlib Python verifiers read envelopes too. `staged_verify.py`,
|
||||
`claim_verify.py`, `pack_verify.py` and `http_verify.py` refuse a manifest past the
|
||||
length limit as the reference does. `http_verify.py` and `wai_http_vectors` agree on
|
||||
`parse_head` at the limit in the HTTP corpus; the other three each build an envelope in
|
||||
memory with a manifest one byte past it and require it refused. Those that read
|
||||
manifests through `staged_verify.strict_json` refuse nesting past 127; `pack_verify.py`
|
||||
reads up to the interpreter's recursion limit and refuses past it as unparseable.
|
||||
|
||||
Payload formats that WAI defines carry their own caps, checked before the first symbol
|
||||
is decoded: a tensor of at most 2²⁶ elements, a `WIV1` clip of at most 2³⁰ decoded
|
||||
bytes and 2²⁰ frames, and a sink cost limit (`int_payload::check_cost`,
|
||||
`cost_over_limit`) ([integer-payloads](extensions/integer-payloads.md) §7, §8).
|
||||
`wai.neural.int_mlicpp` refuses a header whose shapes, or whose buffers, pass its cap
|
||||
before it allocates them (`tests/int_mlicpp_preflight.rs`).
|
||||
|
||||
## Native codec libraries and sandboxed decode
|
||||
|
||||
The default `codecs` feature binds C and C++ libraries for the floor and the
|
||||
recommended set: libzstd and liblzma for `wai.text.zstd` and `wai.text.xz`, libjxl
|
||||
for JPEG XL, libavif for AVIF decode, libopus, dav1d for AV1 decode, and libFLAC for
|
||||
FLAC encode. PNG, JPEG and FLAC decode run in Rust (the `image` and `claxon` crates).
|
||||
The `neural` feature binds ONNX Runtime for the float neural decoders. These libraries
|
||||
parse payload bytes directly, and they are the usual place a memory-safety defect
|
||||
in a media stack lives.
|
||||
|
||||
They run in the caller's process, with its privileges: the reference implementation
|
||||
has no process or system-call sandbox. A sink that decodes payloads from origins it
|
||||
does not trust through these libraries should run the decode in a separate process
|
||||
with no file-system or network access, or use a decoder compiled to WebAssembly and
|
||||
run it in a WebAssembly runtime.
|
||||
|
||||
What does not touch them:
|
||||
|
||||
- CI builds, except the neural workflows: no workflow enables `codecs` or `semantic`,
|
||||
so no CI build links the codec libraries. `wai-neural-gates.yml` and
|
||||
`wai-neural-e2e.yml` build the `neural` feature, which links ONNX Runtime and
|
||||
libzstd, and run the float neural decoders through them; no other workflow's build
|
||||
links any of these libraries.
|
||||
- WAI's own decoders are Rust: the integer codecs, worlds, scores, films and the
|
||||
generator. Outside the C ABI (`ffi.rs`, `ffi_codecs.rs`, `ffi_neural.rs`), the only
|
||||
`unsafe` in `wai-rs`'s library code is in the AArch64 SIMD kernels of
|
||||
`int_kernels.rs`, each block with the bounds argument beside it.
|
||||
- `wai-web`, the browser crate, links none of the C libraries and runs inside the
|
||||
browser's WebAssembly sandbox.
|
||||
|
||||
Model checking covers part of the integer core: Kani harnesses beside the code in
|
||||
`rans.rs`, `int_entropy.rs`, `int_transform.rs`, `wai_video_policy.rs`,
|
||||
`staged/predict.rs` and `staged/wir1_arith.rs`, hosted by `wai-verify` and run by
|
||||
`.github/workflows/wai-formal-verification.yml`. `wai-verify` also compiles
|
||||
`int_kernels.rs`, on its scalar path, because `int_transform.rs` calls it, but no
|
||||
harness reaches it: its AArch64 SIMD kernels, the only `unsafe` named above, are not
|
||||
model-checked. Equality tests hold them to the scalar reference kernels, element by
|
||||
element beside each kernel and over every committed integer-decoder vector
|
||||
(`tests/int_kernels_vectors.rs`), on every platform leg of `wai-conformance.yml`.
|
||||
|
||||
Known gaps:
|
||||
|
||||
- **Decompression output is not bounded.** `wai.text.zstd`, in the floor, and
|
||||
`wai.text.xz` decode the whole stream into memory, so a small payload can expand to
|
||||
any size the format allows. The image, audio and video decoders allocate what the
|
||||
bitstream declares, within the libraries' own limits. The reference sets no output
|
||||
limit of its own.
|
||||
- **Fuzzing is shallow.** `tests/fuzz_smoke.rs` feeds seeded random bytes to each
|
||||
codec wrapper and to `Wai::from_bytes` and requires that nothing panics. It needs
|
||||
the `codecs` feature, which no CI workflow builds, so it runs only by hand. There is
|
||||
no coverage-guided fuzzing of the manifest parser, the integer payload parsers or the
|
||||
codec wrappers.
|
||||
|
||||
## Module authorisation
|
||||
|
||||
A module is code a sink obtains apart from an envelope and runs to decode a
|
||||
capability or to rebuild a reconstruction ([SPEC](SPEC.md) §4, *Module
|
||||
authorisation*). A sink runs one only if it holds it under
|
||||
[jwp-receipts](extensions/jwp-receipts.md) §7.9: a grant, signed under a module
|
||||
origin's key log, naming the SHA-256 of the module's bytes exactly as the sink
|
||||
instantiates them, its form, the capabilities it may decode and a validity window,
|
||||
with revocation through the same key log. A module the sink does not hold leaves its
|
||||
capability unadvertised for that envelope, and the envelope's fallback still runs.
|
||||
Parameter sets pinned by digest are checked before use and revoked the same way
|
||||
(SPEC §3.1, checks 5–8).
|
||||
|
||||
The reference dispatcher runs no module obtained apart from an envelope. With a key
|
||||
log (`key_log::SinkKeyLog`, feature `key_discovery`) it refuses a revoked parameter
|
||||
set (`prior_revoked`).
|
||||
|
||||
Authorisation decides which code runs, not what that code can reach. The
|
||||
environment a module runs in (its imports, a memory limit, a time limit, and the
|
||||
determinism rules an exact replay needs) is not specified yet. Until it is, a sink
|
||||
that runs modules confines them itself, for example in a WebAssembly runtime that
|
||||
gives the module no file-system, network, clock or random-number imports.
|
||||
|
||||
## Fingerprinting: advertised parameter sets
|
||||
|
||||
The Sink Capability Descriptor ([http-delivery](extensions/http-delivery.md) §12.1)
|
||||
can list `held_sets`: for each capability, the digests of the parameter sets the
|
||||
sink holds. Which sets a sink holds depends on what it has fetched before, so the
|
||||
list can identify a sink across the sites it is sent to, much as a cookie does. A
|
||||
shared compression dictionary named by its hash raises the same concern (RFC 9842).
|
||||
|
||||
Privacy rules for the descriptor and for parameter-set fetches are not written yet.
|
||||
They are to be added to http-delivery: the list kept per top-level site, clearable by
|
||||
the user, disclosed only as far as a choice of rendition needs it, never sent across
|
||||
sites, and off by default. Until they land, send `held_sets` only to the origin that
|
||||
needs it to choose a rendition, and never across sites. No code in this tree builds
|
||||
or sends the descriptor.
|
||||
|
||||
## Protected content
|
||||
|
||||
End-to-end encryption at the application layer (`mls-sframe`, SPEC §6) keeps content
|
||||
from relays and from anyone outside the group, not from a member's device: the
|
||||
decrypted payload and the decoded output are in the application's memory. It is not a
|
||||
protected-path robustness level ([SPEC](SPEC.md) §6.5).
|
||||
96
wai/SPEC.md
96
wai/SPEC.md
|
|
@ -86,6 +86,21 @@ OPTIONAL; §5 says which are RECOMMENDED.
|
|||
|
||||
All WAI multi-byte integers are little-endian unless stated otherwise.
|
||||
|
||||
**Reader limits.** A reader MUST NOT refuse a manifest for its length or its nesting
|
||||
while it is at most 134 217 728 bytes (`man_len` ≤ 2²⁷) and its arrays and objects nest
|
||||
at most 127 deep, the manifest object itself counting as one. It MAY refuse a longer or
|
||||
deeper manifest. The length limit holds the largest manifest a conforming layer has: a
|
||||
`prev` ([staged-delivery](extensions/staged-delivery.md) §3) that names a different
|
||||
producer for each of the 2²⁰ units a `WIR1` layer may cover (staged-delivery
|
||||
Appendix A.3, `n_units` ≤ 2²⁰) is about 94 MB. A reader that refuses one past the
|
||||
length limit does so from `man_len` alone, before it reads or requests the manifest
|
||||
(`manifest_too_large`). The reference readers refuse both. The same limits apply to the
|
||||
`WAI2` manifest (§7.1). They bound what a reader must read, not the memory its parse
|
||||
takes, and move no byte of the layout above, which stays frozen (§8). Since a
|
||||
conforming encoder emits only containers every conforming sink can read (§7), a
|
||||
conforming encoder writes no manifest past either limit; the reference writers refuse
|
||||
one past the length limit.
|
||||
|
||||
An envelope delivered over HTTP is one object of the proposed media type
|
||||
`application/wai`, named by its receipt content hash, and fetched whole or, for a
|
||||
`WAI2`, one verified component at a time
|
||||
|
|
@ -604,7 +619,7 @@ registration and makes no conforming envelope non-conforming (§8).
|
|||
|---|---|---|---|
|
||||
| `wai.neural.bmshj2018` | bmshj2018-factorized + rANS (byte-exact CompressAI port) | about 0.26 bpp at 29 dB PSNR (quality 3) | CompressAI |
|
||||
| `wai.neural.mbt2018_mean` | mean-scale hyperprior (Gaussian-conditional entropy) | the conditional-entropy family (arXiv 1809.02736) | `neural/hyperprior.rs` |
|
||||
| `wai.neural.int_synth` | integer fixed-point synthesis (all-i64 transposed-conv, no float/ONNX) | bit-identical on every machine — soundly backs `intent = replicate` | `neural/int_synth.rs` (QAT weights sink-supplied) |
|
||||
| `wai.neural.int_synth` | integer fixed-point synthesis (all-i64 transposed-conv, no float/ONNX) | bit-identical on every machine — soundly backs `intent = replicate` | `int_synth.rs` (integer weights sink-supplied; the reference tools make them by training in float, then quantising after training with per-layer shifts calibrated on measured activations, `tools/wai_int_synth_train.py`. Of the committed vectors only `photo` comes from a trained model, `tools/wai_int_synth_real.py`; `gradient`, `bloom`, `facade` and the cross-architecture golden use seeded integer weights) |
|
||||
| `wai.neural.int_hyper` | integer fixed-point **conditional** codec — whole bitstream→image path integer (integer hyper-decoder σ → integer-σ entropy → integer synthesis) | the integer-entropy contract fully assembled: a PTQ of mbt2018_mean, bit-identical on every machine, whose integer decode CI holds within 1 dB of the same model's float decode on a pinned photograph | payload `WIH1`; `int_hyper_synth.rs` (tables sink-supplied) |
|
||||
| `wai.neural.int_mlicpp` | integer fixed-point **MLIC++ entropy topology** — `K` channel slices of the latent, each decoded as a checkerboard anchor pass then a non-anchor pass, with an entropy network over concat[latent decoded so far, hyper] re-run before every pass to give σ‖μ; hyper-decoder, entropy network and synthesis all integer | the channel-autoregressive × checkerboard entropy model at **decode-equivalence**: the encoder and every decoder compute the same σ, so the same distribution for every element, at any thread count; bit-identical on every machine. The committed conformance vector has synthetic weights, so it demonstrates decode-equivalence and states no quality figure | `int_mlicpp.rs` (one-file model bundle sink-supplied; `tools/wai_int_mlicpp_export.py`) |
|
||||
| `wai.image.jpegai` | JPEG AI (ISO/IEC 6048-1:2025) codestream → RGB — integer entropy front end (mANS, with an integer scale-index derivation) + 16-bit-PTQ integer hyper-decoder, context-model and synthesis networks + dequantisation, latent refinement, post-filters (the eICCI CNN among them) and colour conversion in IEEE-754 float with a fixed evaluation order. Scope: one configuration — 256×256 4:4:4, `model_id` 0 (tools_0), `beta_displacement_log` 0, the high-operation-point synthesis, the eICCI model pair the weight bundle carries, and the other header conditions in [determinism-tiers](extensions/determinism-tiers.md) §5; both decode paths refuse any other codestream. Within it, a sink without the integer weight bundle, or in a non-default floating-point environment, takes a float ONNX decode instead. Only checkpoint constants are sink-supplied | **entropy-consistency** (registry class `NeuralEntropyExact`): the entropy decode, including the scale-index derivation, is integer on both decode paths, so the decoded symbols reproduce on every machine. The exact decoder also pins one reconstruction to RGB, reproducible across machines under the build and floating-point conditions in [determinism-tiers](extensions/determinism-tiers.md) §5–§6 (it checks the floating-point environment before decoding) — a property of that decoder, not of the capability, so `wai.image.jpegai` does not back `replicate` (§7). On one reference codestream the exact decoder stays within max\|Δ\| ≤ 2 of the reference decoder (recorded max\|Δ\| = 1 on ~4.4% of samples); evidence in [determinism-tiers](extensions/determinism-tiers.md) §6. **Downgraded in v0.4.0:** this row previously claimed decode-equivalence, and the registry classed the capability `StandardDefined` | `jpegai_decode.rs` (+ `jpegai_{bitstream,mans,pipeline,synthesis,icci}.rs`) |
|
||||
|
|
@ -628,7 +643,7 @@ registration and makes no conforming envelope non-conforming (§8).
|
|||
| `wai.video.int_hyper` | NEURAL-KEYFRAME inter-frame — the I-frame is the LEARNED integer codec `int_hyper` decoded byte-exact, the P-frames are integer block-MC residuals on top | a learned keyframe you can put in a receipt + integer inter-frames; whole decode integer, byte-identical everywhere | payload `WIV1`, keyframe 1 (a nested `WIH1`); `int_video::decode_pframes_typed` ∘ `int_hyper_synth::decode_conditional_checked` |
|
||||
| `wai.video.manifest` | ATTESTED PER-SESSION MANIFEST — the OTT delivery layer, not a codec: a personalised HLS manifest (ads stitched at `EXT-X-DISCONTINUITY`, blackouts + geo enforced) rendered as a **pure function of the session *class*** `(content, entitlement, geo, blackout set, ad pod, window)` rather than the viewer, and sealed in an Ed25519 receipt that binds the JCP grant (capability, joule ceiling, `funds_ceiling` = ad budget), the composition, the stitched ads and the policy applied. A receipt over a WAI playlist MAY carry `render` and `extras`, the rendering parameters and per-segment lines that playlist adds, both bound through `manifest_hash` and through a render digest the signature covers ([http-delivery](extensions/http-delivery.md) §8.6) | **manifest-equivalence**: `manifest_hash == hash(render(segments))` and `session_class_hash` are recomputable by anyone holding the bytes, so the ads, blackouts and compose-work stop being dashboard claims; class-determinism means one generation serves every viewer of the class, and `uj_per_viewer` is the amortised energy that a 1:1-unique generator cannot report below its per-viewer cost. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_manifest.rs` |
|
||||
| `wai.video.ssai` | ATTESTED SERVER-SIDE AD INSERTION — a **deterministic** ad decision (greedy by price within the avail duration and the funds remaining) plus a per-impression Ed25519 receipt binding the candidate pod, the manifest it belongs to, the served `creative_hash`es and the VAST quartile each reached | **spend-equivalence**: `budget_spent_micros` is recomputed from the *billable* impressions and rejected if it exceeds the grant's `funds_ceiling` — an overspent receipt fails verification; every creative and quartile is checkable, so a buyer and seller reconcile impressions from the same bytes instead of two logs that only a dashboard joins. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_ssai.rs` |
|
||||
| `wai.video.keys` | ATTESTED CONTENT-KEY RELEASE — the DRM licensing decision as a record: every key release, and every refusal, is an Ed25519 receipt binding the KID, the DRM system, the entitlement demanded and the window enforced, with a BLAKE3 `key_commit` proving *which* key was released without carrying it | **decision-equivalence**: `verify()` re-runs the release policy over the receipt's own `(grant capability, entitlement, issued, expiry)` and rejects any misreported outcome, and enforces granted-iff-committed — so "keys went only to entitled, unexpired sessions" is checkable against the signed log, not asserted. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_keys.rs` |
|
||||
| `wai.video.keys` | ATTESTED CONTENT-KEY RELEASE — the DRM licensing decision as a record: every key release, and every refusal, is an Ed25519 receipt binding the KID, the DRM system, the entitlement demanded and the window enforced, with a BLAKE3 `key_commit` proving *which* key was released without carrying it. It is one of Lane A's records (§6.5): it covers the release decision, never a frame the key opens | **decision-equivalence**: `verify()` re-runs the release policy over the receipt's own `(grant capability, entitlement, issued, expiry)` and rejects any misreported outcome, and enforces granted-iff-committed — so "keys went only to entitled, unexpired sessions" is checkable against the signed log, not asserted. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_keys.rs` |
|
||||
| `wai.video.package` | DETERMINISTIC PACKAGING + ATTESTED SEGMENTMAP — a source sliced into content-hashed segments on GOP boundaries; the ordered `(index → content_hash)` map is a signed Merkle commitment, and identical content (ads, bumpers, replays, simulcast overlap) dedups to one stored/encoded copy | **map-equivalence + verifiable dedup**: `segment_map_root` lets a `wai.video.manifest` reference be checked against the package that produced it, and `verify()` recomputes `unique_segments`/`stored_bytes` from the segments so the storage/encode saving cannot be overstated; `uj_per_unique_segment` is the encode-once energy. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable. Version 2 ([packaging](extensions/packaging.md) §9) binds the policy digest and one stage index per component, names each object by its jwp-receipts §2.1 content hash, and covers every media class a packager packs; the string keeps `video` for history | `wai_video_package.rs`, `package.rs` |
|
||||
| `wai.video.channel` | VIRTUAL LINEAR / FAST ASSEMBLY — a continuous schedule of assets over wall-clock time, sliced into attested playback windows; windows chain via `parent_receipt_hash`, so a channel's history is a hash chain of program-as-run records | **schedule-continuity + recomputable program-as-run**: `verify()` rejects a schedule with a gap or overlap (no dead air, no double-booking) and recomputes `aired` from the schedule and the window, so what actually aired is checkable by a licensor, advertiser or regulator against the signed chain rather than an operator-held play-out log. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_channel.rs` |
|
||||
|
||||
|
|
@ -1149,6 +1164,9 @@ capability**. It wraps the payload bytes; the capability still names how
|
|||
to decode the *plaintext* once recovered. A sink decrypts first, then
|
||||
runs capability dispatch (§4) on the plaintext.
|
||||
|
||||
§6.1–§6.4 define that layer. §6.5 sets it beside hardware-protected content: the
|
||||
two lanes protected content travels in, and what a WAI record covers in each.
|
||||
|
||||
### 6.1 Manifest field
|
||||
|
||||
A manifest MAY carry a `security` object:
|
||||
|
|
@ -1240,6 +1258,73 @@ exporter → base key) crates, with browser bindings in
|
|||
`joule-comm-mls-wasm`; `Track::e2e_encrypted` is the switch that selects
|
||||
this path. Live: https://wai.transaction.science/private-channel
|
||||
|
||||
### 6.5 Protected content: two lanes
|
||||
|
||||
Content whose licence requires protection travels in one of two lanes. They keep the
|
||||
content from different parties, and a WAI record covers different things in each. A
|
||||
sink, a receipt, a tier claim or a status text MUST NOT present content in one lane as
|
||||
being in the other.
|
||||
|
||||
**Lane A: hardware-protected content.** The media is a bitstream of a capability whose
|
||||
registration names its standard (§5), encrypted for a platform's protected media path:
|
||||
a content decryption module (in a browser, the one the W3C Encrypted Media Extensions
|
||||
interface reaches) and a decoder whose output the application cannot read. Decryption
|
||||
and decode happen inside that path.
|
||||
|
||||
- This revision registers no `security.encryption` value for Lane A, so no WAI
|
||||
envelope carries Lane A media: it travels in the form its protection system defines,
|
||||
and WAI carries records about it. An envelope that names an encryption the sink does
|
||||
not implement is inert at that sink (§6.4 step 2).
|
||||
- WAI attests about a Lane A title only through its records, among them the manifest
|
||||
a session was served (`wai.video.manifest`), the package's segment map over the bytes
|
||||
it sliced (`wai.video.package`), each key-release decision, which binds the released
|
||||
key by commitment and never carries it (`wai.video.keys`), the ad decision and the
|
||||
per-impression receipts that bind each served `creative_hash` and the quartile it
|
||||
reached (`wai.video.ssai`), a channel's program-as-run windows (`wai.video.channel`),
|
||||
and the content hash of each WAI object delivered (jwp-receipts §2.1). None of them
|
||||
covers a decoded frame or sample.
|
||||
- A capability that synthesises output from parameters (grain, an enhancement layer, a
|
||||
generative stage) MAY apply to Lane A content only inside the protected path, in a
|
||||
decoder that the protection system certifies. WAI does not certify that decoder and
|
||||
cannot read its output, so such synthesis has no tier and no output digest, and a
|
||||
record about it states at most which capability the sink declared it ran.
|
||||
|
||||
**Lane B: end-to-end encryption at the application layer.** The payload is sealed per
|
||||
object under a group key, `mls-sframe` (§6.1–§6.4,
|
||||
[confidentiality](extensions/confidentiality.md) §3–§5). A member's application opens
|
||||
it and runs §4 on the plaintext, so any decoder a sink has can decode it: native code,
|
||||
WebAssembly, or a neural decoder.
|
||||
|
||||
- The sink that holds the key decodes the plaintext, so the capability's tier over its
|
||||
decoded output ([determinism-tiers](extensions/determinism-tiers.md) §2) and the
|
||||
output digests it states in a `reconstruct-session` claim
|
||||
([jwp-receipts](extensions/jwp-receipts.md) §2.4.12) hold as they do for plaintext,
|
||||
and the receipt chain covers the ciphertext (§6.3). Not every record carries over:
|
||||
staged-measurement counts no encrypted object and publishes no byte figure for one
|
||||
([staged-measurement](extensions/staged-measurement.md) §3, *Only a checked format
|
||||
is counted*, code `encrypted`), and a verifier of a `reconstruct-session` claim that
|
||||
holds no key checks none of the rules that need a payload, which stay the signer's
|
||||
statement (jwp-receipts §2.4.12, rule 3). The reference dispatchers implement no
|
||||
scheme (§6.4 step 2), so nothing in the reference states a tier, an output digest or
|
||||
a binding for Lane B content.
|
||||
- **Lane B is not a protected-path robustness level.** It keeps the content from relays
|
||||
and from anyone outside the group (§6.2, §6.3). It does not keep it from a member's
|
||||
device: the base key, the plaintext and the decoded output are in the application's
|
||||
memory, readable by the application and by anything that can read that memory. A
|
||||
publisher MUST NOT offer Lane B to meet a licence term that requires a
|
||||
hardware-protected path, and a sink or a receipt MUST NOT describe Lane B content as
|
||||
hardware-protected.
|
||||
|
||||
**What the exact tier covers.** `decode-equivalence` covers the medium a sink emits and
|
||||
can hash ([determinism-tiers](extensions/determinism-tiers.md) §2), on the fabrics
|
||||
determinism-tiers §5 lists: native CPU and WebAssembly in this revision, with no GPU or
|
||||
NPU decoder. It never covers output the application cannot read. A sink MUST NOT state
|
||||
a tier, an output digest, a reconstruction binding or an energy binding for output
|
||||
decoded inside a protected media path, and a `reconstruct-session` claim
|
||||
(jwp-receipts §2.4.12), whose every presentation carries the SHA-256 of its output,
|
||||
lists no presentation whose output its sink could not hash. In Lane B the tier holds as
|
||||
it does for plaintext; in Lane A there is no output for it to cover.
|
||||
|
||||
---
|
||||
|
||||
## 7. Conformance
|
||||
|
|
@ -1846,7 +1931,7 @@ by which capability happens to be installed.
|
|||
JSON value (whitespace around a member's value is not part of it, so an explode
|
||||
cannot write it back) and nothing follows its payload; a packer refuses any other
|
||||
envelope, and refuses an output that does not explode back to its sources (one
|
||||
whose manifest, nested in the `WAI2` manifest, passes a reader's depth limit). Entry
|
||||
whose manifest, nested in the `WAI2` manifest, passes §2's nesting limit). Entry
|
||||
*i*'s exploded envelope is defined by entry *i* and the entries its `refines` names,
|
||||
so an entry that cannot be exploded leaves every entry that does not name it intact.
|
||||
A tool that re-serialises a `WAI2` manifest may change these bytes, and with them
|
||||
|
|
@ -2238,8 +2323,9 @@ can be called from any language. The crate wraps:
|
|||
- **video**: `rav1e` (AV1 encode) + `dav1d` (AV1 decode).
|
||||
- **text**: `zstd` (libzstd), `xz2` (liblzma).
|
||||
|
||||
Run `cargo test --lib` (with `RUSTFLAGS="-L /opt/homebrew/opt/flac/lib"`
|
||||
on macOS, or via the committed `.cargo/config.toml`).
|
||||
Run `cargo test --lib`. With the `codecs` feature, `wai-rs/build.rs` finds libFLAC
|
||||
through pkg-config (on macOS without pkg-config, in the Homebrew prefix); where it
|
||||
does not, set `RUSTFLAGS="-L <the directory holding libFLAC>"`.
|
||||
|
||||
Decode speed is an implementation property, not a conformance property: the
|
||||
integer decoders run faster kernels (SIMD, threads) built to give the same
|
||||
|
|
|
|||
|
|
@ -768,7 +768,7 @@ def wai2_read(data):
|
|||
if len(data) < 8 or data[:4] != b"WAI2":
|
||||
return None
|
||||
ml = struct.unpack_from("<I", data, 4)[0]
|
||||
if 8 + ml > len(data):
|
||||
if ml > SV.MANIFEST_MAX or 8 + ml > len(data):
|
||||
return None
|
||||
try:
|
||||
m = SV.strict_json(data[8:8 + ml])
|
||||
|
|
@ -1964,6 +1964,9 @@ def main(argv):
|
|||
bm = brackets_match_wai_rs()
|
||||
print(("PASS" if not bm else "FAIL") + " bracket arithmetic equals the literals pinned in wai-rs " + ",".join(bm))
|
||||
fail += bool(bm)
|
||||
rl = SV.reader_limit_failures(lambda b: wai2_read(b) is not None, True)
|
||||
print(("PASS" if not rl else "FAIL") + " wai2_read refuses a manifest past the reader limit (SPEC §2) " + "; ".join(rl))
|
||||
fail += bool(rl)
|
||||
rd = OER.check_canonical_form() # the OER reference's own fixtures, its reading of -0 among them
|
||||
print(("PASS" if not rd else "FAIL") + " the OER reference's canonical form and number reading hold " + ",".join(rd))
|
||||
fail += bool(rd)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@
|
|||
> `moq-sframe` (SFrame object encryption) and `joule-comm-mls` (MLS group
|
||||
> key agreement) crates, with browser bindings in `joule-comm-mls-wasm`.
|
||||
> Live demonstration: https://wai.transaction.science/private-channel
|
||||
>
|
||||
> This is Lane B of SPEC §6.5: end-to-end encryption at the application layer. The
|
||||
> sink that holds the key decodes the plaintext, so the tier and the output digests it
|
||||
> states hold as they do for plaintext; staged-measurement counts no encrypted object,
|
||||
> and a verifier without the key checks none of a claim's rules that need a payload
|
||||
> (SPEC §6.5). It is not a protected-path robustness level.
|
||||
|
||||
Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY**, **REQUIRED**,
|
||||
**OPTIONAL** are RFC 2119 / RFC 8174.
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ tier costs them.
|
|||
|
||||
| capability | hashable medium | how the reconstruction is pinned |
|
||||
|---|---|---|
|
||||
| `wai.neural.int_synth` | RGB | integer synthesis (QAT weights) |
|
||||
| `wai.neural.int_synth` | RGB | integer synthesis (post-training quantisation of a float model, with per-layer shifts calibrated on measured activations; of the committed vectors only `photo` is such a model, and `gradient`, `bloom`, `facade` and the cross-architecture golden of §6 use seeded integer weights) |
|
||||
| `wai.neural.int_hyper` | RGB | integer hyper-decoder, integer scale → distribution step, integer synthesis (post-training quantisation of a float model) |
|
||||
| `wai.video.int_hyper` | per-frame | an `int_hyper` keyframe, then integer motion-compensated P-frames |
|
||||
| `wai.audio.int_codec` | int16 PCM | integer synthesis over a factorized rANS latent |
|
||||
|
|
@ -480,6 +480,39 @@ Parameter transmission pays in bitrate and leaves the decoder's arithmetic free;
|
|||
the integer decode pays in quantisation, compute and model preparation and adds no
|
||||
bits. A capability's tier states which trade it made.
|
||||
|
||||
### Where `decode-equivalence` holds: fabrics, and output a sink cannot read
|
||||
|
||||
The tier is a property of the integer arithmetic a capability's registration fixes,
|
||||
so a fabric meets it when it executes that arithmetic exactly. What this revision
|
||||
shows, by fabric:
|
||||
|
||||
- **CPU, native.** The reference decoders, whose fast kernels give the same integer
|
||||
for every output as the scalar reference kernels (*Compute*, above). The
|
||||
golden-byte suite passes on Linux x86_64, Linux arm64 and macOS arm64 (§6).
|
||||
- **WebAssembly.** The same Rust compiled to wasm32. `wai-web-wasm.yml` builds the
|
||||
browser crate for `wasm32-unknown-unknown` and, under Node, requires the `int_synth`
|
||||
corpus and the committed `WIV1`, `WIH1`, `WIA1` and `WIS1` vectors to decode to the
|
||||
hashes the native decode gives; it passed on the forge for main at `412a08c5`
|
||||
(2026-10-09). The `int_mlicpp` vector decodes to its pinned bytes on
|
||||
`wasm32-wasip2` under wasmtime (§6). The browser crate's release build contains no
|
||||
SIMD instruction: on the source of the change that added this subsection it
|
||||
validated with WebAssembly's SIMD and relaxed-SIMD features disabled
|
||||
(`wasm-tools validate`), which CI does not check. A WebAssembly decoder at this
|
||||
tier MUST NOT use relaxed-SIMD instructions, whose results the WebAssembly
|
||||
specification lets engines choose among, and MUST NOT depend on the bits of a NaN.
|
||||
- **GPU and NPU.** No decoder of these capabilities runs on either in this revision,
|
||||
and the tier is not claimed there. A network evaluated in floating point on a GPU or
|
||||
NPU does not meet it (§1). An integer GPU kernel could; until a run shows its output
|
||||
equal to the reference's, none is claimed.
|
||||
|
||||
The tier covers the medium a sink emits and can hash (§2, *What a tier covers*). A
|
||||
sink that hands a payload to a protected media path whose output the application
|
||||
cannot read, as Lane A of [SPEC](../SPEC.md) §6.5 does, holds no output to hash, and
|
||||
the tier never covers that output: the sink MUST NOT claim the tier for it, or bind a
|
||||
digest, a reconstruction or an energy figure to it. Content that the application
|
||||
decrypts itself, as in Lane B of SPEC §6.5, is decoded like any plaintext, and the
|
||||
tier holds for it as it does for plaintext.
|
||||
|
||||
## 6. Evidence status
|
||||
|
||||
What the repository demonstrates, claim by claim, as of 2026-09-27 (main at
|
||||
|
|
|
|||
|
|
@ -318,7 +318,8 @@ The procedure:
|
|||
16369 otherwise, and request `<cid>.wai.hp`.
|
||||
2. Check each response (§7.1). If `covered_len` exceeds the bytes received, request the
|
||||
rest of the first `covered_len` bytes.
|
||||
3. Verify the head (§6.3). Parse it (`head_malformed`); apply §5.1; refuse a
|
||||
3. Verify the head (§6.3). Parse it (`head_malformed`, which covers a manifest past
|
||||
[SPEC](../SPEC.md) §2's reader limits); apply §5.1; refuse a
|
||||
capability the variant's union does not list (`codecs_undeclared`).
|
||||
4. Select from the head exactly as [SPEC](../SPEC.md) §7.1 selects, deferring only
|
||||
the component-digest test to the bytes. A deployer policy MAY reorder and restrict
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# WAI Extension: Quantum-Operations Attestation (`wai.quantum.calibration` · `wai.quantum.job`)
|
||||
|
||||
Status: **Draft.** Engine: `wai-rs/src/quantum_ops.rs` (feature `quantum_ops`).
|
||||
Status: **Draft.** Engine: `wai-quantum/src/quantum_ops.rs` (feature `quantum_ops`, which `wai-rs` forwards under the same name and re-exports as `wai::quantum_ops`).
|
||||
The **verify-half** of WAI (`wai.asset.provenance`'s lineage, specialized) applied
|
||||
to the quantum processing industry.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
> layout, the gate IR and the arithmetic: `wai.quantum.circuit2` (magic
|
||||
> `WQC2`), for new content, and `wai.quantum.circuit` (magic `WQC1`), frozen
|
||||
> so that existing content reproduces bit for bit. Reference impl: the
|
||||
> `quantum` module of the `wai-quantum` crate, which `wai-rs` re-exports as
|
||||
> `wai::quantum` under its `quantum` feature; corpus: `quantum-conformance/`.
|
||||
> `quantum` feature of `wai-quantum` (`wai_quantum::quantum`), which `wai-rs`
|
||||
> forwards and re-exports as `wai::quantum`; corpus: `quantum-conformance/`.
|
||||
Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||||
|
||||
## 1. Scope and model
|
||||
|
|
@ -508,8 +508,9 @@ It is not a media codec and it is not a route to quantum advantage.
|
|||
|
||||
## 6. Receipt (JWP profile)
|
||||
|
||||
The receipt (`quantum_receipt.rs`, feature `quantum_receipt = ["quantum",
|
||||
"world"]`) is the artifact this whole extension exists to make possible. Its
|
||||
The receipt (`wai-quantum/src/quantum_receipt.rs`, feature `quantum_receipt`,
|
||||
which `wai-rs` forwards under the same name) is the artifact this whole extension
|
||||
exists to make possible. Its
|
||||
design principle: **separate the two halves of the reconstruction's cost so each
|
||||
is trusted the way it honestly can be.**
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ desyncs across machines exactly as a float σ desyncs a learned entropy coder.
|
|||
integer shift). So the entire *decode* path is `i64`/`i128` with no float, and a
|
||||
reconstruction is byte-identical on every machine. Encoder-side float (the SVD,
|
||||
the optimal per-column scale) runs once at encode and never at decode — the same
|
||||
posture as QAT and as `wai.neural.int_hyper`.
|
||||
posture as `wai.neural.int_hyper`, whose float model is quantised once, after
|
||||
training, and never runs in float at decode.
|
||||
|
||||
## 2. Container (`WQTT`)
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,8 @@ lists the cases it must fail, as each patch does: it passes only when `http_veri
|
|||
non-zero exit that names no case, is a failure of the harness. `http_verify.py
|
||||
--mutate NAME` does the same on the Python side (`left-half`, `no-root`,
|
||||
`counter-zero` fail the proof cases; `floor-as-codecs` fails `union_O9`; `blake3-iv`
|
||||
fails the self-test; `base-at-done`, `tick-noop` and `refinement-unclipped` fail the
|
||||
fails the self-test; `manifest-limit-dropped` fails the reader-limit `parse_head`
|
||||
cases; `base-at-done`, `tick-noop` and `refinement-unclipped` fail the
|
||||
`timed_*` cases; `pdt-ignored`, `dseq-ignored`, `parts-after-extinf`, `average-floor`,
|
||||
`render-digest-off`, `name-unchecked`, `chain-unchecked`, `ilog2-no-interp`,
|
||||
`bola-waits`, `unlabelled-as-zero` and `round-half-down` fail the playlist, receipt and
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@
|
|||
{"name":"object_length_short","op":"parse_head","input":{"object":"O3","envelope_len":98614},"expect":{"error":"object_length_mismatch"}},
|
||||
{"name":"head_malformed_magic","op":"parse_head","input":{"object":"O5","patch_object":[[3,"33"]]},"expect":{"error":"head_malformed"}},
|
||||
{"name":"head_malformed_offset","op":"parse_head","input":{"object":"OD","patch_object":[[574,"01"]]},"expect":{"error":"head_malformed"}},
|
||||
{"name":"head_malformed_manifest_too_large_O5","op":"parse_head","input":{"object":"O5","prefix":8,"patch_object":[[4,"01"],[5,"00"],[6,"00"],[7,"08"]]},"expect":{"error":"head_malformed"}},
|
||||
{"name":"parse_head_need_manifest_at_limit_O5","op":"parse_head","input":{"object":"O5","prefix":8,"patch_object":[[4,"00"],[5,"00"],[6,"00"],[7,"08"]]},"expect":{"need":134217740}},
|
||||
{"name":"head_malformed_manifest_too_large_OD","op":"parse_head","input":{"object":"OD","prefix":8,"patch_object":[[4,"01"],[5,"00"],[6,"00"],[7,"08"]]},"expect":{"error":"head_malformed"}},
|
||||
{"name":"parse_head_need_manifest_at_limit_OD","op":"parse_head","input":{"object":"OD","prefix":8,"patch_object":[[4,"00"],[5,"00"],[6,"00"],[7,"08"]]},"expect":{"need":134217738}},
|
||||
{"name":"head_proof_ok_O1","op":"head_proof","input":{"object":"O1"},"expect":{"covered_len":859,"n_cv":0,"envelope_len":859}},
|
||||
{"name":"head_proof_ok_O10","op":"head_proof","input":{"object":"O10"},"expect":{"covered_len":1009,"n_cv":1,"envelope_len":1825}},
|
||||
{"name":"head_proof_ok_O10L1","op":"head_proof","input":{"object":"O10L1"},"expect":{"covered_len":1009,"n_cv":4,"envelope_len":12711}},
|
||||
|
|
|
|||
14
wai/http-conformance/falsify/manifest-limit-dropped.patch
Normal file
14
wai/http-conformance/falsify/manifest-limit-dropped.patch
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# must fail: head_malformed_manifest_too_large_O5 head_malformed_manifest_too_large_OD
|
||||
# a man_len past the reader limit is asked for, not refused (head::parse_head)
|
||||
diff --git a/wai/wai-rs/src/http_binding/head.rs b/wai/wai-rs/src/http_binding/head.rs
|
||||
index 83fe9492..53219b7c 100644
|
||||
--- a/wai/wai-rs/src/http_binding/head.rs
|
||||
+++ b/wai/wai-rs/src/http_binding/head.rs
|
||||
@@ -66,7 +66,6 @@ pub fn parse_head(prefix: &[u8], envelope_len: u64) -> Result<HeadParse, RangeRe
|
||||
_ => return Err(RangeRefusal::HeadMalformed("the magic is neither WAI1 nor WAI2".into())),
|
||||
};
|
||||
let man_len = u32_at(prefix, 4);
|
||||
- crate::container::check_manifest_len(man_len).map_err(RangeRefusal::HeadMalformed)?;
|
||||
let manifest = 8..8 + man_len;
|
||||
let (head_len, components) = match version {
|
||||
Version::Wai1 => {
|
||||
|
|
@ -29,6 +29,7 @@ rebind-sequence live_resequenced
|
|||
key-unchecked live_edge_other_key live_edge_other_key_walked
|
||||
walk-unbounded live_edge_reanchored live_edge_reanchored_then_reloaded live_edge_reanchored_skipping
|
||||
reload-walked live_edge_unchanged_reload live_edge_reanchored_then_reloaded
|
||||
manifest-limit-dropped head_malformed_manifest_too_large_O5 head_malformed_manifest_too_large_OD
|
||||
xml-char-unchecked mpd_rep_id_not_a_char
|
||||
xml-attribute-twice dash_adjacency_attribute_twice
|
||||
dash-start-ignored dash_adjacency_period_start
|
||||
|
|
|
|||
|
|
@ -131,6 +131,10 @@ def parse_head(prefix, n):
|
|||
if magic not in (b"WAI1", b"WAI2"):
|
||||
raise Refusal("head_malformed")
|
||||
ml = u32(prefix, 4)
|
||||
# SPEC §2, *Reader limits*: refused before it is asked for (--mutate
|
||||
# manifest-limit-dropped asks for it).
|
||||
if ml > SV.MANIFEST_MAX and MUTATE != "manifest-limit-dropped":
|
||||
raise Refusal("head_malformed")
|
||||
if magic == b"WAI1":
|
||||
head_len = 12 + ml
|
||||
if len(prefix) < head_len:
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ import _ed25519 as ED # noqa: E402
|
|||
import int_payload_verify as IPV # noqa: E402
|
||||
|
||||
MAX_INTEGER = (1 << 53) - 1
|
||||
# The longest manifest a reader must accept (SPEC §2, *Reader limits*).
|
||||
MANIFEST_MAX = 1 << 27
|
||||
U32 = (1 << 32) - 1
|
||||
# The frames of each piece an encode vector codes: the first ENCODE_FRAMES.
|
||||
ENCODE_FRAMES = 12
|
||||
|
|
@ -1445,7 +1447,7 @@ def read_envelope(b):
|
|||
if len(b) < 8 or b[:4] not in (b"WAI1", b"WAI2"):
|
||||
return None
|
||||
ml = le(b, 4, 4)
|
||||
if 8 + ml > len(b):
|
||||
if ml > MANIFEST_MAX or 8 + ml > len(b):
|
||||
return None
|
||||
man = read_json(b[8:8 + ml])
|
||||
# §6.3: "does not parse" is the typed parse with SPEC §3's members.
|
||||
|
|
@ -1474,6 +1476,25 @@ def read_envelope(b):
|
|||
return (ml, total, rs[0], rs[0].get("capability"), parts) if len(b) == head + total else None
|
||||
|
||||
|
||||
def check_reader_limit(rep):
|
||||
"""SPEC §2, *Reader limits*, checked in memory, since no file in the corpus is past
|
||||
128 MiB: read_envelope reads a WAI1 and a WAI2 whose manifests are 300 bytes, and
|
||||
refuses each with its manifest padded to MANIFEST_MAX + 1 bytes by a member no
|
||||
revision defines, which a reader ignores (SPEC §8), so only the limit refuses it."""
|
||||
for magic, head, rest in (
|
||||
(b"WAI1", b'{"wai":"1.0","media":"text","intent":"replicate","model_requirement":'
|
||||
b'{"capability":"wai.text.zstd"},"conditioning":{"kind":"zstd"},"pad":"',
|
||||
(3).to_bytes(4, "little")),
|
||||
(b"WAI2", b'{"wai":"1.1","media":"text","intent":"replicate","renditions":'
|
||||
b'[{"capability":"wai.text.zstd","kind":"zstd"}],"pad":"',
|
||||
(1).to_bytes(2, "little") + (0).to_bytes(4, "little") + (3).to_bytes(4, "little"))):
|
||||
def env(n):
|
||||
return b"".join([magic, n.to_bytes(4, "little"), head, b"x" * (n - len(head) - 2), b'"}', rest, b"abc"])
|
||||
rep.check(read_envelope(env(300)) is not None, f"reader limit: a {magic.decode()} with a 300-byte manifest is read")
|
||||
rep.check(read_envelope(env(MANIFEST_MAX + 1)) is None,
|
||||
f"reader limit: a {magic.decode()} manifest of {MANIFEST_MAX + 1} bytes, past the limit, is refused")
|
||||
|
||||
|
||||
WIR1_KIND = {"wai.image.int_refine": "rgb8", "wai.video.int_refine": "rgb8", "wai.audio.int_refine": "pcm",
|
||||
"wai.splat.int_refine": "tensor"}
|
||||
|
||||
|
|
@ -3096,6 +3117,7 @@ def main(argv):
|
|||
print(__doc__)
|
||||
return 2
|
||||
rep = verify(argv[1])
|
||||
check_reader_limit(rep)
|
||||
print(f"pack_verify: {rep.checked - rep.failures}/{rep.checked} checks hold")
|
||||
return 0 if rep.failures == 0 else 1
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ U64 = (1 << 64) - 1
|
|||
# that two readers must agree on stays within.
|
||||
DEPTH_LIMIT = 127
|
||||
|
||||
# The longest manifest a reader must accept (SPEC §2, *Reader limits*); the reference
|
||||
# reader refuses a longer one from its length field alone (`manifest_too_large`).
|
||||
MANIFEST_MAX = 1 << 27
|
||||
|
||||
|
||||
class NotJSON(ValueError):
|
||||
pass
|
||||
|
|
@ -251,6 +255,8 @@ def read_wai1(b):
|
|||
if len(b) < 8 or b[:4] != b"WAI1":
|
||||
raise NotWai1("not a WAI1 envelope")
|
||||
ml = struct.unpack_from("<I", b, 4)[0]
|
||||
if ml > MANIFEST_MAX:
|
||||
raise NotWai1("manifest_too_large")
|
||||
if 8 + ml + 4 > len(b):
|
||||
raise NotWai1("truncated manifest")
|
||||
text = b[8:8 + ml]
|
||||
|
|
@ -1169,6 +1175,8 @@ def read_wai2(b):
|
|||
if len(b) < 8 or b[:4] != b"WAI2":
|
||||
raise NotWai2("not WAI2")
|
||||
ml = struct.unpack_from("<I", b, 4)[0]
|
||||
if ml > MANIFEST_MAX:
|
||||
raise NotWai2("manifest_too_large")
|
||||
if 8 + ml > len(b):
|
||||
raise NotWai2("truncated manifest")
|
||||
try:
|
||||
|
|
@ -1201,6 +1209,48 @@ def parse_wai2(b):
|
|||
return text, payloads
|
||||
|
||||
|
||||
def envelope_with_manifest_len(v2, n):
|
||||
"""A WAI1 (or, with `v2`, a WAI2) envelope whose manifest is exactly `n` bytes: a small
|
||||
manifest with one member no revision defines, `pad`, filled to length (a reader
|
||||
ignores such a member, SPEC §8), over a 3-byte payload. Past MANIFEST_MAX, only the
|
||||
reader limit tells it from one that reads."""
|
||||
if v2:
|
||||
head = (b'{"wai":"1.1","media":"text","intent":"replicate","renditions":'
|
||||
b'[{"capability":"wai.text.zstd","kind":"zstd"}],"target":null,"pad":"')
|
||||
rest = struct.pack("<HII", 1, 0, 3)
|
||||
else:
|
||||
head = (b'{"wai":"1.0","media":"text","intent":"replicate","model_requirement":'
|
||||
b'{"capability":"wai.text.zstd"},"conditioning":{"kind":"zstd"},"target":null,"pad":"')
|
||||
rest = struct.pack("<I", 3)
|
||||
return b"".join([b"WAI2" if v2 else b"WAI1", struct.pack("<I", n), head, b"x" * (n - len(head) - 2),
|
||||
b'"}', rest, b"abc"])
|
||||
|
||||
|
||||
def reader_limit_failures(reads, v2):
|
||||
"""SPEC §2, *Reader limits*, checked in memory, since no file in a corpus is past
|
||||
128 MiB: `reads(bytes)` (True when the reader takes the envelope) takes an envelope
|
||||
whose manifest is 300 bytes and refuses the same envelope with a manifest of
|
||||
MANIFEST_MAX + 1 bytes. What failed, as a list of lines."""
|
||||
form = "WAI2" if v2 else "WAI1"
|
||||
out = []
|
||||
if not reads(envelope_with_manifest_len(v2, 300)):
|
||||
out.append(f"{form}: an envelope with a 300-byte manifest is refused")
|
||||
if reads(envelope_with_manifest_len(v2, MANIFEST_MAX + 1)):
|
||||
out.append(f"{form}: a manifest of {MANIFEST_MAX + 1} bytes, past the reader limit, is read")
|
||||
return out
|
||||
|
||||
|
||||
def reads_as(read, refusal):
|
||||
"""`read` as a predicate: True when it returns, False when it raises `refusal`."""
|
||||
def reads(b):
|
||||
try:
|
||||
read(b)
|
||||
return True
|
||||
except refusal:
|
||||
return False
|
||||
return reads
|
||||
|
||||
|
||||
def is_component_id(s):
|
||||
return isinstance(s, str) and 1 <= len(s) <= 64 and all(c in "abcdefghijklmnopqrstuvwxyz0123456789_-" for c in s)
|
||||
|
||||
|
|
@ -2149,6 +2199,10 @@ def main(d):
|
|||
for name in cases:
|
||||
verify_case(os.path.join(d, "cases", name), fails)
|
||||
n_wai2 = verify_wai2(d, fails)
|
||||
for v2, read, refusal in ((False, read_wai1, NotWai1), (True, read_wai2, NotWai2)):
|
||||
for f in reader_limit_failures(reads_as(read, refusal), v2):
|
||||
fails.append(f"reader limit: {f}")
|
||||
print(f"FAIL reader limit: {f}")
|
||||
sched = json.load(open(os.path.join(d, "schedule.json")))
|
||||
for i, t in enumerate(sched["traces"]):
|
||||
p = present(sched["clock"], t["base_ms"], t["layers_ms"], t["lead_ms"])
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""Quantize a synthesis transform to the integer `int_synth.json` spec decoded
|
||||
bit-exactly by `wai-rs/src/neural/int_synth.rs` — the integer-exact `replicate`
|
||||
bit-exactly by `wai-rs/src/int_synth.rs` — the integer-exact `replicate`
|
||||
path.
|
||||
|
||||
`int_synth` runs an all-integer transposed-conv stack (no float, no ONNX), so
|
||||
|
|
@ -8,10 +8,13 @@ synthesis (ConvTranspose2d + ReLU layers) into the integer weights + per-layer
|
|||
requantize shifts the Rust decoder needs.
|
||||
|
||||
IMPORTANT on quality: integer fixed-point inference is bit-exact regardless, but
|
||||
to match the float model's *quality* the model should be **quantization-aware
|
||||
trained** (QAT) for these shifts. A post-hoc round of an arbitrary float model
|
||||
will decode deterministically but may look worse — the determinism is free, the
|
||||
quality is the training step. This tool does the export; QAT is upstream.
|
||||
this tool rounds every layer with ONE fixed shift, so a float model exported here
|
||||
will decode deterministically and may look much worse than it does in float — the
|
||||
determinism is free, the quality is the quantisation step. The working path is
|
||||
`tools/wai_int_synth_train.py` (and `wai_int_synth_real.py`, which wrote the trained
|
||||
corpus vector): float training, then post-training quantisation with each layer's
|
||||
shift derived from its measured activation range. `wai_int_synth_qat.py` is an
|
||||
experimental attempt to train through a fixed shift; no committed model came from it.
|
||||
|
||||
Spec written (matches int_synth.rs):
|
||||
{ "layers": [ { in_ch, out_ch, k, stride, pad, out_pad,
|
||||
|
|
@ -73,7 +76,7 @@ def main() -> int:
|
|||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--w-bits", type=int, default=8)
|
||||
ap.add_argument("--shift", type=int, default=8,
|
||||
help="per-layer requantize right-shift; tune with QAT for quality.")
|
||||
help="per-layer requantize right-shift (one value for every layer).")
|
||||
args = ap.parse_args()
|
||||
|
||||
model = torch.load(args.weights, map_location="cpu", weights_only=False)
|
||||
|
|
@ -93,7 +96,7 @@ def main() -> int:
|
|||
print(f"wrote {len(layers)}-layer integer synthesis → {out} "
|
||||
f"(w_bits={args.w_bits}, shift={args.shift})")
|
||||
print("determinism: bit-exact on any machine by construction; "
|
||||
"quality depends on QAT for these shifts.")
|
||||
"quality depends on the shift; see wai_int_synth_train.py for calibrated shifts.")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
"""Quantization-aware training (QAT) of an integer synthesis for the
|
||||
`wai.neural.int_synth` capability — closes the one open gap: integer-exact
|
||||
decode is already proven (`neural/int_synth.rs`), this makes it look *good*.
|
||||
"""An EXPERIMENTAL quantization-aware training (QAT) attempt for the
|
||||
`wai.neural.int_synth` capability (decoder: `wai-rs/src/int_synth.rs`). No
|
||||
committed model was made with it.
|
||||
|
||||
`int_synth` runs an all-integer transposed-conv stack with a per-layer
|
||||
requantize `>> shift`. The decode is bit-exact on every machine regardless of
|
||||
weights; QAT tunes the weights AND the shifts so the integer reconstruction
|
||||
matches the float teacher. We train with straight-through fake-quant (forward =
|
||||
quantized integers, backward = float gradients) so the network learns weights
|
||||
that survive int8 + the chosen shifts.
|
||||
weights. This tool trains the weights with straight-through fake-quant (forward =
|
||||
quantized integers, backward = float gradients) against ONE fixed shift for every
|
||||
layer; it does not learn the shifts. With a fixed shift, small activations can
|
||||
collapse to 0 (see the comment in `IntSynth.forward`). The working path is
|
||||
`tools/wai_int_synth_train.py`: train in
|
||||
float, then quantise after training with each layer's shift derived from its
|
||||
measured activation range. The trained corpus vector was made that way
|
||||
(`tools/wai_int_synth_real.py`).
|
||||
|
||||
Inputs: paired (latent, target_image) tensors — e.g. dump `(y_hat, x)` from a
|
||||
trained float codec on a dataset, or co-train with an analysis transform. This
|
||||
|
|
@ -114,8 +118,8 @@ def main() -> int:
|
|||
|
||||
out = Path(args.out); out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(net.eval().export_spec(), separators=(",", ":")))
|
||||
print(f"wrote QAT integer synthesis → {out} (shift={args.shift}); "
|
||||
f"decode is bit-exact on any machine by construction.")
|
||||
print(f"wrote an experimental QAT integer synthesis → {out} (shift={args.shift}); "
|
||||
f"decode is bit-exact on any machine by construction, quality is not checked.")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,20 @@
|
|||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/**
|
||||
* The longest manifest, in bytes, a reader must accept (SPEC §2, *Reader limits*):
|
||||
* 2^27 (128 MiB). It is sized to the largest manifest a conforming layer has: a `prev`
|
||||
* (staged-delivery §3) that names a different producer for each of the 2^20 units a
|
||||
* `WIR1` layer may cover (staged-delivery Appendix A.3, `n_units ≤ 2^20`), about 90
|
||||
* bytes a run. The reference readers refuse a longer one before they read it, and the
|
||||
* reference writers refuse to write one, so `man_len` never asks a reader to read
|
||||
* more. It bounds the bytes read, not the memory a parse takes: the reference readers
|
||||
* build a JSON tree, which can take some 40 times the manifest's length. The
|
||||
* manifest's nesting has its own limit: the JSON reader refuses 128 nested arrays and
|
||||
* objects and takes 127 (`check_unique_members`).
|
||||
*/
|
||||
#define MANIFEST_MAX_LEN (1 << 27)
|
||||
|
||||
#define WAI_OK 0
|
||||
|
||||
#define WAI_ERR_INVALID_ARG 1
|
||||
|
|
|
|||
|
|
@ -863,6 +863,16 @@ fn cases(objs: &BTreeMap<String, Vec<u8>>, extra: &mut BTreeMap<String, Vec<u8>>
|
|||
v.push(refused("object_length_short", "parse_head", json!({ "object": "O3", "envelope_len": n("O3") + 1 }), "object_length_mismatch"));
|
||||
v.push(refused("head_malformed_magic", "parse_head", json!({ "object": "O5", "patch_object": [[3, "33"]] }), "head_malformed"));
|
||||
v.push(refused("head_malformed_offset", "parse_head", json!({ "object": "OD", "patch_object": [[574, "01"]] }), "head_malformed"));
|
||||
// SPEC §2, *Reader limits*: a `man_len` of 2^27 + 1 is refused from the first eight
|
||||
// bytes, and one of 2^27 asks for the head it declares.
|
||||
let man_len = |v: u32| json!([[4, format!("{:02x}", v as u8)], [5, format!("{:02x}", (v >> 8) as u8)],
|
||||
[6, format!("{:02x}", (v >> 16) as u8)], [7, format!("{:02x}", (v >> 24) as u8)]]);
|
||||
for id in ["O5", "OD"] {
|
||||
v.push(refused(format!("head_malformed_manifest_too_large_{id}"), "parse_head",
|
||||
json!({ "object": id, "prefix": 8, "patch_object": man_len((1 << 27) + 1) }), "head_malformed"));
|
||||
v.push(case(format!("parse_head_need_manifest_at_limit_{id}"), "parse_head",
|
||||
json!({ "object": id, "prefix": 8, "patch_object": man_len(1 << 27) })));
|
||||
}
|
||||
|
||||
// head_proof: every object, and every way a proof can be wrong.
|
||||
for id in objs.keys() {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ pub const CAP_NEURAL_MIMI: &str = "wai.neural.mimi"; // Mimi, re
|
|||
pub const CAP_NEURAL_WAVTOKENIZER: &str = "wai.neural.wavtokenizer"; // ultra-low bitrate audio
|
||||
pub const CAP_NEURAL_BMSHJ2018: &str = "wai.neural.bmshj2018"; // bmshj2018-factorized image codec (2018 baseline)
|
||||
pub const CAP_NEURAL_MBT2018_MEAN: &str = "wai.neural.mbt2018_mean"; // mean-scale HYPERPRIOR image codec (Gaussian-conditional entropy) — decoder IMPLEMENTED in this crate (neural/hyperprior.rs); the conditional-entropy family: a mean-scale hyperprior conditions each latent's Gaussian; sink-supplied ONNX (hyper_synthesis + synthesis)
|
||||
pub const CAP_NEURAL_INT_SYNTH: &str = "wai.neural.int_synth"; // INTEGER fixed-point synthesis (neural/int_synth.rs): all-i64 transposed-conv decode, no float/ONNX — bit-identical on every machine BY CONSTRUCTION. The first capability that can soundly back intent=replicate cross-hardware (determinism IS integer-exact). Weights are sink-supplied (int_synth.json from a QAT model)
|
||||
pub const CAP_NEURAL_INT_SYNTH: &str = "wai.neural.int_synth"; // INTEGER fixed-point synthesis (int_synth.rs): all-i64 transposed-conv decode, no float/ONNX — bit-identical on every machine BY CONSTRUCTION. The first capability that can soundly back intent=replicate cross-hardware (determinism IS integer-exact). Weights are sink-supplied (int_synth.json; the reference tools train in float, then quantise after training with calibrated per-layer shifts, tools/wai_int_synth_train.py)
|
||||
pub const CAP_NEURAL_INT_HYPER: &str = "wai.neural.int_hyper"; // INTEGER fixed-point CONDITIONAL codec (int_hyper_synth.rs): the full Gaussian-conditional decode integer end-to-end — factorized rANS-decode z (z_q = s·2^afrac + median_q) → integer hyper-decoder h_s → σ,μ → integer-σ bucket (int_entropy) → integer rANS-decode residual → y_hat = r·2^afrac + μ → integer synthesis g_s. Both bitstreams entropy-coded, no raw latents; payload WIH1 (extensions/integer-payloads.md). No float/ONNX anywhere, so the WHOLE bitstream→image decode is bit-identical on every machine BY CONSTRUCTION — int_synth's sibling that adds the conditional entropy. Quality: PTQ of the pretrained mbt2018_mean (quality 6); CI (wai-sota-vectors.yml) requires the integer decode within 1 dB of the same model's float decode on a pinned test photograph — measured cost there 0.19 dB (35.985 vs 36.175 dB, arm64 macOS); image-dependent, see int_hyper_synth.rs. Tables sink-supplied
|
||||
pub const CAP_NEURAL_INT_MLICPP: &str = "wai.neural.int_mlicpp"; // INTEGER MLIC++-topology codec (int_mlicpp.rs): the channel-autoregressive × checkerboard entropy model — K channel slices, each decoded as an anchor pass then a non-anchor pass, with an entropy network over concat[y decoded so far, hyper] giving σ‖μ before every pass — with every decode-side network integer (int_hyper_synth networks, int_entropy bucketing, integer rANS). The encoder and every decoder compute the same σ, so the same bucket, at any thread count: no float/ONNX anywhere, byte-identical on every machine BY CONSTRUCTION. Refuses any stream value outside the exact integer range. Payload (WIM1) and one-file model bundle (WMB1, so a prior pins every network and table) specified in int_mlicpp.rs. Conformance vector tests/vectors/int_mlicpp/ has SYNTHETIC weights (seeded, quantised like a trained model): it shows decode-equivalence, not compression quality. Model sink-supplied
|
||||
pub const CAP_NEURAL_ELIC: &str = "wai.neural.elic"; // checkerboard CONTEXT-MODEL image codec (neural/context.rs) — the JPEG-AI/MLIC++ entropy class: hyperprior + 2-pass spatial context (anchor → context net → non-anchor). Decoder IMPLEMENTED; sink-supplied ONNX (hyper_synthesis + context + synthesis). WAI-native, NOT the ISO JPEG AI bitstream
|
||||
|
|
|
|||
|
|
@ -35,6 +35,75 @@ use crate::codecs::{DeclaredTier, DeterminismTier, ParameterSetShape};
|
|||
pub const MAGIC: &[u8; 4] = b"WAI1"; // v1.0 single-payload
|
||||
pub const MAGIC_V2: &[u8; 4] = b"WAI2"; // v1.1 multi-rendition
|
||||
|
||||
/// The longest manifest, in bytes, a reader must accept (SPEC §2, *Reader limits*):
|
||||
/// 2^27 (128 MiB). It is sized to the largest manifest a conforming layer has: a `prev`
|
||||
/// (staged-delivery §3) that names a different producer for each of the 2^20 units a
|
||||
/// `WIR1` layer may cover (staged-delivery Appendix A.3, `n_units ≤ 2^20`), about 90
|
||||
/// bytes a run. The reference readers refuse a longer one before they read it, and the
|
||||
/// reference writers refuse to write one, so `man_len` never asks a reader to read
|
||||
/// more. It bounds the bytes read, not the memory a parse takes: the reference readers
|
||||
/// build a JSON tree, which can take some 40 times the manifest's length. The
|
||||
/// manifest's nesting has its own limit: the JSON reader refuses 128 nested arrays and
|
||||
/// objects and takes 127 (`check_unique_members`).
|
||||
pub const MANIFEST_MAX_LEN: usize = 1 << 27;
|
||||
|
||||
/// SPEC §2, *Reader limits*: `Err` with the code `manifest_too_large` when a manifest
|
||||
/// of `len` bytes is longer than [`MANIFEST_MAX_LEN`].
|
||||
pub fn check_manifest_len(len: u64) -> Result<(), String> {
|
||||
if len > MANIFEST_MAX_LEN as u64 {
|
||||
return Err(format!(
|
||||
"manifest_too_large: the manifest is {len} bytes; a reader need accept no more than {MANIFEST_MAX_LEN} (SPEC §2)"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What a container writer may write: the longest manifest, and the largest length a
|
||||
/// `u32` field holds. [`Wai::to_bytes`] and [`WaiMulti::to_bytes`] write within
|
||||
/// [`WriteLimits::SPEC`]; a test passes limits it can reach without serialising 128 MiB
|
||||
/// or holding 4 GiB.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct WriteLimits {
|
||||
manifest: usize,
|
||||
field: u32,
|
||||
}
|
||||
|
||||
impl WriteLimits {
|
||||
/// SPEC §2's reader limit for the manifest, and the whole of a `u32`.
|
||||
const SPEC: Self = Self { manifest: MANIFEST_MAX_LEN, field: u32::MAX };
|
||||
}
|
||||
|
||||
/// A manifest's JSON as the writers emit it (no insignificant whitespace, members in
|
||||
/// order), refused with `manifest_too_large` as soon as it would pass `cap` bytes
|
||||
/// ([`MANIFEST_MAX_LEN`] in the writers), so a writer never builds more of it than that.
|
||||
fn manifest_json<T: Serialize>(m: &T, cap: usize) -> std::io::Result<Vec<u8>> {
|
||||
struct Capped(Vec<u8>, usize);
|
||||
impl std::io::Write for Capped {
|
||||
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
|
||||
if self.0.len().saturating_add(b.len()) > self.1 {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!(
|
||||
"manifest_too_large: the manifest passes {} bytes, the most a reader need accept (SPEC §2)", self.1)));
|
||||
}
|
||||
self.0.extend_from_slice(b);
|
||||
Ok(b.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let mut w = Capped(Vec::new(), cap);
|
||||
serde_json::to_writer(&mut w, m).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
Ok(w.0)
|
||||
}
|
||||
|
||||
/// A length a container writes into a `u32` field (SPEC §2, §7.1), or an error
|
||||
/// naming the field when it is past `max` (in the writers, `u32::MAX`: when it does not
|
||||
/// fit).
|
||||
fn u32_field(len: usize, max: u32, what: &str) -> std::io::Result<u32> {
|
||||
u32::try_from(len).ok().filter(|&l| l <= max).ok_or_else(|| std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData, format!("{what} is {len} bytes, past the u32 field that holds it")))
|
||||
}
|
||||
|
||||
fn is_none<T>(v: &Option<T>) -> bool {
|
||||
v.is_none()
|
||||
}
|
||||
|
|
@ -1183,16 +1252,23 @@ impl Wai {
|
|||
Ok(bytes.len())
|
||||
}
|
||||
|
||||
/// The envelope's bytes. Refuses a manifest longer than SPEC §2's reader limit
|
||||
/// (`manifest_too_large`), and a payload whose length does not fit its `u32` field.
|
||||
pub fn to_bytes(&self) -> std::io::Result<Vec<u8>> {
|
||||
self.to_bytes_within(WriteLimits::SPEC)
|
||||
}
|
||||
|
||||
/// [`Self::to_bytes`] within `lim`.
|
||||
fn to_bytes_within(&self, lim: WriteLimits) -> std::io::Result<Vec<u8>> {
|
||||
// No insignificant whitespace, key order preserved (serde_json's
|
||||
// `preserve_order`), so the bytes round-trip identically.
|
||||
let m = serde_json::to_vec(&self.manifest)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
let m = manifest_json(&self.manifest, lim.manifest)?;
|
||||
let payload_len = u32_field(self.payload.len(), lim.field, "the payload")?;
|
||||
let mut out = Vec::with_capacity(12 + m.len() + self.payload.len());
|
||||
out.extend_from_slice(MAGIC);
|
||||
out.extend_from_slice(&(m.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(&m);
|
||||
out.extend_from_slice(&(self.payload.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(&payload_len.to_le_bytes());
|
||||
out.extend_from_slice(&self.payload);
|
||||
Ok(out)
|
||||
}
|
||||
|
|
@ -1220,6 +1296,8 @@ impl Wai {
|
|||
}
|
||||
need(4, 4, "manifest length")?;
|
||||
let ml = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
|
||||
check_manifest_len(ml as u64)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
need(8, ml, "manifest body")?;
|
||||
check_unique_members(&bytes[8..8 + ml])
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
|
|
@ -1520,6 +1598,7 @@ pub fn manifest_bytes(envelope: &[u8]) -> std::io::Result<&[u8]> {
|
|||
return Err(bad("unknown magic (not WAI1/WAI2)".into()));
|
||||
}
|
||||
let ml = u32::from_le_bytes(envelope[4..8].try_into().unwrap()) as usize;
|
||||
check_manifest_len(ml as u64).map_err(bad)?;
|
||||
if 8usize.saturating_add(ml) > envelope.len() {
|
||||
return Err(bad(format!("truncated envelope (manifest body needs {ml} bytes at offset 8, have {})", envelope.len())));
|
||||
}
|
||||
|
|
@ -2470,7 +2549,15 @@ impl WaiMulti {
|
|||
Self::from_bytes(&bytes)
|
||||
}
|
||||
|
||||
/// The envelope's bytes. Refuses a manifest longer than SPEC §2's reader limit
|
||||
/// (`manifest_too_large`), and a payload, or the payloads' total, that does not fit
|
||||
/// a `u32` field.
|
||||
pub fn to_bytes(&self) -> std::io::Result<Vec<u8>> {
|
||||
self.to_bytes_within(WriteLimits::SPEC)
|
||||
}
|
||||
|
||||
/// [`Self::to_bytes`] within `lim`.
|
||||
fn to_bytes_within(&self, lim: WriteLimits) -> std::io::Result<Vec<u8>> {
|
||||
if self.manifest.renditions.len() != self.renditions.len() {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||||
format!("manifest has {} renditions but payload list has {}",
|
||||
|
|
@ -2480,8 +2567,7 @@ impl WaiMulti {
|
|||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||||
"too many renditions for u16 count field"));
|
||||
}
|
||||
let m = serde_json::to_vec(&self.manifest)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
let m = manifest_json(&self.manifest, lim.manifest)?;
|
||||
let table_bytes = 2 + self.renditions.len() * 8;
|
||||
let payload_bytes: usize = self.renditions.iter().map(|p| p.len()).sum();
|
||||
|
||||
|
|
@ -2494,10 +2580,11 @@ impl WaiMulti {
|
|||
// Rendition table: (u32 offset, u32 length); offsets are relative to
|
||||
// the start of the payloads block (the byte right after the table).
|
||||
let mut off: u32 = 0;
|
||||
for p in &self.renditions {
|
||||
for (i, p) in self.renditions.iter().enumerate() {
|
||||
let len = u32_field(p.len(), lim.field, &format!("rendition {i}'s payload"))?;
|
||||
out.extend_from_slice(&off.to_le_bytes());
|
||||
out.extend_from_slice(&(p.len() as u32).to_le_bytes());
|
||||
off = off.checked_add(p.len() as u32).ok_or_else(|| std::io::Error::new(
|
||||
out.extend_from_slice(&len.to_le_bytes());
|
||||
off = off.checked_add(len).filter(|&o| o <= lim.field).ok_or_else(|| std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData, "payload sum overflows u32"))?;
|
||||
}
|
||||
for p in &self.renditions {
|
||||
|
|
@ -2521,6 +2608,8 @@ impl WaiMulti {
|
|||
}
|
||||
need(4, 4, "manifest length")?;
|
||||
let ml = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
|
||||
check_manifest_len(ml as u64)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
need(8, ml, "manifest body")?;
|
||||
check_unique_members(&bytes[8..8 + ml])
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
|
|
@ -4308,4 +4397,116 @@ mod tests {
|
|||
let back: RenditionMeta = serde_json::from_str(&j).unwrap();
|
||||
assert_eq!(DerivedFrom::from_value(back.derived_from.as_ref().unwrap()).unwrap(), d);
|
||||
}
|
||||
|
||||
/// A `WAI1` (or, with `v2`, a `WAI2`) envelope whose manifest is exactly `len`
|
||||
/// bytes: a small manifest with one member no revision defines, `pad`, filled to
|
||||
/// length (a reader ignores such a member, SPEC §8), over a 3-byte payload.
|
||||
fn envelope_with_manifest_len(v2: bool, len: usize) -> Vec<u8> {
|
||||
let head = if v2 {
|
||||
r#"{"wai":"1.1","media":"text","intent":"replicate","renditions":[{"capability":"wai.text.zstd","kind":"zstd"}],"target":null,"pad":""#
|
||||
} else {
|
||||
r#"{"wai":"1.0","media":"text","intent":"replicate","model_requirement":{"capability":"wai.text.zstd"},"conditioning":{"kind":"zstd"},"target":null,"pad":""#
|
||||
};
|
||||
let m = format!("{head}{}\"}}", "x".repeat(len - head.len() - 2));
|
||||
assert_eq!(m.len(), len);
|
||||
let mut b = if v2 { MAGIC_V2.to_vec() } else { MAGIC.to_vec() };
|
||||
b.extend_from_slice(&(m.len() as u32).to_le_bytes());
|
||||
b.extend_from_slice(m.as_bytes());
|
||||
if v2 {
|
||||
b.extend_from_slice(&1u16.to_le_bytes());
|
||||
b.extend_from_slice(&0u32.to_le_bytes());
|
||||
}
|
||||
b.extend_from_slice(&3u32.to_le_bytes());
|
||||
b.extend_from_slice(b"abc");
|
||||
b
|
||||
}
|
||||
|
||||
/// SPEC §2, *Reader limits*, from the length field: eight bytes declaring a manifest
|
||||
/// of exactly `MANIFEST_MAX_LEN` bytes are refused only as truncated, and one byte
|
||||
/// more, or the most a u32 can declare, as too large, by both readers and by
|
||||
/// `manifest_bytes`. A 2 MiB manifest, the size a staged layer's `prev` reaches over
|
||||
/// some 23 000 units with alternating producers, is read and written back byte for
|
||||
/// byte. Both writers stop at their limits: an envelope is written at limits equal to
|
||||
/// its manifest's and its payloads' lengths, and refused at one byte less of either
|
||||
/// (at the real limits the check would serialise 128 MiB, which a debug build takes
|
||||
/// many seconds over, or hold a 4 GiB payload).
|
||||
#[test]
|
||||
fn a_manifest_past_the_reader_limit_is_refused_and_never_written() {
|
||||
assert_eq!(MANIFEST_MAX_LEN, 134_217_728);
|
||||
let too_large = |e: String| assert!(e.starts_with("manifest_too_large:"), "{e}");
|
||||
let truncated = |e: String| assert!(e.contains("truncated"), "{e}");
|
||||
for magic in [MAGIC, MAGIC_V2] {
|
||||
let read = |b: &[u8]| if magic == MAGIC { Wai::from_bytes(b).err() } else { WaiMulti::from_bytes(b).err() }
|
||||
.expect("refused").to_string();
|
||||
let at = [&magic[..], &(MANIFEST_MAX_LEN as u32).to_le_bytes()].concat();
|
||||
truncated(read(&at));
|
||||
truncated(manifest_bytes(&at).unwrap_err().to_string());
|
||||
for declared in [MANIFEST_MAX_LEN as u32 + 1, u32::MAX] {
|
||||
let b = [&magic[..], &declared.to_le_bytes()].concat();
|
||||
too_large(read(&b));
|
||||
too_large(manifest_bytes(&b).unwrap_err().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let len = 2 << 20;
|
||||
let one = envelope_with_manifest_len(false, len);
|
||||
let w = Wai::from_bytes(&one).expect("a 2 MiB manifest is read");
|
||||
assert_eq!(w.to_bytes().unwrap(), one, "and written back byte for byte");
|
||||
let multi = envelope_with_manifest_len(true, len);
|
||||
let w2 = WaiMulti::from_bytes(&multi).expect("a 2 MiB WAI2 manifest is read");
|
||||
assert_eq!(w2.to_bytes().unwrap(), multi, "and written back byte for byte");
|
||||
|
||||
// Each payload is 3 bytes.
|
||||
let lim = |manifest, field| WriteLimits { manifest, field };
|
||||
let past_field = |e: std::io::Error, what: &str| {
|
||||
let e = e.to_string();
|
||||
assert!(e.contains(&format!("{what} is 3 bytes, past the u32 field")), "{e}");
|
||||
};
|
||||
assert_eq!(w.to_bytes_within(lim(len, 3)).unwrap(), one, "written at its own lengths");
|
||||
too_large(w.to_bytes_within(lim(len - 1, 3)).unwrap_err().to_string());
|
||||
past_field(w.to_bytes_within(lim(len, 2)).unwrap_err(), "the payload");
|
||||
assert_eq!(w2.to_bytes_within(lim(len, 3)).unwrap(), multi, "written at its own lengths");
|
||||
too_large(w2.to_bytes_within(lim(len - 1, 3)).unwrap_err().to_string());
|
||||
past_field(w2.to_bytes_within(lim(len, 2)).unwrap_err(), "rendition 0's payload");
|
||||
// A second rendition of 3 bytes: each fits a field of 5, their total does not.
|
||||
let mut w3 = WaiMulti::from_bytes(&multi).unwrap();
|
||||
w3.manifest.renditions.push(w3.manifest.renditions[0].clone());
|
||||
w3.renditions.push(b"def".to_vec());
|
||||
assert!(w3.to_bytes_within(lim(MANIFEST_MAX_LEN, 6)).is_ok());
|
||||
let e = w3.to_bytes_within(lim(MANIFEST_MAX_LEN, 5)).unwrap_err().to_string();
|
||||
assert!(e.contains("payload sum overflows u32"), "{e}");
|
||||
}
|
||||
|
||||
/// The manifest's nesting limit the reference readers apply (SPEC §2, *Reader
|
||||
/// limits*): 127 nested arrays and objects are read, 128 are refused, in `WAI1` and
|
||||
/// `WAI2` manifests alike.
|
||||
#[test]
|
||||
fn a_manifest_nested_past_127_is_refused() {
|
||||
for v2 in [false, true] {
|
||||
let base = envelope_with_manifest_len(v2, 300);
|
||||
let ml = u32::from_le_bytes(base[4..8].try_into().unwrap()) as usize;
|
||||
let text = std::str::from_utf8(&base[8..8 + ml]).unwrap();
|
||||
let rest = &base[8 + ml..];
|
||||
// The manifest object is level 1, so `pad` holding n nested arrays is n + 1.
|
||||
let pad = text.find("\"pad\":").unwrap();
|
||||
let nested = |levels: usize| {
|
||||
let n = levels - 1;
|
||||
let m = format!("{}\"pad\":{}{}}}", &text[..pad], "[".repeat(n), "]".repeat(n));
|
||||
[&base[..4], &(m.len() as u32).to_le_bytes(), m.as_bytes(), rest].concat()
|
||||
};
|
||||
let read = |b: &[u8]| if v2 { WaiMulti::from_bytes(b).map(|_| ()) } else { Wai::from_bytes(b).map(|_| ()) };
|
||||
assert!(read(&nested(127)).is_ok(), "v2={v2}: 127 levels are read");
|
||||
assert!(read(&nested(128)).is_err(), "v2={v2}: 128 levels are refused");
|
||||
}
|
||||
}
|
||||
|
||||
/// A length a container writes into a u32 field must fit it: the writers refuse a
|
||||
/// payload past 2^32 − 1 bytes instead of writing its length modulo 2^32.
|
||||
#[test]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn a_length_past_a_u32_field_is_refused() {
|
||||
assert_eq!(u32_field(u32::MAX as usize, u32::MAX, "x").unwrap(), u32::MAX);
|
||||
let e = u32_field(u32::MAX as usize + 1, u32::MAX, "the payload").unwrap_err().to_string();
|
||||
assert!(e.contains("the payload is 4294967296 bytes, past the u32 field"), "{e}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ fn u32_at(b: &[u8], at: usize) -> u64 {
|
|||
|
||||
/// Parse the head at the start of `prefix`, an object of `envelope_len` bytes, and
|
||||
/// apply §5.1: the object is exactly one envelope. Returns `Need(n)` while `prefix`
|
||||
/// holds fewer than the `n` bytes the head needs.
|
||||
/// holds fewer than the `n` bytes the head needs. A `man_len` past SPEC §2's reader
|
||||
/// limit ([`crate::container::MANIFEST_MAX_LEN`]) is refused from the first eight
|
||||
/// bytes, so `n` never asks for a manifest longer than that.
|
||||
pub fn parse_head(prefix: &[u8], envelope_len: u64) -> Result<HeadParse, RangeRefusal> {
|
||||
let have = prefix.len() as u64;
|
||||
if have < 8 {
|
||||
|
|
@ -64,6 +66,7 @@ pub fn parse_head(prefix: &[u8], envelope_len: u64) -> Result<HeadParse, RangeRe
|
|||
_ => return Err(RangeRefusal::HeadMalformed("the magic is neither WAI1 nor WAI2".into())),
|
||||
};
|
||||
let man_len = u32_at(prefix, 4);
|
||||
crate::container::check_manifest_len(man_len).map_err(RangeRefusal::HeadMalformed)?;
|
||||
let manifest = 8..8 + man_len;
|
||||
let (head_len, components) = match version {
|
||||
Version::Wai1 => {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,24 @@ fn head_parse_needs_more() {
|
|||
assert!(matches!(parse_head(b"WAI3\0\0\0\0", 8), Err(RangeRefusal::HeadMalformed(_))));
|
||||
}
|
||||
|
||||
/// SPEC §2, *Reader limits*: the first eight bytes of a head whose `man_len` passes
|
||||
/// the limit are refused (`head_malformed`), so the head never asks for more than a
|
||||
/// manifest of `MANIFEST_MAX_LEN` bytes; at the limit it asks for the head.
|
||||
#[test]
|
||||
fn head_parse_refuses_a_manifest_past_the_reader_limit() {
|
||||
let max = crate::container::MANIFEST_MAX_LEN as u64;
|
||||
for (magic, head_at_max) in [(b"WAI1", 12 + max), (b"WAI2", 10 + max)] {
|
||||
let at = [&magic[..], &(max as u32).to_le_bytes()].concat();
|
||||
assert_eq!(parse_head(&at, u64::MAX).unwrap(), HeadParse::Need(head_at_max));
|
||||
for declared in [max as u32 + 1, u32::MAX] {
|
||||
let over = [&magic[..], &declared.to_le_bytes()].concat();
|
||||
let e = parse_head(&over, u64::MAX).unwrap_err();
|
||||
assert_eq!(e.code(), "head_malformed");
|
||||
assert!(matches!(&e, RangeRefusal::HeadMalformed(m) if m.starts_with("manifest_too_large:")), "{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_length_rule() {
|
||||
let d = appendix_d();
|
||||
|
|
|
|||
|
|
@ -17,10 +17,13 @@
|
|||
//! Runtime present. The `neural` module re-exposes it through the
|
||||
//! `wai.neural.int_synth` capability.
|
||||
//!
|
||||
//! Quality needs a quantization-aware-trained model (the weights are integers);
|
||||
//! producing that model is a tooling step (`tools/wai_int_synth_export.py`). The
|
||||
//! *decode contract* — the thing that makes cross-hardware bit-exactness real —
|
||||
//! lives here and is exact regardless.
|
||||
//! Quality depends on how the integer weights were made, which is a tooling step
|
||||
//! outside the decode contract. The reference tools train in float, then quantise
|
||||
//! after training, deriving each layer's shift from its measured activation range
|
||||
//! (`tools/wai_int_synth_train.py`, `tools/wai_int_synth_real.py`); the trained
|
||||
//! model in the conformance corpus was made that way. The *decode contract* — the
|
||||
//! thing that makes cross-hardware bit-exactness real — lives here and is exact
|
||||
//! whatever the weights.
|
||||
//!
|
||||
//! Wire format (little-endian):
|
||||
//! u32 H, u32 W, u16 C, u8 S_log2, u8 _reserved (12 B)
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
//! byte-identical on every machine — a portable reconstruction hash you can put
|
||||
//! in a receipt — the same `NeuralIntegerExact` discipline as `int_hyper`, now
|
||||
//! for the quantum-inspired tensor-network transform. Encoder-side float (the
|
||||
//! SVD, the optimal per-column scale) is fine and expected: like QAT it runs
|
||||
//! once at encode and never at decode.
|
||||
//! SVD, the optimal per-column scale) is fine and expected: like the float
|
||||
//! training behind a quantised model, it runs once at encode and never at decode.
|
||||
//!
|
||||
//! This is a **lossy** codec whose decode is **deterministic**. It does not claim
|
||||
//! to recover an arbitrary source losslessly; it claims that whatever it *does*
|
||||
|
|
|
|||
|
|
@ -1118,12 +1118,11 @@ fn snac_real_model_matches_reference() {
|
|||
assert!(maxd < 1e-3, "real snac decode diverges (max {maxd})");
|
||||
}
|
||||
|
||||
/// int_synth against a REAL QAT/PTQ-trained model (not the constructed unit
|
||||
/// test): a float autoencoder trained on synthetic images, then quantized to
|
||||
/// the integer spec with per-layer activation-calibrated shifts. Confirms the
|
||||
/// Rust integer decode is BIT-IDENTICAL to the numpy integer-forward reference
|
||||
/// on a trained model (the determinism guarantee, on real weights). Skipped
|
||||
/// without /tmp artifacts.
|
||||
/// int_synth against a REAL trained model (not the constructed unit test): a float
|
||||
/// autoencoder trained on synthetic images, then quantised to the integer spec with
|
||||
/// per-layer activation-calibrated shifts. Confirms the Rust integer decode is
|
||||
/// BIT-IDENTICAL to the numpy integer-forward reference on a trained model (the
|
||||
/// determinism guarantee, on real weights). Skipped without /tmp artifacts.
|
||||
#[test]
|
||||
fn int_synth_trained_model_matches_integer_forward() {
|
||||
let wai = Path::new("/tmp/test.intsynth.wai");
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
// either role), and still write an envelope that names none. The candidates are
|
||||
// read from the committed export wai/registry/capabilities.json, so a newly
|
||||
// registered candidate is checked without editing this script. They also refuse a
|
||||
// manifest that names a member twice in one object (SPEC §2), which no reader reads.
|
||||
// manifest that names a member twice in one object (SPEC §2), which no reader reads,
|
||||
// and one longer than SPEC §2's reader limit, while writing a long one within it.
|
||||
//
|
||||
// node verify_pack.mjs [path/to/wai_web.js] [path/to/capabilities.json]
|
||||
// The package path is relative to this directory (as in the other verify scripts);
|
||||
|
|
@ -73,6 +74,31 @@ refusesTwice('pack, a member twice in model_requirement',
|
|||
() => wai.pack(v1Text.replace('"capability":"wai.image.png"', '"capability":"wai.image.png","q":1,"q":2'), bytes));
|
||||
refusesTwice('packMulti, a member twice in an entry',
|
||||
() => wai.packMulti(v2([['wai.video.av1']]).replace('"kind":"bytes"', '"kind":"bytes","zz":1,"zz":2'), [bytes]));
|
||||
// SPEC §2, *Reader limits*: a manifest of 3 MiB is written as given; one of 2^27 + 1
|
||||
// bytes, one byte past the limit, is refused (`manifest_too_large`), as every
|
||||
// reference reader refuses it.
|
||||
const MAX = 1 << 27;
|
||||
const LONG = 3 << 20;
|
||||
// `text` with a member `pad` added so that it is exactly `len` bytes (all ASCII).
|
||||
const padTo = (text, len) => text.slice(0, -1) + ',"pad":"' + 'x'.repeat(len - text.length - 9) + '"}';
|
||||
function refusesTooLarge(what, write) {
|
||||
try {
|
||||
write();
|
||||
fail(`${what}: written, but it is past the reader limit`);
|
||||
} catch (e) {
|
||||
if (!String(e.message ?? e).includes('manifest_too_large')) fail(`${what}: refused for another reason: ${e.message ?? e}`);
|
||||
}
|
||||
}
|
||||
const v2Text = v2([['wai.video.av1']]);
|
||||
try {
|
||||
for (const [what, env] of [['pack', wai.pack(padTo(v1Text, LONG), bytes)], ['packMulti', wai.packMulti(padTo(v2Text, LONG), [bytes])]]) {
|
||||
if (new DataView(env.buffer, env.byteOffset, env.byteLength).getUint32(4, true) !== LONG) fail(`${what}: a 3 MiB manifest was not written as given`);
|
||||
}
|
||||
} catch (e) {
|
||||
fail(`a manifest within the reader limit was refused: ${e.message ?? e}`);
|
||||
}
|
||||
refusesTooLarge('pack, a manifest one byte past the limit', () => wai.pack(padTo(v1Text, MAX + 1), bytes));
|
||||
refusesTooLarge('packMulti, a manifest one byte past the limit', () => wai.packMulti(padTo(v2Text, MAX + 1), [bytes]));
|
||||
// An envelope that names no candidate is still written, and parses back.
|
||||
try {
|
||||
const one = wai.pack(v1('wai.image.png', 'wai.image.jpeg'), bytes);
|
||||
|
|
@ -82,6 +108,6 @@ try {
|
|||
} catch (e) {
|
||||
fail(`an envelope with no candidate was refused: ${e.message ?? e}`);
|
||||
}
|
||||
console.log(pass ? `PASS pack and packMulti refuse every candidate (${candidates.join(', ')}) and a member named twice`
|
||||
console.log(pass ? `PASS pack and packMulti refuse every candidate (${candidates.join(', ')}), a member named twice and a manifest past the reader limit`
|
||||
: '\n✗ a browser writer emits what no reader may take');
|
||||
process.exit(pass ? 0 : 1);
|
||||
|
|
|
|||
|
|
@ -164,10 +164,13 @@ Every rung must give the shipped build's outputs. The runners below check this:
|
|||
- the demo verify scripts (`demo/verify_*.mjs`: integer payloads, int_synth, the staged
|
||||
corpus with its guards and fuzz, the menu, the writers, the staged measurement) run
|
||||
against shipped, L1, L2 and J through a generated adapter module. The measurement
|
||||
script's written files must equal the shipped build's.
|
||||
script's written files must equal the shipped build's. They are this tree's scripts,
|
||||
or, with `--demo-commit <commit>`, that commit's scripts over that commit's corpora
|
||||
(its `wai/`, exported with `git archive`). A script this tree runs that the commit
|
||||
lacks is reported as `SKIP`.
|
||||
|
||||
```
|
||||
node wai/wai-web/reach/test/run-modern.mjs --dist <dist> --shipped <package-dir>
|
||||
node wai/wai-web/reach/test/run-modern.mjs --dist <dist> --shipped <package-dir> [--demo-commit <commit>]
|
||||
node wai/wai-web/reach/test/conformance.js --dist <dist> --wai wai --rung J --profile receipt --reference <reference.json>
|
||||
```
|
||||
|
||||
|
|
@ -181,9 +184,13 @@ CI (`.github/workflows/wai-web-reach.yml`, on the `linux` runner) runs a leg per
|
|||
kept rather than rebuilt in CI: the same commit and recipe give a different wasm on a
|
||||
Linux x86_64 host than on the macOS arm64 host that built it (the glue is the same), so
|
||||
the commit's build is checked on that host (the site's `build-wasm-pkg.sh recorded wai`).
|
||||
The demo verify scripts on this leg are that commit's, over that commit's corpora
|
||||
(`run-modern.mjs --demo-commit`): a script a later commit changes checks code the
|
||||
package does not hold. The reach workloads and the engine images are this tree's.
|
||||
- **The tree.** wai-web is built from the pull request's own tree, so a change to wai-web
|
||||
meets the reach builds before the package of record moves to it. These outputs are not
|
||||
the recorded ones.
|
||||
the recorded ones. The demo verify scripts are this tree's, so a check a pull request
|
||||
adds runs here against the code it checks.
|
||||
|
||||
On each leg:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Every rung forced, on a modern Node: the reach conformance at every profile, and the
|
||||
// existing wai-web verify scripts (demo/verify_*.mjs) on every full build.
|
||||
//
|
||||
// node run-modern.mjs --dist <dist> --shipped <package-dir> [--out <dir>]
|
||||
// node run-modern.mjs --dist <dist> --shipped <package-dir> [--out <dir>] [--demo-commit <commit>]
|
||||
//
|
||||
// 1. The shipped package, chosen by the loader itself (it must choose `shipped` here),
|
||||
// runs the workloads; its outputs are the reference.
|
||||
|
|
@ -16,6 +16,15 @@
|
|||
// module; the measurement script's written outputs must equal the shipped build's
|
||||
// file for file.
|
||||
//
|
||||
// The scripts are this tree's by default. With --demo-commit, they and the corpora
|
||||
// they read are that commit's: its wai/, exported with `git archive` into
|
||||
// <out>/demo-tree. A package is held to the scripts of the commit it was built
|
||||
// from, not to checks written after it: a check a later commit adds describes code
|
||||
// the package does not hold. CI's leg on the package of record passes the commit
|
||||
// reach/manifest.json names (input.commit). A script this tree runs that the commit
|
||||
// does not have is reported as SKIP, not as a pass; if the commit has none of them,
|
||||
// the run fails.
|
||||
//
|
||||
// Exit 0 only if every run passes. --out keeps the reference, the logs and a JSON
|
||||
// summary there (default: a temporary folder, removed).
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
|
@ -28,7 +37,7 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|||
const web = resolve(here, '../..');
|
||||
const wai = resolve(web, '..');
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
|
||||
if (!args.dist || !args.shipped) { console.log('usage: node run-modern.mjs --dist <dist> --shipped <package-dir> [--out <dir>]'); process.exit(2); }
|
||||
if (!args.dist || !args.shipped) { console.log('usage: node run-modern.mjs --dist <dist> --shipped <package-dir> [--out <dir>] [--demo-commit <commit>]'); process.exit(2); }
|
||||
const dist = resolve(args.dist), shipped = resolve(args.shipped);
|
||||
const keep = !!args.out;
|
||||
const out = keep ? resolve(args.out) : mkdtempSync(join(tmpdir(), 'wai-reach-'));
|
||||
|
|
@ -36,6 +45,33 @@ mkdirSync(out, { recursive: true });
|
|||
const ref = join(out, 'reference.json');
|
||||
const runs = [];
|
||||
|
||||
// Where step 3's scripts and corpora come from: this tree, or --demo-commit's wai/.
|
||||
let demoWai = wai, demoFrom = 'this tree';
|
||||
if ('demo-commit' in args) {
|
||||
if (!args['demo-commit'] || args['demo-commit'].startsWith('--')) { console.log('FAIL --demo-commit names no commit'); process.exit(2); }
|
||||
// From the top of the repository: run in a subfolder, `git archive` keeps only that
|
||||
// subfolder's part of the tree it is given.
|
||||
const top = spawnSync('git', ['-C', wai, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' }).stdout.trim();
|
||||
const git = (...a) => spawnSync('git', ['-C', top || wai, ...a], { encoding: 'utf8', maxBuffer: 1 << 26 });
|
||||
const rev = git('rev-parse', '--verify', '--quiet', `${args['demo-commit']}^{commit}`);
|
||||
if (rev.status !== 0) {
|
||||
console.log(`FAIL --demo-commit ${args['demo-commit']} is not a commit in this clone (a shallow clone must fetch it first)`);
|
||||
process.exit(1);
|
||||
}
|
||||
const commit = rev.stdout.trim();
|
||||
const root = join(out, 'demo-tree'), tar = join(out, 'demo-tree.tar');
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
mkdirSync(root, { recursive: true });
|
||||
const a = git('archive', '--format=tar', '--prefix=wai/', '-o', tar, `${commit}:wai`);
|
||||
const x = a.status === 0 ? spawnSync('tar', ['-xf', tar, '-C', root], { encoding: 'utf8' }) : a;
|
||||
rmSync(tar, { force: true });
|
||||
if (x.status !== 0) { console.log(`FAIL could not export wai/ at ${commit}: ${(x.stderr || '').trim()}`); process.exit(1); }
|
||||
demoWai = join(root, 'wai');
|
||||
demoFrom = `commit ${commit}`;
|
||||
}
|
||||
const demoWeb = join(demoWai, 'wai-web');
|
||||
console.log(`the demo verify scripts and their corpora: ${demoFrom === 'this tree' ? demoFrom : `wai/ at ${demoFrom}`}`);
|
||||
|
||||
function conformance(label, extra) {
|
||||
const r = spawnSync(process.execPath, [join(here, 'conformance.js'), '--dist', dist, '--wai', wai, '--shipped', shipped, ...extra], { encoding: 'utf8', maxBuffer: 1 << 26 });
|
||||
const text = (r.stdout || '') + (r.stderr || '');
|
||||
|
|
@ -86,24 +122,34 @@ export const { ${names.join(', ')} } = st.api;
|
|||
return file;
|
||||
}
|
||||
const SCRIPTS = [
|
||||
['verify_int_payload_wasm', (p) => [p, join(wai, 'wai-rs/tests/vectors')]],
|
||||
['verify_int_synth_wasm', (p) => [p, join(wai, 'neural-conformance')]],
|
||||
['verify_staged_wasm', (p) => [p, join(wai, 'staged-conformance')]],
|
||||
['verify_menu', (p) => [p, join(wai, 'registry/capabilities.json')]],
|
||||
['verify_pack', (p) => [p, join(wai, 'registry/capabilities.json')]],
|
||||
['verify_measure_wasm', (p, rung) => [p, join(wai, 'staged-measure-conformance'), '--out', join(out, `measure.${rung}`)]],
|
||||
['verify_int_payload_wasm', (p) => [p, join(demoWai, 'wai-rs/tests/vectors')]],
|
||||
['verify_int_synth_wasm', (p) => [p, join(demoWai, 'neural-conformance')]],
|
||||
['verify_staged_wasm', (p) => [p, join(demoWai, 'staged-conformance')]],
|
||||
['verify_menu', (p) => [p, join(demoWai, 'registry/capabilities.json')]],
|
||||
['verify_pack', (p) => [p, join(demoWai, 'registry/capabilities.json')]],
|
||||
['verify_measure_wasm', (p, rung) => [p, join(demoWai, 'staged-measure-conformance'), '--out', join(out, `measure.${rung}`)]],
|
||||
];
|
||||
const absent = SCRIPTS.map(([s]) => s).filter((s) => !existsSync(join(demoWeb, 'demo', `${s}.mjs`)));
|
||||
if (absent.length === SCRIPTS.length) {
|
||||
runs.push({ kind: 'demo', label: 'the demo verify scripts', ok: false, failed: [`${demoFrom} has none of them`] });
|
||||
console.log(`FAIL ${demoFrom} has none of the demo verify scripts`);
|
||||
}
|
||||
for (const rung of ['shipped', 'L1', 'L2', 'J']) {
|
||||
const a = adapter(rung);
|
||||
for (const [script, argv] of SCRIPTS) {
|
||||
const r = spawnSync(process.execPath, [join(web, 'demo', `${script}.mjs`), ...argv(a, rung)], { encoding: 'utf8', cwd: web, maxBuffer: 1 << 26 });
|
||||
if (absent.includes(script)) {
|
||||
runs.push({ kind: 'demo', label: `${rung} ${script}`, ok: true, skipped: `${demoFrom} has no demo/${script}.mjs` });
|
||||
console.log(`SKIP ${rung} demo/${script}.mjs: ${demoFrom} has no such script`);
|
||||
continue;
|
||||
}
|
||||
const r = spawnSync(process.execPath, [join(demoWeb, 'demo', `${script}.mjs`), ...argv(a, rung)], { encoding: 'utf8', cwd: demoWeb, maxBuffer: 1 << 26 });
|
||||
const text = (r.stdout || '') + (r.stderr || '');
|
||||
writeFileSync(join(out, `${script}.${rung}.log`), text);
|
||||
const ok = r.status === 0;
|
||||
runs.push({ kind: 'demo', label: `${rung} ${script}`, ok, failed: ok ? [] : text.split('\n').filter((l) => /FAIL|Error/.test(l)).slice(0, 8) });
|
||||
console.log(`${ok ? 'PASS' : 'FAIL'} ${rung} demo/${script}.mjs${ok ? '' : '\n ' + text.trim().split('\n').slice(-6).join('\n ')}`);
|
||||
}
|
||||
if (rung !== 'shipped') {
|
||||
if (rung !== 'shipped' && !absent.includes('verify_measure_wasm')) {
|
||||
const A = join(out, 'measure.shipped'), B = join(out, `measure.${rung}`);
|
||||
const files = existsSync(A) ? readdirSync(A).sort() : [];
|
||||
const differ = files.filter((f) => !existsSync(join(B, f)) || !readFileSync(join(A, f)).equals(readFileSync(join(B, f))));
|
||||
|
|
@ -114,7 +160,8 @@ for (const rung of ['shipped', 'L1', 'L2', 'J']) {
|
|||
}
|
||||
|
||||
const failed = runs.filter((r) => !r.ok);
|
||||
writeFileSync(join(out, 'summary.json'), JSON.stringify({ engine: `node ${process.version}`, runs }, null, 1) + '\n');
|
||||
console.log(`\n${runs.length - failed.length} of ${runs.length} runs passed on node ${process.version}${keep ? `; logs in ${out}` : ''}`);
|
||||
const skipped = runs.filter((r) => r.skipped);
|
||||
writeFileSync(join(out, 'summary.json'), JSON.stringify({ engine: `node ${process.version}`, demo_scripts: demoFrom, runs }, null, 1) + '\n');
|
||||
console.log(`\n${runs.length - failed.length - skipped.length} of ${runs.length - skipped.length} runs passed${skipped.length ? ` (${skipped.length} skipped)` : ''} on node ${process.version}; demo scripts from ${demoFrom}${keep ? `; logs in ${out}` : ''}`);
|
||||
if (!keep) rmSync(out, { recursive: true, force: true });
|
||||
process.exit(failed.length ? 1 : 0);
|
||||
|
|
|
|||
|
|
@ -515,6 +515,13 @@ fn pack_multi_refusal(manifest: &wai::container::ManifestV2, payloads: &[Vec<u8>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// A length the writers put in a `u32` field (SPEC §2, §7.1), or an error naming the
|
||||
/// field when it does not fit, as `wai-rs`'s writers refuse it. (On wasm32 a `usize`
|
||||
/// is 32 bits, so every length fits; the check holds on any target.)
|
||||
fn u32_field(len: usize, what: &str) -> Result<u32, JsError> {
|
||||
u32::try_from(len).map_err(|_| JsError::new(&format!("{what} is {len} bytes, past the u32 field that holds it")))
|
||||
}
|
||||
|
||||
/// Build a WAI envelope from a manifest JSON string + a payload byte
|
||||
/// array. Mirrors `wai_envelope_pack` from the native SDK so JS code
|
||||
/// can produce .wai files in the browser too: like it, refuses a manifest
|
||||
|
|
@ -522,17 +529,20 @@ fn pack_multi_refusal(manifest: &wai::container::ManifestV2, payloads: &[Vec<u8>
|
|||
/// encoder MUST NOT emit (SPEC §5, *Candidate capabilities*), and a payload that is
|
||||
/// not the format SPEC §5 pins its integer capability or fallback to. The manifest is
|
||||
/// written as given, so a manifest that names a member twice in one object is refused
|
||||
/// (SPEC §2), as every reader refuses it.
|
||||
/// (SPEC §2), as every reader refuses it, and so is one longer than SPEC §2's reader
|
||||
/// limit (`manifest_too_large`).
|
||||
#[wasm_bindgen(js_name = pack)]
|
||||
pub fn pack(manifest_json: &str, payload: &[u8]) -> Result<Box<[u8]>, JsError> {
|
||||
wai::container::check_manifest_len(manifest_json.len() as u64).map_err(|e| JsError::new(&e))?;
|
||||
let m: wai::container::Manifest = read_manifest(manifest_json).map_err(|e| JsError::new(&e))?;
|
||||
pack_refusal(&m, payload).map_err(|e| JsError::new(&e))?;
|
||||
let mb = manifest_json.as_bytes();
|
||||
let payload_len = u32_field(payload.len(), "the payload")?;
|
||||
let mut out = Vec::with_capacity(12 + mb.len() + payload.len());
|
||||
out.extend_from_slice(wai::container::MAGIC);
|
||||
out.extend_from_slice(&(mb.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(mb);
|
||||
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(&payload_len.to_le_bytes());
|
||||
out.extend_from_slice(payload);
|
||||
Ok(out.into_boxed_slice())
|
||||
}
|
||||
|
|
@ -672,9 +682,11 @@ pub fn parse_multi(bytes: &[u8]) -> Result<WaiMulti, JsError> {
|
|||
/// payloads. The manifest is written as given. Like `wai_envelope_pack_multi`,
|
||||
/// refuses a table with a component, in either role, whose capability is a
|
||||
/// candidate, or whose payload is not the format SPEC §5 pins its integer capability
|
||||
/// to (SPEC §5).
|
||||
/// to (SPEC §5), and a manifest longer than SPEC §2's reader limit
|
||||
/// (`manifest_too_large`).
|
||||
#[wasm_bindgen(js_name = packMulti)]
|
||||
pub fn pack_multi(manifest_json: &str, payloads: js_sys::Array) -> Result<Box<[u8]>, JsError> {
|
||||
wai::container::check_manifest_len(manifest_json.len() as u64).map_err(|e| JsError::new(&e))?;
|
||||
let manifest: wai::container::ManifestV2 = read_manifest(manifest_json).map_err(|e| JsError::new(&e))?;
|
||||
wai::container::check_component_candidates(manifest.renditions.iter().map(|r| r.capability.as_str()))
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
|
@ -705,10 +717,11 @@ pub fn pack_multi(manifest_json: &str, payloads: js_sys::Array) -> Result<Box<[u
|
|||
out.extend_from_slice(mb);
|
||||
out.extend_from_slice(&(n as u16).to_le_bytes());
|
||||
let mut off: u32 = 0;
|
||||
for p in &raw_payloads {
|
||||
for (i, p) in raw_payloads.iter().enumerate() {
|
||||
let len = u32_field(p.len(), &format!("rendition {i}'s payload"))?;
|
||||
out.extend_from_slice(&off.to_le_bytes());
|
||||
out.extend_from_slice(&(p.len() as u32).to_le_bytes());
|
||||
off = off.checked_add(p.len() as u32).ok_or_else(||
|
||||
out.extend_from_slice(&len.to_le_bytes());
|
||||
off = off.checked_add(len).ok_or_else(||
|
||||
JsError::new("payload sum overflows u32"))?;
|
||||
}
|
||||
for p in &raw_payloads {
|
||||
|
|
|
|||
Loading…
Reference in a new issue