wai: stream audit, an audit kit for streams another service delivers (RI1) #474

Closed
dcharlot wants to merge 0 commits from wai/audit-kit into main
Owner

RI1: an audit kit for streams a content owner already distributes through another service. The owner's players sign what they fetched and presented; the owner's collector verifies, stores and reports it; anyone holding the stored records recomputes the report. Nothing is asked of the service that packages and delivers the stream. No price, tariff or amount is computed anywhere (billing is not WAI's). Based on origin/main at 9d984511a, with origin/main merged in at 96117c570.

What is in it

wai/extensions/stream-audit.md registers the record wai.delivery.audit.

  • Channel and session key (§3): a channel is the owner's label (never a URL); a player makes a fresh Ed25519 key per playback session, and the session is ad-presentation §3's derivation with the channel id in the room id's place.
  • Acknowledgements (§4): settlement WDA1 records of every playlist, manifest and segment the player fetched, under settlement §14 (below): one chain per origin, one record per origin and window (10 s by default), live media in class 1, on-demand media in class 2 (no grant), everything else in class 4. A repeat in a window is counted in the session record and not listed. Key and licence responses are never acknowledged.
  • Session record WAS1 (§5): channel, owner (the collector's key), session key, presentation, span, how it ended, origins, the digests of its acknowledgements, claims and attachments, its presentation spans (each with its least visible fraction, whether audible, whether within an ad), start time and stalls, repeats, the digest of its delivery-telemetry log, the digest of its CMSD notes, and energy entries per role (origin, delivery, sink decode, display), each a figure with its acquisition class or the unmetered marker, read under live-metering §3.6's rules.
  • Ads (§6): an EXT-X-DATERANGE with SCTE35-OUT, SCTE35-CMD or a configured CLASS marks an ad; the claim is ad-presentation's WPC1 with room the channel, cue the marker's digest, and a creative of the acknowledged segments the ad covers. Play that enters an ad between two observations starts its first span at media time 0, and play that leaves it ends the last span where it left.
  • Telemetry (§7): the delivery-telemetry log (its sid the session), whose vst_ms and stalls the record's figures must equal; its Event-mode CMCD version 2 reports sent to the collector as application/cmcd batches as the session runs; CMSD-Static and CMSD-Dynamic recorded where a response exposes them.
  • Termination and attachments (§8): how the session ended (ended, user, hidden, idle, error), and other signed records of the session bound by digest (a termination record of wai.live.termination, a sink energy receipt of wai.sink.energy), stored, not read in this revision.
  • Bundle and verification (§9, §10): what the player posts, and nine steps, each refusal with its place; claim verdicts never refuse the session.
  • Channel report (§11): sessions by digest, time presented, in view, audible and within ads, sessions by how they ended, ads presented with the five quartile counts and those viewable under the video rule, rebuffering (suppressed below 50 sessions, delivery-telemetry §8), acknowledged objects and bytes per origin and class, repeats, and joules per role and class (never summed across classes; the unmetered marker counted apart, never as zero). Its bytes are the RFC 8785 form of the document; the collector signs them; §11.4 recomputes and checks a served report.
  • Collector (§12), player integration (§13), its reasons (§14), conformance (§15), security and privacy (§16, no gate of its own; the lawful-basis declaration for the operator).

wai/extensions/settlement.md §14: acknowledging the objects of an HTTP service that delivers no WAI envelopes. Such an object is a response's content with content codings removed (a range response's range an object of its own), named by plain SHA-256; its upstream is the digest of its origin (RFC 6454 §6.2), which signs nothing and appears in acknowledgements only; no receipt is checked, no range proof applies, no statement is signed; everything else of §3 is unchanged. Settlement's corpus, verifier and reason table are untouched.

Reference: wai-rs module stream_audit (feature stream_audit = settlement + live_ads + telemetry; no new dependency), generator wai_stream_audit_vectors, tests/stream_audit.rs; live_metering gains a crate-visible reader of a figure and its class; registered wai.delivery.audit (record) and WAS1 (magic), export regenerated; SPEC §5 paragraph; CHANGELOG.

Corpus wai/live-conformance/audit/ (one vectors.json, from fixed seeds): accepted sessions (two origins, two marked ads one in view and one at 30 %, a repeat, CMSD notes, a stall, an Estimator sink figure beside an unmetered display; on demand ended hidden with an OnChipCounter figure and an attachment; nothing presented, ended error; a session with one claim of every verdict; a second record of one session; a channel of 52 sessions), one refused control per defect, and reports (rebuffering suppressed; rebuffering stated; and refused: bytes that do not recompute, a bad signature, a session not given, two records of one session).

Independent verifier wai/live-conformance/stream_audit_verify.py (python3 -I, standard library), written from the text, sharing no code with wai-rs: it reads acknowledgements with settlement_verify.py, claims with ad_presentation_verify.py, logs and energy classes with live_metering_verify.py, signatures with claim-conformance/_ed25519.py; recomputes every report itself; mutation and dropped-check self-tests.

The kit, wai/wai-audit (what runs):

  • wai-audit serve: the collector (axum, loopback 127.0.0.1:9694). POST /v1/sessions (verify, store one file per session, 201/200 again/409 another record of a stored session/413/422/429), POST /v1/cmcd (Event-mode batches, advisory, never in a report), GET /v1/channels/{name}/report?from=&to= (signed), GET /v1, /v1/health, /v1/key; per-client limits (IPv4 address or IPv6 /64, X-Forwarded-For only from trusted proxies), per-route body limits, CORS for configured page origins. wai-audit key | verify | report | check on the command line (check is §11.4).
  • web/audit.js: the player integration, no dependency: WebCrypto SHA-256 and Ed25519, a one-chunk BLAKE3 (web/blake3.js) for the settlement tree root, span and ad recorders, the playlist reader, the log and its CMCD, the bundle. web/adapter-tap.js wraps fetch and XMLHttpRequest for any web player; web/adapter-hlsjs.js wraps the loader class of a player whose configuration takes one (the one file that names that open-source player, with the repo's opt-out marker).
  • e2e/run.sh + e2e/audit.e2e.mjs: a real browser end to end (below).
  • Workflows: a step in wai-conformance.yml (module tests, corpus test, Python verifier) and paths: entry; new wai-audit.yml (collector tests and clippy -D warnings on Linux and macOS; on the macOS host, the Node tests and the browser end to end).

Design decisions, and why

  • One kit, two integrations. The network tap works for any web player that fetches over fetch/XMLHttpRequest, which covers the common open-source players without an adapter each; the loader adapter adds the player's own program time, which a live stream needs. A dedicated adapter for the other two common players was not written: their packages were not available offline here, and an adapter written blind against an interface not run is worse than none; the tap covers them.
  • The upstream of a third-party object is its origin's digest, so acknowledgements chain per CDN origin and a multi-origin delivery is reported per origin, and nothing pretends the service signed anything.
  • Plain SHA-256 of the decoded content: what a page's script can see (a user agent removes content codings before handing a body over, and Content-Encoding is not a CORS-safelisted header), and the value Repr-Digest/Content-Digest state for an uncoded response.
  • The log binds the QoE figures. The record's start time and stalls must equal what its delivery-telemetry log gives, and the log's sid is the session, so a report's rebuffering is the players' signed, recomputable statements; the CMCD batches stay advisory.
  • WPC1 under a channel binding for marked ads, as the brief says when RI2's form does not exist yet. RI2's form is now #470 (wai.video.stitched_presentation, WSC1); this PR references it by name (§6.3: where a deployment holds that record, a stitched ad is claimed with it and the bundle carries the manifest bytes its claims name) and is not based on its branch, so it does not stack on an open PR. Moving stitched ads to WSC1 is the follow-up (below).
  • Termination and sink energy by reference. #457 (wai.live.termination) and #460 (wai.sink.energy) are open; the record carries its own ended reason and its energy entries with classes under the hop-table rule both PRs use (figure, unmetered marker, or absent), and binds either PR's records by digest as attachments, stored and not read in this revision.
  • No consent gates (owner rule): §16 states what a session reveals and to whom and points to lawful-basis.
  • No money. The report counts; it prices nothing. Per the owner, only billing (ledger, invoices, charging) belongs to waiTransport; nothing of this slice does that, so nothing went there.
  • Collector port 9694, since #470's stitched-presentation tool defaults to 9690.

Checks

Heavy work on the build host through rb; light scripts locally. At head d56916dac (after the merge of 96117c570) unless stated.

check command result
module unit tests cargo test --locked --no-default-features --features stream_audit --lib stream_audit:: 5 passed
corpus regenerates, every outcome, every reason and borrowed code, §14's table cargo test --locked --no-default-features --features stream_audit --test stream_audit 4 passed
generator's verify wai_stream_audit_vectors verify ../live-conformance/audit PASS, PASS
settlement (text changed) --features settlement --lib settlement::, --test settlement 5 passed; 4 passed
live-metering (a crate-visible reader added) --features live_metering --lib live_metering::, --test live_metering 12 passed; 4 passed
ad-presentation (read, unchanged) --features live_ads --test ad_presentation 3 passed
registry cargo test --locked --lib codecs::registry; wai registry export --check (export regenerated with --pull) 14 passed; up to date
clippy, this slice's files cargo clippy --locked --no-default-features --features stream_audit --all-targets; --features live_metering --all-targets 0 notes in the slice's files (the crate's existing unpinned_read and igdn_check warnings unchanged)
C header default cargo build --locked, then git diff --quiet include/wai.h unchanged (module cbindgen:ignore)
lockfiles cargo metadata --locked --offline in wai/wai-rs (unchanged) and wai/wai-audit (new, generated --offline) both resolve
collector cd wai/wai-audit && cargo test --locked --release; cargo clippy --locked --release --all-targets -- -D warnings 4 passed (every corpus session posted gets its outcome; stored sessions report exactly the corpus's served reports, byte for byte and signature; CORS, limits, CMCD intake; client identity); clippy clean
player integration node --test web/audit.test.mjs 8 passed (the corpus's live session rebuilt byte for byte: record, acknowledgements, claims, log, notes, CMCD batch)
browser end to end NPM_OFFLINE=1 BROWSER_BIN=… e2e/run.sh --runs 3 on the build host (installed browser, packages from the host's npm cache, nothing fetched from the network) 3 of 3 runs, 75 of 75 checks ok, 6 sessions: presented 12.11–12.13 s of a 12 s stream, the ad's claim presented at quartile 5 and viewable in every session, the acknowledged objects exactly the origin's SHA-256 of what it served, CMSD notes per segment, the report recomputed by wai-audit check and refused with a bundle withheld
independent verifier python3 -I live-conformance/stream_audit_verify.py live-conformance/audit 90 sessions and 7 reports agree; every reason appears; 15 of 15 mutated copies fail; 32 of 32 dropped checks caught; exit 0
verifiers reading changed text settlement_verify.py, ad_presentation_verify.py, live_metering_verify.py, browser_sink_verify.py (reads the export), claim_verify.py (reads the export) all exit 0
workflows ruby -ryaml over every workflow; wfsteps.py HEAD b7e241ec5 origin/main and HEAD origin/main b7e241ec5 for the merge parse; 0 problems (against the base, the only flags are this PR's new steps, by construction)
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 every tracked wai/ file and the new workflow; commit messages and this body against the same patterns clean; 0 hits
CI on this PR every run cancelled on opening, as asked cancelled

Falsifications

All committed first; each rule broken alone in the build host's copy, the guarding tests run, the file restored (git diff --quiet clean after each run).

  • wai-rs and the collector, 36 of 37 caught, the one survivor by design (--test stream_audit, or wai-audit's tests): owner, channel, session binding, record signature, origin rule, energy rule, acknowledgement unbound, origin unknown, window, classes, whole items, an acknowledgement the record does not name, chain first/overlap/prev, log digest, QoE agreement, notes digest, notes acknowledged, attachment missing, claim binding, marker, duplicate claim, asset, acknowledged ad, report duplicate/missing/bytes/signature, rebuffering suppression, the unmetered marker counted apart, repeats; the collector's duplicate session, CORS, rate limit, and its body limit with both its layers removed. Found: the check refusing an acknowledgement the record does not name was guarded by no vector in either implementation; ack_not_named added, and the Python verifier can now drop that check too. The collector's body limit with one layer removed survives by design (the route's limit and the handler's are two layers; removing both is caught).
  • Python verifier, 32 of 32 dropped checks caught, 15 of 15 mutations. Found: three checks first went uncaught (an item that is not a whole object, a chain's first record naming a prev, notes that are not the bound ones); a control was added for each.
  • Player integration, 12 of 12 caught by its Node tests: repeats listed, the tree's leaf domain, a key acknowledged, CMCD keys unsorted, the ad flag in spans, the span cut where an ad is left, the span from where an ad is entered, CMSD dropped, stalls before the first presentation, the tap taking everything, the loader adapter recording keys, BLAKE3's ROOT flag. Found: two first survived (an ad entered between observations, which the corpus's observations never exercise; a stall before the first presentation); a test was added for each.
  • In the browser, 3 of 3 caught: the session reading a buffer after the player moved it (no bundle is ever sent), the tap reading a response after the player's handler (5 checks fail: 2 objects acknowledged of 8 served), and an ad entered between observations (now caught first by its Node test, which run.sh runs before the browser).
  • What the browser end to end found, fixed in this PR (each is a commit): the player hands fragment buffers to a worker and empties them after the loader's callback, so the session now copies the bytes at once and keeps each object in the order given; the tap read XMLHttpRequest responses after the player had moved them, and now listens from the request's construction; a stitched ad is entered between two 200 ms observations, so its claim began past 0 and reached quartile 1, and play that enters an ad continuously now starts its first span at 0 (stream-audit §6.3); and a failed run left its collector running.

Not done

  • Stitched ads in WSC1 (#470): the claim form, the manifest bytes in the bundle (#470 refuses a claim whose manifest the verifier lacks, claim_manifest_unknown), and the browser producer joule-moq-web/web/ads/stitched.js. This PR claims stitched ads with WPC1 under §6's channel binding.
  • DASH ad markers (EventStream, in-band events) are not read: the clause text of ISO/IEC 23009-1 was not read for this revision. DASH segments are acknowledged through the tap (by media type under an MPD).
  • Termination and sink-energy records are bound, not verified (#457, #460 open).
  • No dedicated adapters for the other two common web players; they go through the tap. Their packages were not available offline here; none was installed from the network.
  • A page has no meter: the kit's energy entries in a page are the unmetered marker; a native host or a sink-energy profile supplies figures.
  • Live streams in the browser end to end: the e2e stream is on demand; live is covered by the corpus (class 1, program time from the player) but not by a browser run.
  • A bundle sent at page hide rides a keep-alive request a user agent may refuse past its quota; the kit sends at the end of playback first.
  • One process per owner key; no accounts, multi-tenancy, HSM/KMS custody or published key log for the owner's key.
  • Deploy: not deployed (owner's). The deploy note (binary, config, port, storage, Caddy route, launchd/systemd, health check) is with the lead.

🤖 Generated with Claude Code


Closed: Landed on main through merge train E (#499, merge bf39ec9a); verified by content: this PR's head is in main

RI1: an audit kit for streams a content owner already distributes through another service. The owner's players sign what they fetched and presented; the owner's collector verifies, stores and reports it; anyone holding the stored records recomputes the report. Nothing is asked of the service that packages and delivers the stream. No price, tariff or amount is computed anywhere (billing is not WAI's). Based on `origin/main` at `9d984511a`, with `origin/main` merged in at `96117c570`. ## What is in it **`wai/extensions/stream-audit.md`** registers the record `wai.delivery.audit`. - **Channel and session key** (§3): a channel is the owner's label (never a URL); a player makes a fresh Ed25519 key per playback session, and the session is ad-presentation §3's derivation with the channel id in the room id's place. - **Acknowledgements** (§4): settlement `WDA1` records of every playlist, manifest and segment the player fetched, under settlement §14 (below): one chain per origin, one record per origin and window (10 s by default), live media in class 1, on-demand media in class 2 (no grant), everything else in class 4. A repeat in a window is counted in the session record and not listed. Key and licence responses are never acknowledged. - **Session record `WAS1`** (§5): channel, owner (the collector's key), session key, presentation, span, how it ended, origins, the digests of its acknowledgements, claims and attachments, its presentation **spans** (each with its least visible fraction, whether audible, whether within an ad), start time and stalls, repeats, the digest of its delivery-telemetry log, the digest of its CMSD notes, and **energy entries per role** (origin, delivery, sink decode, display), each a figure with its acquisition class or the unmetered marker, read under live-metering §3.6's rules. - **Ads** (§6): an `EXT-X-DATERANGE` with `SCTE35-OUT`, `SCTE35-CMD` or a configured `CLASS` marks an ad; the claim is ad-presentation's `WPC1` with `room` the channel, `cue` the marker's digest, and a creative of the acknowledged segments the ad covers. Play that enters an ad between two observations starts its first span at media time 0, and play that leaves it ends the last span where it left. - **Telemetry** (§7): the delivery-telemetry log (its `sid` the session), whose `vst_ms` and stalls the record's figures must equal; its Event-mode CMCD version 2 reports sent to the collector as `application/cmcd` batches as the session runs; CMSD-Static and CMSD-Dynamic recorded where a response exposes them. - **Termination and attachments** (§8): how the session ended (`ended`, `user`, `hidden`, `idle`, `error`), and other signed records of the session bound by digest (a termination record of `wai.live.termination`, a sink energy receipt of `wai.sink.energy`), stored, not read in this revision. - **Bundle and verification** (§9, §10): what the player posts, and nine steps, each refusal with its place; claim verdicts never refuse the session. - **Channel report** (§11): sessions by digest, time presented, in view, audible and within ads, sessions by how they ended, ads presented with the five quartile counts and those viewable under the video rule, rebuffering (suppressed below 50 sessions, delivery-telemetry §8), acknowledged objects and bytes per origin and class, repeats, and joules per role and class (never summed across classes; the unmetered marker counted apart, never as zero). Its bytes are the RFC 8785 form of the document; the collector signs them; §11.4 recomputes and checks a served report. - **Collector** (§12), **player integration** (§13), its reasons (§14), conformance (§15), security and privacy (§16, no gate of its own; the `lawful-basis` declaration for the operator). **`wai/extensions/settlement.md` §14**: acknowledging the objects of an HTTP service that delivers no WAI envelopes. Such an object is a response's content with content codings removed (a range response's range an object of its own), named by plain SHA-256; its upstream is the digest of its origin (RFC 6454 §6.2), which signs nothing and appears in acknowledgements only; no receipt is checked, no range proof applies, no statement is signed; everything else of §3 is unchanged. Settlement's corpus, verifier and reason table are untouched. **Reference**: `wai-rs` module `stream_audit` (feature `stream_audit` = settlement + live_ads + telemetry; no new dependency), generator `wai_stream_audit_vectors`, `tests/stream_audit.rs`; `live_metering` gains a crate-visible reader of a figure and its class; registered `wai.delivery.audit` (record) and `WAS1` (magic), export regenerated; SPEC §5 paragraph; CHANGELOG. **Corpus** `wai/live-conformance/audit/` (one `vectors.json`, from fixed seeds): accepted sessions (two origins, two marked ads one in view and one at 30 %, a repeat, CMSD notes, a stall, an `Estimator` sink figure beside an unmetered display; on demand ended `hidden` with an `OnChipCounter` figure and an attachment; nothing presented, ended `error`; a session with one claim of every verdict; a second record of one session; a channel of 52 sessions), one refused control per defect, and reports (rebuffering suppressed; rebuffering stated; and refused: bytes that do not recompute, a bad signature, a session not given, two records of one session). **Independent verifier** `wai/live-conformance/stream_audit_verify.py` (`python3 -I`, standard library), written from the text, sharing no code with `wai-rs`: it reads acknowledgements with `settlement_verify.py`, claims with `ad_presentation_verify.py`, logs and energy classes with `live_metering_verify.py`, signatures with `claim-conformance/_ed25519.py`; recomputes every report itself; mutation and dropped-check self-tests. **The kit, `wai/wai-audit`** (what runs): - `wai-audit serve`: the collector (axum, loopback `127.0.0.1:9694`). `POST /v1/sessions` (verify, store one file per session, 201/200 again/409 another record of a stored session/413/422/429), `POST /v1/cmcd` (Event-mode batches, advisory, never in a report), `GET /v1/channels/{name}/report?from=&to=` (signed), `GET /v1`, `/v1/health`, `/v1/key`; per-client limits (IPv4 address or IPv6 /64, `X-Forwarded-For` only from trusted proxies), per-route body limits, CORS for configured page origins. `wai-audit key | verify | report | check` on the command line (`check` is §11.4). - `web/audit.js`: the player integration, no dependency: WebCrypto SHA-256 and Ed25519, a one-chunk BLAKE3 (`web/blake3.js`) for the settlement tree root, span and ad recorders, the playlist reader, the log and its CMCD, the bundle. `web/adapter-tap.js` wraps `fetch` and `XMLHttpRequest` for any web player; `web/adapter-hlsjs.js` wraps the loader class of a player whose configuration takes one (the one file that names that open-source player, with the repo's opt-out marker). - `e2e/run.sh` + `e2e/audit.e2e.mjs`: a real browser end to end (below). - Workflows: a step in `wai-conformance.yml` (module tests, corpus test, Python verifier) and `paths:` entry; new `wai-audit.yml` (collector tests and clippy `-D warnings` on Linux and macOS; on the macOS host, the Node tests and the browser end to end). ## Design decisions, and why - **One kit, two integrations.** The network tap works for any web player that fetches over `fetch`/`XMLHttpRequest`, which covers the common open-source players without an adapter each; the loader adapter adds the player's own program time, which a live stream needs. A dedicated adapter for the other two common players was not written: their packages were not available offline here, and an adapter written blind against an interface not run is worse than none; the tap covers them. - **The upstream of a third-party object is its origin's digest**, so acknowledgements chain per CDN origin and a multi-origin delivery is reported per origin, and nothing pretends the service signed anything. - **Plain SHA-256 of the decoded content**: what a page's script can see (a user agent removes content codings before handing a body over, and `Content-Encoding` is not a CORS-safelisted header), and the value `Repr-Digest`/`Content-Digest` state for an uncoded response. - **The log binds the QoE figures.** The record's start time and stalls must equal what its delivery-telemetry log gives, and the log's `sid` is the session, so a report's rebuffering is the players' signed, recomputable statements; the CMCD batches stay advisory. - **WPC1 under a channel binding** for marked ads, as the brief says when RI2's form does not exist yet. RI2's form is now #470 (`wai.video.stitched_presentation`, `WSC1`); this PR **references it by name** (§6.3: where a deployment holds that record, a stitched ad is claimed with it and the bundle carries the manifest bytes its claims name) and is **not** based on its branch, so it does not stack on an open PR. Moving stitched ads to `WSC1` is the follow-up (below). - **Termination and sink energy by reference.** #457 (`wai.live.termination`) and #460 (`wai.sink.energy`) are open; the record carries its own `ended` reason and its energy entries with classes under the hop-table rule both PRs use (figure, unmetered marker, or absent), and binds either PR's records by digest as attachments, stored and not read in this revision. - **No consent gates** (owner rule): §16 states what a session reveals and to whom and points to `lawful-basis`. - **No money.** The report counts; it prices nothing. Per the owner, only billing (ledger, invoices, charging) belongs to waiTransport; nothing of this slice does that, so nothing went there. - **Collector port 9694**, since #470's stitched-presentation tool defaults to 9690. ## Checks Heavy work on the build host through `rb`; light scripts locally. At head `d56916dac` (after the merge of `96117c570`) unless stated. | check | command | result | |---|---|---| | module unit tests | `cargo test --locked --no-default-features --features stream_audit --lib stream_audit::` | 5 passed | | corpus regenerates, every outcome, every reason and borrowed code, §14's table | `cargo test --locked --no-default-features --features stream_audit --test stream_audit` | 4 passed | | generator's verify | `wai_stream_audit_vectors verify ../live-conformance/audit` | PASS, PASS | | settlement (text changed) | `--features settlement --lib settlement::`, `--test settlement` | 5 passed; 4 passed | | live-metering (a crate-visible reader added) | `--features live_metering --lib live_metering::`, `--test live_metering` | 12 passed; 4 passed | | ad-presentation (read, unchanged) | `--features live_ads --test ad_presentation` | 3 passed | | registry | `cargo test --locked --lib codecs::registry`; `wai registry export --check` (export regenerated with `--pull`) | 14 passed; up to date | | clippy, this slice's files | `cargo clippy --locked --no-default-features --features stream_audit --all-targets`; `--features live_metering --all-targets` | 0 notes in the slice's files (the crate's existing `unpinned_read` and `igdn_check` warnings unchanged) | | C header | default `cargo build --locked`, then `git diff --quiet include/wai.h` | unchanged (module `cbindgen:ignore`) | | lockfiles | `cargo metadata --locked --offline` in `wai/wai-rs` (unchanged) and `wai/wai-audit` (new, generated `--offline`) | both resolve | | collector | `cd wai/wai-audit && cargo test --locked --release`; `cargo clippy --locked --release --all-targets -- -D warnings` | 4 passed (every corpus session posted gets its outcome; stored sessions report exactly the corpus's served reports, byte for byte and signature; CORS, limits, CMCD intake; client identity); clippy clean | | player integration | `node --test web/audit.test.mjs` | 8 passed (the corpus's live session rebuilt byte for byte: record, acknowledgements, claims, log, notes, CMCD batch) | | browser end to end | `NPM_OFFLINE=1 BROWSER_BIN=… e2e/run.sh --runs 3` on the build host (installed browser, packages from the host's npm cache, nothing fetched from the network) | 3 of 3 runs, 75 of 75 checks ok, 6 sessions: presented 12.11–12.13 s of a 12 s stream, the ad's claim presented at quartile 5 and viewable in every session, the acknowledged objects exactly the origin's SHA-256 of what it served, CMSD notes per segment, the report recomputed by `wai-audit check` and refused with a bundle withheld | | independent verifier | `python3 -I live-conformance/stream_audit_verify.py live-conformance/audit` | 90 sessions and 7 reports agree; every reason appears; 15 of 15 mutated copies fail; 32 of 32 dropped checks caught; exit 0 | | verifiers reading changed text | `settlement_verify.py`, `ad_presentation_verify.py`, `live_metering_verify.py`, `browser_sink_verify.py` (reads the export), `claim_verify.py` (reads the export) | all exit 0 | | workflows | `ruby -ryaml` over every workflow; `wfsteps.py HEAD b7e241ec5 origin/main` and `HEAD origin/main b7e241ec5` for the merge | parse; 0 problems (against the base, the only flags are this PR's new steps, by construction) | | 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 every tracked `wai/` file and the new workflow; commit messages and this body against the same patterns | clean; 0 hits | | CI on this PR | every run cancelled on opening, as asked | cancelled | ## Falsifications All committed first; each rule broken alone in the build host's copy, the guarding tests run, the file restored (`git diff --quiet` clean after each run). - **wai-rs and the collector, 36 of 37 caught, the one survivor by design** (`--test stream_audit`, or `wai-audit`'s tests): owner, channel, session binding, record signature, origin rule, energy rule, acknowledgement unbound, origin unknown, window, classes, whole items, an acknowledgement the record does not name, chain first/overlap/prev, log digest, QoE agreement, notes digest, notes acknowledged, attachment missing, claim binding, marker, duplicate claim, asset, acknowledged ad, report duplicate/missing/bytes/signature, rebuffering suppression, the unmetered marker counted apart, repeats; the collector's duplicate session, CORS, rate limit, and its body limit with both its layers removed. **Found:** the check refusing an acknowledgement the record does not name was guarded by no vector in either implementation; `ack_not_named` added, and the Python verifier can now drop that check too. The collector's body limit with one layer removed survives by design (the route's limit and the handler's are two layers; removing both is caught). - **Python verifier, 32 of 32 dropped checks caught**, 15 of 15 mutations. **Found:** three checks first went uncaught (an item that is not a whole object, a chain's first record naming a `prev`, notes that are not the bound ones); a control was added for each. - **Player integration, 12 of 12 caught** by its Node tests: repeats listed, the tree's leaf domain, a key acknowledged, CMCD keys unsorted, the ad flag in spans, the span cut where an ad is left, the span from where an ad is entered, CMSD dropped, stalls before the first presentation, the tap taking everything, the loader adapter recording keys, BLAKE3's ROOT flag. **Found:** two first survived (an ad entered between observations, which the corpus's observations never exercise; a stall before the first presentation); a test was added for each. - **In the browser, 3 of 3 caught**: the session reading a buffer after the player moved it (no bundle is ever sent), the tap reading a response after the player's handler (5 checks fail: 2 objects acknowledged of 8 served), and an ad entered between observations (now caught first by its Node test, which `run.sh` runs before the browser). - **What the browser end to end found, fixed in this PR** (each is a commit): the player hands fragment buffers to a worker and empties them after the loader's callback, so the session now copies the bytes at once and keeps each object in the order given; the tap read `XMLHttpRequest` responses after the player had moved them, and now listens from the request's construction; a stitched ad is entered between two 200 ms observations, so its claim began past 0 and reached quartile 1, and play that enters an ad continuously now starts its first span at 0 (stream-audit §6.3); and a failed run left its collector running. ## Not done - **Stitched ads in `WSC1`** (#470): the claim form, the manifest bytes in the bundle (#470 refuses a claim whose manifest the verifier lacks, `claim_manifest_unknown`), and the browser producer `joule-moq-web/web/ads/stitched.js`. This PR claims stitched ads with `WPC1` under §6's channel binding. - **DASH ad markers** (`EventStream`, in-band events) are not read: the clause text of ISO/IEC 23009-1 was not read for this revision. DASH segments are acknowledged through the tap (by media type under an MPD). - **Termination and sink-energy records are bound, not verified** (#457, #460 open). - **No dedicated adapters for the other two common web players**; they go through the tap. Their packages were not available offline here; none was installed from the network. - **A page has no meter**: the kit's energy entries in a page are the unmetered marker; a native host or a sink-energy profile supplies figures. - **Live streams in the browser end to end**: the e2e stream is on demand; live is covered by the corpus (class 1, program time from the player) but not by a browser run. - **A bundle sent at page hide** rides a keep-alive request a user agent may refuse past its quota; the kit sends at the end of playback first. - **One process per owner key**; no accounts, multi-tenancy, HSM/KMS custody or published key log for the owner's key. - **Deploy**: not deployed (owner's). The deploy note (binary, config, port, storage, Caddy route, launchd/systemd, health check) is with the lead. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- **Closed:** Landed on main through merge train E (#499, merge bf39ec9a); verified by content: this PR's head is in main
A content owner's evidence from the players of a stream another service
delivers over HTTP. settlement §14 acknowledges such objects by plain
SHA-256 from an upstream named by its origin. stream-audit defines the
session key and channel, the acknowledgement windows and chains, the
WAS1 session record (spans presented, start time and stalls recomputed
from the delivery-telemetry log, energy per role with its class, how the
session ended), WPC1 claims for marked ads under the channel, the bundle
a collector verifies, and the channel report it signs and any holder of
the bundles recomputes. No price, tariff or amount.

wai-rs module stream_audit (feature stream_audit), the generator
wai_stream_audit_vectors, tests/stream_audit.rs, and the corpus
live-conformance/audit/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
live-conformance/stream_audit_verify.py, written from the extension's
text: it reads acknowledgements with settlement_verify.py, claims with
ad_presentation_verify.py and logs and energy classes with
live_metering_verify.py, recomputes every report, gives every vector the
corpus's outcome, requires 15 mutated copies to fail and catches each of
its 31 checks dropped. Dropping three of them first went uncaught (an
item that is not a whole object, a chain's first acknowledgement naming
a prev, notes that are not the bound ones); the corpus gains a control
for each.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai/wai-audit is the reference collector of stream-audit §12: it
verifies each bundle a player posts (wai-rs stream_audit), stores the
accepted ones one file per session (a different record of a stored
session is refused audit_session_duplicate), takes Event-mode CMCD
batches as advisory telemetry, and serves each channel's report signed
with its key. Limits per client (an IPv4 address or an IPv6 /64), per
route body limits, CORS for the configured page origins. The CLI also
verifies a bundle, recomputes a report from a data directory, and
checks a served report against its bundles. Its tests post every corpus
session and require the corpus's outcome, and require the stored
sessions to report exactly the corpus's served reports.

web/audit.js is the player integration: it hashes each object with
WebCrypto, signs the acknowledgements, claims and session record with
WebCrypto's Ed25519, computes the settlement tree root with a one-chunk
BLAKE3 of its own, records spans, ads, the log and its CMCD batch. Its
Node tests drive it with the corpus's inputs and require the corpus's
records byte for byte. adapter-tap.js wraps fetch and XMLHttpRequest for
any player; adapter-hlsjs.js wraps a player's loader class.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
e2e/run.sh builds the collector, installs the automation package and an
open-source web player from e2e/package-lock.json (npm ci, no install
scripts), runs the player integration's unit tests, then
e2e/audit.e2e.mjs: an HLS stream made with the host's media tool (H.264
and AAC in 2 s CMAF segments, a program time, a marked ad over two
segments) served from another origin with CMSD-Static exposed, played in
a real browser twice, once through the loader adapter and once through
the network tap, each session sent to a collector on loopback. It checks
the outcome, the acknowledged objects against the origin's own SHA-256
of what it served, the ad's claim, the CMSD notes, the unmetered energy,
the CMCD batches, the report, its recomputation by `wai-audit check`, a
replay stored once and a tampered record refused.

Three defects it found, fixed here:
- the player hands fragment buffers on (to a worker), emptying them
  after the loader's callback: the session now copies the bytes at once,
  and keeps each object in the order it was given;
- the tap read XMLHttpRequest responses after the player had moved them:
  it now listens from the request's construction, so it reads first;
- a stitched ad is entered between two observations, so its claim began
  past media time 0 and reached quartile 1: play that enters an ad
  continuously now starts its first span at 0 (stream-audit §6.3), as
  leaving it already ran the last span to the ad's end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The record wai.delivery.audit and the magic WAS1 in REGISTRY.md and the
code registry, the export regenerated; a SPEC §5 paragraph for the
audit, and a sentence on settlement §14 in settlement's; the changelog
entry; the corpus in live-conformance's index; a step in
wai-conformance.yml (the module's tests, tests/stream_audit.rs and
stream_audit_verify.py) with the extension in its paths; and
wai-audit.yml for the collector on Linux and macOS and the player
integration with the browser end to end on the macOS host; the
collector's README.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Of twelve rules of web/audit.js and its adapters broken one at a time,
two first went uncaught by the Node tests: an ad entered between two
observations (the corpus's observations fall on the ad's start, so only
the browser end to end caught it), and a stall begun before the first
presentation counted. A test for each; all twelve now fail the tests.

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]>
- The falsifications found the check that refuses an acknowledgement the
  record does not name guarded by no vector in either implementation:
  the corpus gains ack_not_named (audit_bundle_invalid), and the Python
  verifier can drop that check too, which the corpus now catches.
- The collector's default port moves to 9694: 9690 is the stitched
  presentation tool's.
- stream-audit §6.3 states that this revision claims every marked ad
  with WPC1, and that a stitched ad is claimed with the stitched
  presentation record where a deployment holds it, its bundle then
  carrying the manifests its claims name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai-audit e2e: the collector ends with the run, however it ends
Some checks failed
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (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 / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (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
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
wai-audit collector and player integration / does this change touch it (pull_request) Has been cancelled
wai-audit collector and player integration / collector · linux (pull_request) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
d56916dac5
A run that failed left the collector it started running, holding the
output of whatever started the script open (the falsification harness
waited on it). It is now killed on the process's exit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot closed this pull request 2026-10-26 20:58:42 -04:00
Some checks failed
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (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 / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (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
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
wai-audit collector and player integration / does this change touch it (pull_request) Has been cancelled
wai-audit collector and player integration / collector · linux (pull_request) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (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!474
No description provided.