wai: the browser sink — contentBinding on decoded frames, and browser negotiation rebuilt (supersedes #54) #413

Closed
dcharlot wants to merge 0 commits from wai/browser-binding into main
Owner

What this is

The browser sink, as one new extension and its code: how a sink in a web page answers SPEC §4's "the sink advertises cap", and what the frame it shows can be checked against.

  • wai/extensions/browser-sink.md (new). §4 routes each object to the user agent's decoder, a native host's or WAI's wasm decoder. The user agent is asked only about codec strings the WebCodecs Codec Registry (Registry Draft, 24 September 2026) lists, so AV2, VVC and IAMF are never put to isConfigSupported and an answer about them is never read. The UA decoder is taken only where it is exact for the tier the object needs; wasm is the fallback. §5 evaluates capability-constraints' sets in the page. §6 reports a measured figure per decode beside Media Capabilities' powerEfficient, which stays the user agent's word. §7–§10 define contentBinding { alg, digest, receipt?, tier } on every decoded frame, its life through clone(), transfer, copyTo() and re-encode, the side table a page keeps until a registry lists the member, and the state of the frame on screen. §11 is the reasons table.
  • wai/proposals/webcodecs-content-binding.md (new; draft, not filed). The registry requirements read against the proposal, a draft registry row, draft IDL and semantics, the issue text ready to paste, what was tested, and what is open. Filing is the owner's decision.
  • capability-constraints.md §3.1: the wai: parameter register the extension needed and never had, and a pointer to its evaluation (browser-sink §5).
  • wai-rs: module browser_sink (feature browser_sink = jwp_hash + ed25519; builds for wasm32), corpus generator wai_browser_sink_vectors, tests/browser_sink.rs.
  • Corpus wai/live-conformance/browser/ (vectors.json, int_motion.wai, README with **Provenance.**). It holds routes (energy entries among them), constraint sets, bindings, frame tiers, a chain of hand-signed legacy group receipts whose objects bind, frame states and side-table sequences. The second implementation is browser_sink_verify.py (python3 -I, standard library). It reads §4.1, §4.2, §11, §2's tier table and capability-constraints §3.1 from the texts, and each capability's class from registry/capabilities.json.
  • wai-live-web: src/browser.rs exports routeReport, evaluateConstraints, readBinding, makeBinding, frameTier and frameState. With the new feature int_video it also exports decodeBound, the wasm-route decoder: it verifies a wai.video.int_motion envelope against its group and object receipts (jwp_delivery), decodes it, and binds every frame. An object that does not verify is not decoded. One decoded without receipts is left unbound. Native tests hold these functions to the corpus.
  • joule-moq-web:
    • The verifier's object() now also returns content_hash and group_receipt (the link hash).
    • web/room/binding.js (new) holds the side table, the two-pass capability report and the state helpers.
    • The room routes each track before decoding it, binds every frame its video decoders output, and shows each tile's state with ?debug=binding.
    • web/binding/ is the test page.
    • e2e/energy.py reads the host's per-process counter.
    • e2e/run.sh builds wai_live_web_decode and runs web/room/binding.test.mjs.
    • The e2e scenario binding is in the default list.
  • Registry: contentBinding is a wire identifier of the new kind frame-metadata, status proposed. The export is regenerated and registry export --check passes.
  • SPEC §4 paragraph for a sink in a web page. CHANGELOG entry. README of joule-moq-web and the live-conformance index.
  • CI: a wai-conformance.yml step "browser sink · wai-rs, wai-live-web and browser_sink_verify.py", with paths for the two extensions and wai/wai-live-web/**. joule-moq-web.yml now watches wai/live-conformance/browser/** and wai/wai-rs/tests/vectors/int_video/**, and its header describes the scenario.

PR #54: what was kept, changed and dropped (close #54 as superseded)

#54's branch was not merged. Its intent is rebuilt on current main inside browser-sink.md.

  • Kept
    • A browser sink answers §4's "advertises cap" with the platform's own question first (isConfigSupported) and WAI's wasm decoders as the fallback, which depends on nothing the device ships.
    • #54's reconciliation with capability-constraints is now §4.5. isConfigSupported already does a pre-flight, per-configuration constraint check: it answers "unsupported", never a decode error. A page SHOULD configure the decoder with the configuration the answer returns.
    • The stated limit: the WebCodecs vocabulary is conventional media only. The neural, quantum, world and haptic families have no UA tier, and only WAI's constraint sets constrain them.
    • A SPEC §4 paragraph pointing at the browser mapping.
  • Changed
    • Informative became normative. #54 was a mapping with "SHOULD resolve"; the route is now a deterministic function of a capability report, with a reason for every refusal, a corpus and a second implementation.
    • The resolution order. #54 had native → wasm → fallback. It is now UA → host → wasm, and the UA leg is taken only where the decoder is exact for the object's need (the tier from its pin or the track's wai_determinism). A need of decode-equivalence takes the UA decoder only after a probe matches published frame digests (§4.4).
    • The UA table is gated by the Codec Registry (2026-09). #54 listed AV1 and Opus with no registry check; AV2, VVC and IAMF now never reach the user agent.
    • #54 said the wasm decode is "byte-identical"; that now holds only through the registered tier. A wasm route carries the capability's registered tier, so it is decode-equivalence only for StandardDefined and NeuralIntegerExact capabilities.
    • capability-constraints' evaluation, spec-only in #54's day, now has a register and code.
  • Dropped
    • ImageDecoder rows for wai.image.png/jpeg/jxl/avif: a user agent's image decode may apply colour conversion and does not say so.
    • The claim that a browser sink "MAY carry wasm decoders for PNG/FLAC/zstd" for the classical floor: no page here ships them, and a page now lists what its wasm decodes in the report's wasm.
    • The file name webcodecs-negotiation.md, #54's CHANGELOG entry, and its §6 framing.

Decisions the lead should know

  1. The extension's home. I made it a new extension rather than an addition to http-delivery §12. The room is MoQ and the browser sink is transport-independent; http-delivery §12.1's via is filled from the route (§4.6), and http-delivery is not edited.
  2. What digest and receipt name. digest is jwp-receipts §2.1's content hash of the receipted object. In an E2EE room that is the sealed object the receipt signs, not the plaintext decoded from it. Over MoQ, digest and receipt equal the WAI_RECEIPT property's content_hash and group_receipt.
  3. StandardDefined is read as decode-equivalence for this extension's "registered tier", because SPEC §7 gives it the same property. GenerativeExact is held at none. The text says this is a reading. In the room, VP9 frames get tier none because no probe ran, and that is shown as-is.
  4. A frame is bound only when every object its decoder was given since its last key frame was established. Predicted frames depend on their references. The room verifies before decoding, so it meets this by construction.
  5. The energy classes. The brief says "capability-constraints' energy classes", but that extension has none (its §4 says it is not an energy mechanism). I used energy-measurement §2's classes, and capability-constraints is left unchanged on energy.
  6. Reason::ALL became Reason::all(). A pub const array of a type named Reason in a new module made cbindgen add four opaque typedef struct Reason Reason; to include/wai.h, one for each module's Reason. I bisected it on the build host. With the function, a fresh generation of wai.h is byte-identical to main's.
  7. A separate decode wasm build (wai_live_web_decode), so the room's own wai_live_web is unchanged in what it carries.
  8. No consent rule. §13 states what a report reveals and points a service that collects reports to jwp-receipts' lawful-basis claim. It sets no rule for the user.
  9. JSON integers in a JS page. A page cannot keep 1920.0 apart from 1920, so the corpus's malformed-bound case is a true fraction (1920.5). §5 says so.

Findings from the browser

  • The user agent drops the unregistered member. It is the one the e2e drives, headless; its version is in the run's log. metadata() was empty for a frame constructed with init.metadata = { contentBinding, rtpTimestamp }, through both the buffer constructor and the frame constructor, and after clone(). The registered rtpTimestamp was dropped the same way. The proposal's issue text states this.
  • Energy. On the build host, decoding 640×360 VP9 through the user agent's decoder measured 816–858 µJ per decode across eight runs (840, 838, 816, 857, 858, 851, 836 and 839). Each run is about 36 000 decodes over about 14 s, taken as the marginal over idle windows of the same length. It is OnChipCounter with filtering Unknown and coverage Partial, because the per-process counter covers CPU energy only (plus billed and less serviced). The user agent's powerEfficient beside it was true every time. Each run declares a type-A standard uncertainty of 0.3–2.8 % from its own windows, but the spread between runs is about 5 %. The declared figure therefore understates what a repeat would see. A meter that publishes such figures should add a between-run component; the e2e only checks that a figure is carried with its class beside the boolean.

Checks

check command result
browser_sink unit tests cargo test --locked --no-default-features --features browser_sink --lib browser_sink:: (rb) 12 passed
integration test cargo test --locked --no-default-features --features browser_sink,receipt_claims --test browser_sink (rb) 6 passed
corpus regenerates wai_browser_sink_vectors verify ../live-conformance/browser (rb) PASS ×3
second implementation python3 -I live-conformance/browser_sink_verify.py live-conformance/browser 101 cases PASS; unregistered answers changed 3 ways never read; 28 of 28 mutations fail
wai-live-web native cargo test --locked --lib and --features int_video --lib browser_tests (rb) 5 passed; 3 passed (every chain object's frames hash to sha256.json)
joule-moq-web cargo test --locked -p joule-moq-web --lib (rb) 8 passed
side table in node node --test web/room/binding.test.mjs 12 passed
registry wai registry export then --check; cargo test --lib codecs::registry (rb) up to date; 14 passed
header fresh cbindgen run (touch src/container.rs, build) then git diff include/wai.h (rb) unchanged
clippy --all-targets on wai-rs (browser_sink,receipt_claims), wai-live-web (± int_video), joule-moq-web (rb) zero notes in the new code (joule-moq's and int_payload's notes are main's)
full browser e2e e2e/run.sh on the build host, installed browser 188 of 188 checks, every scenario
binding e2e, looped e2e/run.sh --only binding --runs 3, then --runs 2 on the final tree 3 of 3 runs, 90 of 90 checks; 2 of 2, 60 of 60
repo checks check-tests-in-ci.py, check-workflow-concurrency.sh, check-spec-refs.py, check-wai-section-refs.py, check-no-vanity-metrics.sh, test-public-text-checks.sh, check-comparative-prose.sh, check-orphan-sources.py all exit 0
public names check-public-names.sh over wai/, and file by file over every file this branch changes every changed file clean. The hits are main's: live_provenance.rs ×3 and the existing BROWSER_BIN path in joule-moq-web.yml
after merging origin/main (#397, #399, #403) registry export --check, codecs::registry, browser_sink lib and --test browser_sink, int_kernels_vectors every_decoded_output_states_a_sha256, corpus_provenance, the verifier, the repo checks (rb and local) up to date; all pass. The conflicts were in five files where both sides added (registry wire identifiers, the live-conformance index, the digest-exclusion reason, the CHANGELOG, the workflow), and each kept both

Falsifications (each committed first, broken on the build host or locally, then restored)

  • Rust (wai_browser_sink_vectors verify). Each of these fails (exit 101 from the generator's own asserts, or exit 1 with FAIL lines), and the restored code exits 0:
    • registry gate removed;
    • a timestamp given twice not struck;
    • refused provenance ignored;
    • zero-with-class read as a figure;
    • probe not required;
    • powerEfficient stated off the UA route;
    • receipt dropped from the canonical form.
    • Also, a reason row dropped from §11 fails the_extension_lists_the_reasons.
  • Python (browser_sink_verify.py). Each fails with exit 1, and the restored verifier exits 0:
    • gate dropped;
    • provenance ignored;
    • side table never strikes;
    • an unregistered answer read.
    • The signature check failed no case at first: every signature mutation was also caught by the canonical bytes. I added a mutation that forges a signature and makes the canonical bytes, link hash and binding all agree, and dropping the signature check now fails it (27 of 28, exit 1).
  • JS (binding.test.mjs): with the side table never striking, 2 tests fail.
  • Browser e2e (--only binding), each against an otherwise unchanged tree:
    • the page asks about every codec string: exit 1, and the check "no codec string outside the WebCodecs Codec Registry…" FAILs;
    • the room enters no binding for its chunks: exit 1, and the tile never shows a bound, verified frame (the wait times out);
    • the wasm decoder binds an object that did not verify: exit 1. The changed object is then decoded, its payload no longer decodes, and the page fails at "an object changed after it was receipted is refused and not decoded" and the checks after it.

Deploy note

The live room must be redeployed as one unit after this lands: page assets, relay and room service from one commit. The page assets are web/room/ (with the new binding.js), both rebuilt wasm (joule_moq_web, whose verifier result changed, and wai_live_web, now with browser_sink), and web/binding/ with wai_live_web_decode if /binding/ is served. Nothing was deployed.

Not done

  • No probe objects for UA codecs are published. A need of decode-equivalence therefore never routes to a UA decoder (§4.4 says so), and the room's VP9 frames are tier none.
  • No page implements the host route. It is in the spec and the corpus only.
  • C2's live-provenance verdict enters §10 as an input, and the corpus covers accepted, refused, missing and not_applicable. The room's tracks are WAI-packaged, so their provenance is not_applicable (live-provenance §1). No page verifies a CMAF provenance track in the browser yet.
  • No binding for AudioData, which has no metadata dictionary. Frames decoded for a late joiner's FETCH and for DVR playback are not bound.
  • The energy figure is measured only where the host has a per-process counter (macOS here). Elsewhere the e2e step is reported skipped, not passed.
  • The WebCodecs issue is not filed.
  • wai-live-web's pre-existing native tests (compose and fixture) are still in no workflow; this PR adds only its browser_tests to CI.

🤖 Generated with Claude Code


Closed: Landed in #428 (merge train, merged as aa61ab3b with all 65 checks green).

## What this is The browser sink, as one new extension and its code: how a sink in a web page answers SPEC §4's "the sink advertises `cap`", and what the frame it shows can be checked against. - **`wai/extensions/browser-sink.md`** (new). §4 routes each object to the user agent's decoder, a native host's or WAI's wasm decoder. The user agent is asked only about codec strings the WebCodecs Codec Registry (Registry Draft, 24 September 2026) lists, so AV2, VVC and IAMF are never put to `isConfigSupported` and an answer about them is never read. The UA decoder is taken only where it is exact for the tier the object needs; wasm is the fallback. §5 evaluates capability-constraints' sets in the page. §6 reports a measured figure per decode beside Media Capabilities' `powerEfficient`, which stays the user agent's word. §7–§10 define **`contentBinding`** `{ alg, digest, receipt?, tier }` on every decoded frame, its life through `clone()`, transfer, `copyTo()` and re-encode, the side table a page keeps until a registry lists the member, and the state of the frame on screen. §11 is the reasons table. - **`wai/proposals/webcodecs-content-binding.md`** (new; **draft, not filed**). The registry requirements read against the proposal, a draft registry row, draft IDL and semantics, the issue text ready to paste, what was tested, and what is open. Filing is the owner's decision. - **`capability-constraints.md` §3.1**: the `wai:` parameter register the extension needed and never had, and a pointer to its evaluation (browser-sink §5). - **`wai-rs`**: module `browser_sink` (feature `browser_sink` = `jwp_hash` + `ed25519`; builds for wasm32), corpus generator `wai_browser_sink_vectors`, `tests/browser_sink.rs`. - **Corpus** `wai/live-conformance/browser/` (`vectors.json`, `int_motion.wai`, README with `**Provenance.**`). It holds routes (energy entries among them), constraint sets, bindings, frame tiers, a chain of hand-signed legacy group receipts whose objects bind, frame states and side-table sequences. The second implementation is **`browser_sink_verify.py`** (`python3 -I`, standard library). It reads §4.1, §4.2, §11, §2's tier table and capability-constraints §3.1 from the texts, and each capability's class from `registry/capabilities.json`. - **`wai-live-web`**: `src/browser.rs` exports `routeReport`, `evaluateConstraints`, `readBinding`, `makeBinding`, `frameTier` and `frameState`. With the new feature `int_video` it also exports `decodeBound`, the wasm-route decoder: it verifies a `wai.video.int_motion` envelope against its group and object receipts (`jwp_delivery`), decodes it, and binds every frame. An object that does not verify is not decoded. One decoded without receipts is left unbound. Native tests hold these functions to the corpus. - **`joule-moq-web`**: - The verifier's `object()` now also returns `content_hash` and `group_receipt` (the link hash). - `web/room/binding.js` (new) holds the side table, the two-pass capability report and the state helpers. - The room routes each track before decoding it, binds every frame its video decoders output, and shows each tile's state with `?debug=binding`. - `web/binding/` is the test page. - `e2e/energy.py` reads the host's per-process counter. - `e2e/run.sh` builds `wai_live_web_decode` and runs `web/room/binding.test.mjs`. - The e2e scenario `binding` is in the default list. - **Registry**: `contentBinding` is a wire identifier of the new kind `frame-metadata`, status `proposed`. The export is regenerated and `registry export --check` passes. - **SPEC §4** paragraph for a sink in a web page. **CHANGELOG** entry. README of `joule-moq-web` and the `live-conformance` index. - **CI**: a `wai-conformance.yml` step "browser sink · wai-rs, wai-live-web and browser_sink_verify.py", with paths for the two extensions and `wai/wai-live-web/**`. `joule-moq-web.yml` now watches `wai/live-conformance/browser/**` and `wai/wai-rs/tests/vectors/int_video/**`, and its header describes the scenario. ## PR #54: what was kept, changed and dropped (close #54 as superseded) #54's branch was not merged. Its intent is rebuilt on current main inside `browser-sink.md`. - **Kept** - A browser sink answers §4's "advertises `cap`" with the platform's own question first (`isConfigSupported`) and WAI's wasm decoders as the fallback, which depends on nothing the device ships. - #54's reconciliation with capability-constraints is now §4.5. `isConfigSupported` already does a pre-flight, per-configuration constraint check: it answers "unsupported", never a decode error. A page SHOULD configure the decoder with the configuration the answer returns. - The stated limit: the WebCodecs vocabulary is conventional media only. The neural, quantum, world and haptic families have no UA tier, and only WAI's constraint sets constrain them. - A SPEC §4 paragraph pointing at the browser mapping. - **Changed** - Informative became normative. #54 was a mapping with "SHOULD resolve"; the route is now a deterministic function of a capability report, with a reason for every refusal, a corpus and a second implementation. - The resolution order. #54 had native → wasm → fallback. It is now UA → host → wasm, and the UA leg is taken only where the decoder is exact for the object's need (the tier from its pin or the track's `wai_determinism`). A need of `decode-equivalence` takes the UA decoder only after a probe matches published frame digests (§4.4). - The UA table is gated by the Codec Registry (2026-09). #54 listed AV1 and Opus with no registry check; AV2, VVC and IAMF now never reach the user agent. - #54 said the wasm decode is "byte-identical"; that now holds only through the registered tier. A wasm route carries the capability's registered tier, so it is `decode-equivalence` only for `StandardDefined` and `NeuralIntegerExact` capabilities. - capability-constraints' evaluation, spec-only in #54's day, now has a register and code. - **Dropped** - `ImageDecoder` rows for `wai.image.png/jpeg/jxl/avif`: a user agent's image decode may apply colour conversion and does not say so. - The claim that a browser sink "MAY carry wasm decoders for PNG/FLAC/zstd" for the classical floor: no page here ships them, and a page now lists what its wasm decodes in the report's `wasm`. - The file name `webcodecs-negotiation.md`, #54's CHANGELOG entry, and its §6 framing. ## Decisions the lead should know 1. **The extension's home.** I made it a new extension rather than an addition to http-delivery §12. The room is MoQ and the browser sink is transport-independent; http-delivery §12.1's `via` is filled from the route (§4.6), and http-delivery is not edited. 2. **What `digest` and `receipt` name.** `digest` is jwp-receipts §2.1's content hash of the *receipted* object. In an E2EE room that is the sealed object the receipt signs, not the plaintext decoded from it. Over MoQ, `digest` and `receipt` equal the `WAI_RECEIPT` property's `content_hash` and `group_receipt`. 3. **`StandardDefined` is read as `decode-equivalence`** for this extension's "registered tier", because SPEC §7 gives it the same property. `GenerativeExact` is held at `none`. The text says this is a reading. In the room, VP9 frames get tier **`none`** because no probe ran, and that is shown as-is. 4. **A frame is bound only when every object its decoder was given since its last key frame was established.** Predicted frames depend on their references. The room verifies before decoding, so it meets this by construction. 5. **The energy classes.** The brief says "capability-constraints' energy classes", but that extension has none (its §4 says it is not an energy mechanism). I used energy-measurement §2's classes, and capability-constraints is left unchanged on energy. 6. **`Reason::ALL` became `Reason::all()`.** A `pub const` array of a type named `Reason` in a new module made cbindgen add four opaque `typedef struct Reason Reason;` to `include/wai.h`, one for each module's `Reason`. I bisected it on the build host. With the function, a fresh generation of `wai.h` is byte-identical to main's. 7. **A separate decode wasm build** (`wai_live_web_decode`), so the room's own `wai_live_web` is unchanged in what it carries. 8. **No consent rule.** §13 states what a report reveals and points a service that collects reports to jwp-receipts' `lawful-basis` claim. It sets no rule for the user. 9. **JSON integers in a JS page.** A page cannot keep `1920.0` apart from `1920`, so the corpus's malformed-bound case is a true fraction (`1920.5`). §5 says so. ## Findings from the browser - **The user agent drops the unregistered member.** It is the one the e2e drives, headless; its version is in the run's log. `metadata()` was empty for a frame constructed with `init.metadata = { contentBinding, rtpTimestamp }`, through both the buffer constructor and the frame constructor, and after `clone()`. The registered `rtpTimestamp` was dropped the same way. The proposal's issue text states this. - **Energy.** On the build host, decoding 640×360 VP9 through the user agent's decoder measured **816–858 µJ per decode** across eight runs (840, 838, 816, 857, 858, 851, 836 and 839). Each run is about 36 000 decodes over about 14 s, taken as the marginal over idle windows of the same length. It is `OnChipCounter` with filtering `Unknown` and coverage `Partial`, because the per-process counter covers CPU energy only (plus billed and less serviced). The user agent's `powerEfficient` beside it was `true` every time. **Each run declares a type-A standard uncertainty of 0.3–2.8 % from its own windows, but the spread between runs is about 5 %. The declared figure therefore understates what a repeat would see.** A meter that publishes such figures should add a between-run component; the e2e only checks that a figure is carried with its class beside the boolean. ## Checks | check | command | result | |---|---|---| | browser_sink unit tests | `cargo test --locked --no-default-features --features browser_sink --lib browser_sink::` (rb) | 12 passed | | integration test | `cargo test --locked --no-default-features --features browser_sink,receipt_claims --test browser_sink` (rb) | 6 passed | | corpus regenerates | `wai_browser_sink_vectors verify ../live-conformance/browser` (rb) | PASS ×3 | | second implementation | `python3 -I live-conformance/browser_sink_verify.py live-conformance/browser` | 101 cases PASS; unregistered answers changed 3 ways never read; 28 of 28 mutations fail | | wai-live-web native | `cargo test --locked --lib` and `--features int_video --lib browser_tests` (rb) | 5 passed; 3 passed (every chain object's frames hash to `sha256.json`) | | joule-moq-web | `cargo test --locked -p joule-moq-web --lib` (rb) | 8 passed | | side table in node | `node --test web/room/binding.test.mjs` | 12 passed | | registry | `wai registry export` then `--check`; `cargo test --lib codecs::registry` (rb) | up to date; 14 passed | | header | fresh cbindgen run (`touch src/container.rs`, build) then `git diff include/wai.h` (rb) | unchanged | | clippy | `--all-targets` on wai-rs (`browser_sink,receipt_claims`), wai-live-web (± `int_video`), joule-moq-web (rb) | zero notes in the new code (joule-moq's and int_payload's notes are main's) | | full browser e2e | `e2e/run.sh` on the build host, installed browser | **188 of 188 checks**, every scenario | | binding e2e, looped | `e2e/run.sh --only binding --runs 3`, then `--runs 2` on the final tree | **3 of 3 runs, 90 of 90 checks; 2 of 2, 60 of 60** | | repo checks | `check-tests-in-ci.py`, `check-workflow-concurrency.sh`, `check-spec-refs.py`, `check-wai-section-refs.py`, `check-no-vanity-metrics.sh`, `test-public-text-checks.sh`, `check-comparative-prose.sh`, `check-orphan-sources.py` | all exit 0 | | public names | `check-public-names.sh` over `wai/`, and file by file over every file this branch changes | every changed file clean. The hits are main's: `live_provenance.rs` ×3 and the existing `BROWSER_BIN` path in `joule-moq-web.yml` | | after merging origin/main (#397, #399, #403) | registry export `--check`, `codecs::registry`, `browser_sink` lib and `--test browser_sink`, `int_kernels_vectors every_decoded_output_states_a_sha256`, `corpus_provenance`, the verifier, the repo checks (rb and local) | up to date; all pass. The conflicts were in five files where both sides added (registry wire identifiers, the live-conformance index, the digest-exclusion reason, the CHANGELOG, the workflow), and each kept both | ## Falsifications (each committed first, broken on the build host or locally, then restored) - **Rust** (`wai_browser_sink_vectors verify`). Each of these fails (exit 101 from the generator's own asserts, or exit 1 with FAIL lines), and the restored code exits 0: - registry gate removed; - a timestamp given twice not struck; - refused provenance ignored; - zero-with-class read as a figure; - probe not required; - `powerEfficient` stated off the UA route; - receipt dropped from the canonical form. - Also, a reason row dropped from §11 fails `the_extension_lists_the_reasons`. - **Python** (`browser_sink_verify.py`). Each fails with exit 1, and the restored verifier exits 0: - gate dropped; - provenance ignored; - side table never strikes; - an unregistered answer read. - The signature check failed **no** case at first: every signature mutation was also caught by the canonical bytes. I added a mutation that forges a signature and makes the canonical bytes, link hash and binding all agree, and dropping the signature check now fails it (27 of 28, exit 1). - **JS** (`binding.test.mjs`): with the side table never striking, 2 tests fail. - **Browser e2e** (`--only binding`), each against an otherwise unchanged tree: - the page asks about every codec string: exit 1, and the check "no codec string outside the WebCodecs Codec Registry…" FAILs; - the room enters no binding for its chunks: exit 1, and the tile never shows a bound, verified frame (the wait times out); - the wasm decoder binds an object that did not verify: exit 1. The changed object is then decoded, its payload no longer decodes, and the page fails at "an object changed after it was receipted is refused and not decoded" and the checks after it. ## Deploy note **The live room must be redeployed as one unit after this lands**: page assets, relay and room service from one commit. The page assets are `web/room/` (with the new `binding.js`), both rebuilt wasm (`joule_moq_web`, whose verifier result changed, and `wai_live_web`, now with `browser_sink`), and `web/binding/` with `wai_live_web_decode` if `/binding/` is served. Nothing was deployed. ## Not done - No probe objects for UA codecs are published. A need of `decode-equivalence` therefore never routes to a UA decoder (§4.4 says so), and the room's VP9 frames are tier `none`. - No page implements the host route. It is in the spec and the corpus only. - C2's live-provenance verdict enters §10 as an input, and the corpus covers `accepted`, `refused`, `missing` and `not_applicable`. The room's tracks are WAI-packaged, so their provenance is `not_applicable` (live-provenance §1). No page verifies a CMAF provenance track in the browser yet. - No binding for `AudioData`, which has no metadata dictionary. Frames decoded for a late joiner's FETCH and for DVR playback are not bound. - The energy figure is measured only where the host has a per-process counter (macOS here). Elsewhere the e2e step is reported **skipped**, not passed. - The WebCodecs issue is not filed. - `wai-live-web`'s pre-existing native tests (compose and fixture) are still in no workflow; this PR adds only its `browser_tests` to CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- **Closed:** Landed in #428 (merge train, merged as aa61ab3b with all 65 checks green).
extensions/browser-sink.md: a page routes each object to the user agent's decoder, a
native host's or WAI's wasm decoder; the user agent is asked only about codec strings the
WebCodecs Codec Registry lists, and its decoder is taken only where it is exact for the
tier the object needs. capability-constraints gains its register (§3.1) and its
evaluation in a page. A measured figure is reported beside the user agent's
powerEfficient, which stays its word. Every decoded frame carries contentBinding: the
content hash of the receipted object, the link hash of its group receipt, and the tier.

wai-rs: the browser_sink module (feature browser_sink), its corpus generator
wai_browser_sink_vectors, and tests/browser_sink.rs. The corpus is
live-conformance/browser/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Written from extensions/browser-sink.md: it reads §4.1's decoders, §4.2's registry rows,
§11's reasons and capability-constraints §3.1's register from the texts, each
capability's class from the registry export, and recomputes every route, constraint
evaluation, energy entry, binding, chain signature and link hash, frame state and side
table. Answers for codec strings no registry row lists are changed every way and never
read; every mutated copy of the corpus fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai-live-web exports the browser_sink functions (routeReport, evaluateConstraints,
readBinding, makeBinding, frameTier, frameState) and, behind its feature int_video,
decodeBound: a wai.video.int_motion envelope verified against its group and object
receipts, decoded by the integer decoder and every frame bound; an object that does not
verify is not decoded, and one decoded without receipts is unbound. joule-moq-web's
verifier now returns a verified object's content hash and its group receipt's link hash,
which a frame decoded from it names. Reason's list is a function, so nothing of the module
reaches include/wai.h.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
web/room/binding.js: the side table a page carries contentBinding in until a registry
lists the member (decoder and timestamp, clone, transfer, reset, a bound; ambiguity
refused), the capability report built in two passes so that only codec strings the
WebCodecs Codec Registry lists are put to the user agent, and the state of the frame on
screen. The room routes each track before it decodes it, binds every frame its video
decoders output to the verified object and group receipt, and shows the state of each
tile's frame with ?debug=binding. web/binding/ runs live-conformance/browser in the page,
the wasm route's verified decode, the binding through clone, transfer, copyTo and
re-encode, the metadata probe and a capability report from the user agent's answers.
e2e: scenario binding, energy.py for the host's per-process counter, and the decode wasm
built by run.sh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A page whose numbers do not keep 1920.0 apart from 1920 writes it as an integer, so the
malformed-bound case is a true fraction; §5 says so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merge origin/main into wai/browser-binding
Some checks failed
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m15s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m18s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 26s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 10m5s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 1m56s
wai-pack / wai-pack · linux (pull_request) Successful in 1m28s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 19m15s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 2m26s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 4m0s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 15s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m33s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 13s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 23s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m27s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 7m11s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m18s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 15m42s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 22s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m16s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 4m19s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 29m11s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 8m27s
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
2a610f5535
Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot closed this pull request 2026-10-22 12:02:39 -04:00
Some checks failed
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 5s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 5s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 5s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m47s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m38s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m39s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 1m46s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 2m14s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
WAI conformance / does this change touch it (pull_request) Successful in 5s
WAI energy receipts / does this change touch it (pull_request) Successful in 4s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 5s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 8s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 7s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 57s
wai-web wasm32 / does this change touch it (pull_request) Successful in 5s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m15s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m18s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 26s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 10m5s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 1m56s
wai-pack / wai-pack · linux (pull_request) Successful in 1m28s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 19m15s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 2m26s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 4m0s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 15s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m33s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 13s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 23s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m27s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 7m11s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m18s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 15m42s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 22s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m16s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 4m19s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 29m11s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 8m27s
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!413
No description provided.