wai.age.assurance: ratings carried through derivations, admission by an age threshold without identity, and the minors' profile (AGE) #409

Closed
dcharlot wants to merge 0 commits from wai/age-assurance into main
Owner

wai.age.assurance (AGE): ratings carried through derivations, admission by an age threshold without identity, and the minors' profile

Stacked on #398 (wai/ad-presentation). This branch reuses #398's Privacy Pass code (the RSASSA-PSS of RFC 9578 §6.4, the DER key reader and the token reader in live_ads::presentation) and its RFC 9578 Appendix A.2 vector. Merge #398 first. After that, this PR's diff is its own three commits. The branch also carries origin/main through #398's latest merge of it (#383, #400); main has not moved since.

What is in it

  • wai/extensions/age-assurance.md registers wai.age.assurance and three JSON records, each signed the way synthesis signs its records (JCS, Ed25519, BLAKE3 link hash):

    • wai-rating. A rating claim for an object, a track from a group on, a span of a track, or a room, under 1–8 schemes. Each scheme is named by its vocabulary URI and states its value and the minimum age that value means. WAI defines no scheme of its own. An object's age is the greatest age among the claims that cover it, so a claim can raise an age and never lower it.

      • A span rated late holds from its first group, whenever the claim arrives (§3.5).
      • Carry-forward (§3.4): a derived claim names its inputs' claims by link hash. A derivation is refused rating_dropped if it names no claim for a rated input, or drops or lowers a scheme an input is rated under. This follows synthesis §3.4.
      • Who may rate (§3.6): room keys rate the room; a track's owner rates its own track.
    • wai-age-policy. Signed by the room authority. It lists Privacy Pass issuers, with one key per threshold (1–21), so a type-0x0002 token says "at or over T" and nothing else.

    • wai-age-admissions. A receipt signed by a room key. It counts admissions per window, issuer and threshold. Cells under 50 are suppressed (live-metering's QoE rule, delivery-telemetry §8). A member beyond its table is refused receipt_identifies, whatever it holds.

    • Admission (§5.2) checks a token in six steps: it reads; its challenge is one the relay sent; its key belongs to the named issuer; its PSS signature verifies; its nonce is fresh; and its threshold is at least the age the track asks for. The relay learns the issuer and the threshold, nothing more (§5.3).

    • The grant attribute age states a grant holder's threshold (§5.4). The serving rule (§5.5) serves an object only to a session whose threshold is at least the object's age.

    • No return path (§6). The sink's age state is the greatest threshold of a device credential that holds under its provisioned anchor. The predicate is evaluated locally. The text states what a player owes, and that nothing leaves the device.

    • The minors' profile (§8) is named from the bitmaps the room already has; no new field:

      • recording mode 0;
      • no role holds the record or seek power;
      • no role that publishes nothing permits chat or reaction;
      • and no grant that publishes nothing permits them either.

      A room where a grant with minor=true that may publish takes effect must meet the profile, or it is refused minor_profile_unmet.

    • §10 maps each instrument onto the mechanism that answers it. §11 is the reasons table: 18 codes.

  • wai-rs:

    • module age_assurance (feature age_assurance = ["live_room", "live_ads"]), with unit tests;
    • bin wai_age_assurance_vectors;
    • tests/age_assurance.rs: the corpus regenerates byte for byte, every case gets its outcome, and §11 equals Reason::ALL.
  • The corpus, live-conformance/age/: 101 cases covering ratings, admission, serving, predicates, receipts and rooms, plus RFC 9578 A.2's first vector. Every §11 code refuses at least one case. All the requested negative controls are there:

    • 13_where_16 and 16_where_18: a lower-threshold token on a higher-rated track;
    • transcode_names_no_rating, …_lowers_a_scheme, …_drops_a_scheme, composition_drops_a_scheme, second_claim_dropped: a rating dropped through a derivation;
    • director_records, viewers_seek (a clip), viewers_chat, viewers_react, room_records, grant_narrowed_to_watch_and_chat and …_and_react: a minors' room granting clip or record;
    • nonce_in_a_cell and principal_at_the_top: a receipt carrying an identity;
    • small_cell_stated: a count under the floor reported.

    The README has a **Provenance.** line, and int_kernels_vectors.rs's live-conformance exclusion reason now names this corpus.

  • live-conformance/age_assurance_verify.py is the independent second implementation. A separate agent wrote it from the extension text alone, without reading wai-rs.

    • It verifies every RSASSA-PSS in its own arithmetic and reads rooms with live_rooms_verify.py.
    • It runs 38 mutations of the corpus and catches all of them.
    • It drops each of its own checks in turn: 79 of 85 are caught by the corpus. The other 6 are listed under "Not done".
  • live-recording §4.4 step 5 now refuses a recording package of a room whose recording mode is 0 (recording_not_permitted, capture-binding's code), whether or not it holds takes. The Rust and live_record_verify.py both check it, and the recording corpus gains room_never_recorded. §8.2 gains the minors'-room DVR rule.

  • The relay:

    • joule-moq's GrantHead now reads the minor attribute.
    • Once a session proves a minor's publishing grant, joule-moq-relay's DVR store keeps nothing of that room. Its effect is the same as for a sealed room: what was kept is deleted, nothing more is written, and the room is recorded in sealed.wdl, so the rule survives a restart.
    • New test tests/dvr.rs::a_minors_room_is_never_kept. It also shows that a minor's grant that publishes nothing changes nothing.
  • live-rooms.md §4.4 lists age and points minor at the profile; §10.4 points to the age policy.

  • Registry and docs: REGISTRY.md prose, the registry export (wai.age.assurance and the three document kinds; --check clean), a SPEC §5 paragraph, a CHANGELOG entry, live-conformance/README.md, the relay README, and the wai-conformance.yml step and path.

Design decisions, and why

  1. Test keys. RFC 9578 publishes one issuer key, and the design needs three. The other two are RFC 9500 §2.1 testRSA2048 and RFC 7517 Appendix A.2's RSA key, each wrapped in RFC 9578 §6.5's SubjectPublicKeyInfo; all three have published private keys. Thresholds: 13 → RFC 7517, 16 → RFC 9500, 18 → RFC 9578. RFC 9500's testRSA1024 is the "1024-bit key" refusal. No new cryptography: tokens are minted by pss_sign from #398.
  2. Ratings are their own JSON record, not a jwp-receipts claim class. That keeps clear of T2 (#402), which edits jwp-receipts' class table. Carry-forward uses the synthesis model: a derived claim's inputs name the input claims by link hash. A null rating for an input the verifier holds a claim about counts as a drop.
  3. Gating uses the greatest age across schemes. A relay does not know a viewer's jurisdiction, so it gates on that one number. Each scheme's label is still kept, and a sink may show the label of the scheme its own settings name.
  4. Grant holders state their threshold in age. Without it, a rated room could gate only token viewers.
  5. receipt_identifies fires on any extra member. A verifier cannot tell an identifier from any other value, so it refuses every place one could ride.
  6. The minors' profile is named, not added. The role bitmaps already express it, except one gap the independent reading found: a grant can narrow a publishing role down to publish nothing while keeping chat (live-rooms §4.3 step 7 allows this). That is rule 4, applied to grants.
    • Gifts: WAI defines none, so there is nothing to remove.
    • Clips: a clip needs a recording or a DVR window, and a minors' room has neither.
    • Capture by tools outside the service (ICU F3(e)) is stated plainly as beyond WAI's records.
  7. The trigger is a minor's publishing grant taking effect.
    • capture-binding's guardian-consented minor take stays valid under capture-binding's own verifier.
    • A room that records cannot meet the profile, so such a room is outside what this extension admits.
    • The lead should confirm this is the intended split. The alternative is a guardian-consent exception, which I did not invent.
  8. The DVR rule lives in the relay. The relay already reads every grant presented to it, and a room service cannot stop a relay from storing what it carries.
    • The room service needs no change to enforce the profile, because it issues no minor=true grants today.
    • After this lands, the live room must be redeployed as one unit: page, relay and room service. The relay changed, and the room service builds on joule-moq and the relay by path.
  9. Owner's rule on consent. The text sets no consent requirement. It states what a token, a receipt and a count reveal, and to whom. Where a regime asks the operator for a lawful basis, the text points to the lawful-basis claim class (jwp-receipts §2.4.6).
  10. Sources: verified, and not.
    • Verified:
      • Recommendation (EU) 2026/1035: recitals (12) and (13), points (4) and (5). Read from the Publications Office cellar.
      • Guidelines C/2025/5519: paragraphs 46 and 57.
      • The UK consultation Additional Safety Measures (30 June 2025): measure ICU F3. Read from the consultation document.
      • Act No. 127, 2024: sections 63D, 63DA and 63DB, as made.
      • RFC 9576, 9577 and 9578; draft-ietf-moq-privacy-pass-auth-03; ETSI TS 102 796 A.2.28.
    • Not verified, and marked so in the text:
      • the Protection of Children Codes (July 2025);
      • the 10 December 2025 day-of-effect instrument;
      • whether the UK consultation has become a statement.

A finding for live-rooms

draft-ietf-moq-privacy-pass-auth-03 §3.2.4 gives MoQAuthorizationInfo.scopes a one-byte length (at most 255 bytes). live-rooms §10.4's subscribing scopes for max_layer 0, written over a 64-digit room id, take four scopes and 320 bytes, so they cannot be encoded. Three of them fill 246. The corpus's relay sends one prefix scope over the room and holds each track to §5.5; age-assurance §5.1 says so. live-rooms §10.4 needs a follow-up.

Checks run (all on the build host via rb unless marked local)

check command result
wai-rs lib, all tests under the feature (registry included) cargo test --locked --no-default-features --features age_assurance --lib 656 passed
corpus and reasons cargo test --locked --no-default-features --features age_assurance --test age_assurance 3 passed
live-recording cargo test … --features live_record --lib -- live_record:: output_tree::; --test live_record 15 passed; 5 passed
dependents of the recording corpus --features live_moderation --test live_moderation; --features capture_take --test capture_take; --features live_room --test live_room 3, 4, 4 passed
corpus exclusion reasons --features neural_int,video_int,audio_int,world_int,splat_int --test int_kernels_vectors every_decoded_output_states_a_sha256 passed
registry export cargo run --no-default-features --bin wai -- registry export --check up to date
clippy, wai-rs cargo clippy --locked --no-default-features --features age_assurance,live_moderation --all-targets no note in touched files (one collapsible_match fixed in 101d31ee0)
joule-moq cargo test --locked -p joule-moq --lib grant_proof 5 passed
joule-moq-relay cargo test --locked -p joule-moq-relay --test dvr (3 runs), --lib, --test room_authority --test grant_proof 8/8 three times; 34; 1 + 1
joule-moq-room (own workspace and lock) cargo test --locked; cargo clippy --all-targets pass; no new notes
clippy, joule-code cargo clippy --locked -p joule-moq -p joule-moq-relay --all-targets only a note on a pre-existing line of session.rs (319, moved by the insertion)
second implementation (local) python3 -I wai/live-conformance/age_assurance_verify.py wai/live-conformance/age exit 0: 101 cases, 38/38 mutations caught, 79 of 85 dropped checks caught
recording and moderation verifiers (local and host) live_record_verify.py recordings; live_moderation_verify.py moderation PASS (27 recordings, 34 clips; 30 rooms)
repo checks (local) 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
names (local) check-public-names.sh with the private denylist no hit in any file this PR touches; 9 hits, all in wai/wai-rs/src/live_provenance.rs, which this PR does not touch
lockfiles no dependency added no Cargo.lock changed

Falsifications

Each was run after committing. One rule was broken at a time, the guarding test was run, and the file was restored.

rule broken test result
§3.4 rule 2 (null rating for a rated input) --test age_assurance exit 101
§7.3 suppression floor --test age_assurance exit 101
§8.1 rule 4 (grants) --test age_assurance exit 101
§7.3 receipt_identifies --test age_assurance exit 101
§5.5 serve at or above the age --test age_assurance exit 101
§8.2 the profile at a join --test age_assurance exit 101
live-recording §4.4 step 5 (mode 0) --test live_record exit 101
relay hook (DVR keeps a minors' room) --test dvr a_minors_room exit 101
GrantHead minor reader -p joule-moq --lib grant_proof exit 101
Python: corpus input changed, stated refusal kept (13_where_16 asks 13) age_assurance_verify.py exit 1

Not done

  • The relay does not yet admit by Privacy Pass tokens or apply §5.5's serving rule. Both are specified and conformance-tested in wai-rs and the Python verifier, but the reference relay has neither. The relay does no rating checks either.
  • No issuer, attester or device credential store, and no player-side predicate. All are out of scope.
  • The room service has no minors' mode. It issues no minor=true grants today.
  • Rules no corpus case isolates (reported by the verifier as NOTEs):
    • JSON depth;
    • more than 16 inputs;
    • issuer-name syntax;
    • key count per issuer;
    • anchor_key: masked by design, since a wrong key id also fails PSS;
    • window_s 0: masked by the cell-overlap rule.
  • Reading orders the text pins but no case tells apart. The verifier's alternative readings also pass the corpus:
    • ratings in three passes versus claim by claim;
    • receipt admissions before the receipt;
    • room claim's room before its signer;
    • rule 4 over all grants given versus only those that take effect.
  • unidirectional-delivery.md is not edited, to stay out of T2's (#402) way. This extension cites its §2 and §3.

🤖 Generated with Claude Code


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

## wai.age.assurance (AGE): ratings carried through derivations, admission by an age threshold without identity, and the minors' profile **Stacked on #398 (`wai/ad-presentation`).** This branch reuses #398's Privacy Pass code (the RSASSA-PSS of RFC 9578 §6.4, the DER key reader and the token reader in `live_ads::presentation`) and its RFC 9578 Appendix A.2 vector. **Merge #398 first.** After that, this PR's diff is its own three commits. The branch also carries origin/main through #398's latest merge of it (#383, #400); main has not moved since. ### What is in it - **`wai/extensions/age-assurance.md`** registers `wai.age.assurance` and three JSON records, each signed the way synthesis signs its records (JCS, Ed25519, BLAKE3 link hash): - **`wai-rating`.** A rating claim for an object, a track from a group on, a span of a track, or a room, under 1–8 schemes. Each scheme is named by its vocabulary URI and states its value and the minimum age that value means. WAI defines no scheme of its own. An object's age is the greatest age among the claims that cover it, so a claim can raise an age and never lower it. - A span rated late holds from its first group, whenever the claim arrives (§3.5). - Carry-forward (§3.4): a derived claim names its inputs' claims by link hash. A derivation is refused `rating_dropped` if it names no claim for a rated input, or drops or lowers a scheme an input is rated under. This follows synthesis §3.4. - Who may rate (§3.6): room keys rate the room; a track's owner rates its own track. - **`wai-age-policy`.** Signed by the room authority. It lists Privacy Pass issuers, with **one key per threshold** (1–21), so a type-0x0002 token says "at or over T" and nothing else. - **`wai-age-admissions`.** A receipt signed by a room key. It counts admissions per window, issuer and threshold. Cells under 50 are suppressed (live-metering's QoE rule, delivery-telemetry §8). A member beyond its table is refused `receipt_identifies`, whatever it holds. - **Admission** (§5.2) checks a token in six steps: it reads; its challenge is one the relay sent; its key belongs to the named issuer; its PSS signature verifies; its nonce is fresh; and its threshold is at least the age the track asks for. The relay learns the issuer and the threshold, nothing more (§5.3). - The grant attribute **`age`** states a grant holder's threshold (§5.4). The **serving rule** (§5.5) serves an object only to a session whose threshold is at least the object's age. - **No return path** (§6). The sink's age state is the greatest threshold of a device credential that holds under its provisioned anchor. The predicate is evaluated locally. The text states what a player owes, and that nothing leaves the device. - **The minors' profile** (§8) is named from the bitmaps the room already has; no new field: - recording `mode` 0; - no role holds the record or seek power; - no role that publishes nothing permits `chat` or `reaction`; - and no *grant* that publishes nothing permits them either. A room where a grant with `minor=true` that may publish takes effect must meet the profile, or it is refused `minor_profile_unmet`. - §10 maps each instrument onto the mechanism that answers it. §11 is the reasons table: 18 codes. - **`wai-rs`:** - module `age_assurance` (feature `age_assurance = ["live_room", "live_ads"]`), with unit tests; - bin `wai_age_assurance_vectors`; - `tests/age_assurance.rs`: the corpus regenerates byte for byte, every case gets its outcome, and §11 equals `Reason::ALL`. - **The corpus, `live-conformance/age/`:** 101 cases covering ratings, admission, serving, predicates, receipts and rooms, plus RFC 9578 A.2's first vector. Every §11 code refuses at least one case. All the requested negative controls are there: - `13_where_16` and `16_where_18`: a lower-threshold token on a higher-rated track; - `transcode_names_no_rating`, `…_lowers_a_scheme`, `…_drops_a_scheme`, `composition_drops_a_scheme`, `second_claim_dropped`: a rating dropped through a derivation; - `director_records`, `viewers_seek` (a clip), `viewers_chat`, `viewers_react`, `room_records`, `grant_narrowed_to_watch_and_chat` and `…_and_react`: a minors' room granting clip or record; - `nonce_in_a_cell` and `principal_at_the_top`: a receipt carrying an identity; - `small_cell_stated`: a count under the floor reported. The README has a `**Provenance.**` line, and `int_kernels_vectors.rs`'s `live-conformance` exclusion reason now names this corpus. - **`live-conformance/age_assurance_verify.py`** is the independent second implementation. A separate agent wrote it from the extension text alone, without reading wai-rs. - It verifies every RSASSA-PSS in its own arithmetic and reads rooms with `live_rooms_verify.py`. - It runs 38 mutations of the corpus and catches all of them. - It drops each of its own checks in turn: 79 of 85 are caught by the corpus. The other 6 are listed under "Not done". - **live-recording §4.4 step 5** now refuses a recording package of a room whose recording `mode` is 0 (`recording_not_permitted`, capture-binding's code), whether or not it holds takes. The Rust and `live_record_verify.py` both check it, and the recording corpus gains `room_never_recorded`. §8.2 gains the minors'-room DVR rule. - **The relay:** - `joule-moq`'s `GrantHead` now reads the `minor` attribute. - Once a session proves a minor's publishing grant, `joule-moq-relay`'s DVR store keeps nothing of that room. Its effect is the same as for a sealed room: what was kept is deleted, nothing more is written, and the room is recorded in `sealed.wdl`, so the rule survives a restart. - New test `tests/dvr.rs::a_minors_room_is_never_kept`. It also shows that a minor's grant that publishes nothing changes nothing. - **live-rooms.md** §4.4 lists `age` and points `minor` at the profile; §10.4 points to the age policy. - **Registry and docs:** REGISTRY.md prose, the registry export (`wai.age.assurance` and the three document kinds; `--check` clean), a SPEC §5 paragraph, a CHANGELOG entry, `live-conformance/README.md`, the relay README, and the `wai-conformance.yml` step and path. ### Design decisions, and why 1. **Test keys.** RFC 9578 publishes one issuer key, and the design needs three. The other two are RFC 9500 §2.1 `testRSA2048` and RFC 7517 Appendix A.2's RSA key, each wrapped in RFC 9578 §6.5's SubjectPublicKeyInfo; all three have published private keys. Thresholds: 13 → RFC 7517, 16 → RFC 9500, 18 → RFC 9578. RFC 9500's `testRSA1024` is the "1024-bit key" refusal. No new cryptography: tokens are minted by `pss_sign` from #398. 2. **Ratings are their own JSON record, not a jwp-receipts claim class.** That keeps clear of T2 (#402), which edits jwp-receipts' class table. Carry-forward uses the synthesis model: a derived claim's `inputs` name the input claims by link hash. A `null` rating for an input the verifier holds a claim about counts as a drop. 3. **Gating uses the greatest age across schemes.** A relay does not know a viewer's jurisdiction, so it gates on that one number. Each scheme's label is still kept, and a sink may show the label of the scheme its own settings name. 4. **Grant holders state their threshold in `age`.** Without it, a rated room could gate only token viewers. 5. **`receipt_identifies` fires on any extra member.** A verifier cannot tell an identifier from any other value, so it refuses every place one could ride. 6. **The minors' profile is named, not added.** The role bitmaps already express it, except one gap the independent reading found: a grant can narrow a publishing role down to publish nothing while keeping `chat` (live-rooms §4.3 step 7 allows this). That is rule 4, applied to grants. - **Gifts:** WAI defines none, so there is nothing to remove. - **Clips:** a clip needs a recording or a DVR window, and a minors' room has neither. - **Capture by tools outside the service** (ICU F3(e)) is stated plainly as beyond WAI's records. 7. **The trigger is a minor's publishing grant taking effect.** - capture-binding's guardian-consented minor take stays valid under capture-binding's own verifier. - A room that records cannot meet the profile, so such a room is outside what this extension admits. - **The lead should confirm this is the intended split.** The alternative is a guardian-consent exception, which I did not invent. 8. **The DVR rule lives in the relay.** The relay already reads every grant presented to it, and a room service cannot stop a relay from storing what it carries. - The room service needs no change to enforce the profile, because it issues no `minor=true` grants today. - **After this lands, the live room must be redeployed as one unit: page, relay and room service.** The relay changed, and the room service builds on `joule-moq` and the relay by path. 9. **Owner's rule on consent.** The text sets no consent requirement. It states what a token, a receipt and a count reveal, and to whom. Where a regime asks the operator for a lawful basis, the text points to the `lawful-basis` claim class (jwp-receipts §2.4.6). 10. **Sources: verified, and not.** - **Verified:** - Recommendation (EU) 2026/1035: recitals (12) and (13), points (4) and (5). Read from the Publications Office cellar. - Guidelines C/2025/5519: paragraphs 46 and 57. - The UK consultation *Additional Safety Measures* (30 June 2025): measure ICU F3. Read from the consultation document. - Act No. 127, 2024: sections 63D, 63DA and 63DB, as made. - RFC 9576, 9577 and 9578; draft-ietf-moq-privacy-pass-auth-03; ETSI TS 102 796 A.2.28. - **Not verified, and marked so in the text:** - the Protection of Children Codes (July 2025); - the 10 December 2025 day-of-effect instrument; - whether the UK consultation has become a statement. ### A finding for live-rooms draft-ietf-moq-privacy-pass-auth-03 §3.2.4 gives `MoQAuthorizationInfo.scopes` a one-byte length (at most 255 bytes). live-rooms §10.4's subscribing scopes for `max_layer` 0, written over a 64-digit room id, take four scopes and 320 bytes, so they cannot be encoded. Three of them fill 246. The corpus's relay sends one prefix scope over the room and holds each track to §5.5; age-assurance §5.1 says so. live-rooms §10.4 needs a follow-up. ### Checks run (all on the build host via `rb` unless marked local) | check | command | result | |---|---|---| | wai-rs lib, all tests under the feature (registry included) | `cargo test --locked --no-default-features --features age_assurance --lib` | 656 passed | | corpus and reasons | `cargo test --locked --no-default-features --features age_assurance --test age_assurance` | 3 passed | | live-recording | `cargo test … --features live_record --lib -- live_record:: output_tree::`; `--test live_record` | 15 passed; 5 passed | | dependents of the recording corpus | `--features live_moderation --test live_moderation`; `--features capture_take --test capture_take`; `--features live_room --test live_room` | 3, 4, 4 passed | | corpus exclusion reasons | `--features neural_int,video_int,audio_int,world_int,splat_int --test int_kernels_vectors every_decoded_output_states_a_sha256` | passed | | registry export | `cargo run --no-default-features --bin wai -- registry export --check` | up to date | | clippy, wai-rs | `cargo clippy --locked --no-default-features --features age_assurance,live_moderation --all-targets` | no note in touched files (one `collapsible_match` fixed in 101d31ee0) | | joule-moq | `cargo test --locked -p joule-moq --lib grant_proof` | 5 passed | | joule-moq-relay | `cargo test --locked -p joule-moq-relay --test dvr` (3 runs), `--lib`, `--test room_authority --test grant_proof` | 8/8 three times; 34; 1 + 1 | | joule-moq-room (own workspace and lock) | `cargo test --locked`; `cargo clippy --all-targets` | pass; no new notes | | clippy, joule-code | `cargo clippy --locked -p joule-moq -p joule-moq-relay --all-targets` | only a note on a pre-existing line of session.rs (319, moved by the insertion) | | second implementation (local) | `python3 -I wai/live-conformance/age_assurance_verify.py wai/live-conformance/age` | exit 0: 101 cases, 38/38 mutations caught, 79 of 85 dropped checks caught | | recording and moderation verifiers (local and host) | `live_record_verify.py recordings`; `live_moderation_verify.py moderation` | PASS (27 recordings, 34 clips; 30 rooms) | | repo checks (local) | `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 | | names (local) | `check-public-names.sh` with the private denylist | no hit in any file this PR touches; 9 hits, all in `wai/wai-rs/src/live_provenance.rs`, which this PR does not touch | | lockfiles | no dependency added | no Cargo.lock changed | ### Falsifications Each was run after committing. One rule was broken at a time, the guarding test was run, and the file was restored. | rule broken | test | result | |---|---|---| | §3.4 rule 2 (null rating for a rated input) | `--test age_assurance` | exit 101 | | §7.3 suppression floor | `--test age_assurance` | exit 101 | | §8.1 rule 4 (grants) | `--test age_assurance` | exit 101 | | §7.3 `receipt_identifies` | `--test age_assurance` | exit 101 | | §5.5 serve at or above the age | `--test age_assurance` | exit 101 | | §8.2 the profile at a join | `--test age_assurance` | exit 101 | | live-recording §4.4 step 5 (mode 0) | `--test live_record` | exit 101 | | relay hook (DVR keeps a minors' room) | `--test dvr a_minors_room` | exit 101 | | GrantHead `minor` reader | `-p joule-moq --lib grant_proof` | exit 101 | | Python: corpus input changed, stated refusal kept (`13_where_16` asks 13) | `age_assurance_verify.py` | exit 1 | ### Not done - **The relay does not yet admit by Privacy Pass tokens or apply §5.5's serving rule.** Both are specified and conformance-tested in wai-rs and the Python verifier, but the reference relay has neither. The relay does no rating checks either. - **No issuer, attester or device credential store, and no player-side predicate.** All are out of scope. - **The room service has no minors' mode.** It issues no `minor=true` grants today. - **Rules no corpus case isolates** (reported by the verifier as NOTEs): - JSON depth; - more than 16 inputs; - issuer-name syntax; - key count per issuer; - `anchor_key`: masked by design, since a wrong key id also fails PSS; - `window_s` 0: masked by the cell-overlap rule. - **Reading orders the text pins but no case tells apart.** The verifier's alternative readings also pass the corpus: - ratings in three passes versus claim by claim; - receipt admissions before the receipt; - room claim's room before its signer; - rule 4 over all grants given versus only those that take effect. - **unidirectional-delivery.md is not edited**, to stay out of T2's (#402) way. This extension cites its §2 and §3. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- **Closed:** Landed in #428 (merge train, merged as aa61ab3b with all 65 checks green).
A player's signed claim (WPC1) that it presented one impression of a live-ads
break: the creative's objects it presented, spans of its media time with the
fraction of pixels in view and audibility, the geometry they were measured over,
a rendered-frame count, and an optional RFC 9577 token bound to the impression
through its challenge and verified as RFC 9578 §6.4 does for type 0x0002.
An ad policy of version 2 requires claims (and optionally an attestation and an
audible run) under the video or cross-media viewability rule; the room
verifier's spend-equivalence then counts only impressions whose claim verifies.
The corpus live-conformance/presentation/ is generated from fixed seeds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
extensions/ad-presentation.md defines the WPC1 claim, the session a session key
names, the creative digest, the video and cross-media viewability rules as their
texts state them, the quartile, the RFC 9577 token bound to one impression and
verified as RFC 9578 §6.4 has it, the presentation fields of a WAP1 policy of
version 2, and each claim's verdict; live-ads.md takes the version and the spend
that counts. ad_presentation_verify.py is written from the text, verifies RFC
9578's own vector, gives every claim the corpus's verdict, and catches every
mutated copy and every check dropped from itself. The corpus gains a signed
claim that breaks a span rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The registry lists the record and the WPC1 magic (the export regenerated), SPEC
§5 describes the claim and the billing rule, and the CHANGELOG records them.
joule-moq-web gains web/ads/presentation.js, a span recorder over a video
element and the claim's writer and signer on WebCrypto's Ed25519, whose node
tests turn observation sequences into the corpus's claims byte for byte; no page
plays ads yet. wai-conformance runs the module's tests, the corpus test,
ad_presentation_verify.py and the producer's tests; joule-moq-web's e2e script
runs the producer's tests with the page's other unit tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merge remote-tracking branch 'origin/main' into wai/ad-presentation
Some checks failed
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m43s
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 10m2s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 2m5s
wai-pack / wai-pack · linux (pull_request) Successful in 1m25s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 19m14s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m2s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 3m49s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 5m34s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 16s
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 29s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m52s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Failing after 14m39s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 4m47s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 11m17s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 53s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m30s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 4m27s
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
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (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
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
fe66f6d2de
# Conflicts:
#	.github/workflows/wai-conformance.yml
#	wai/CHANGELOG.md
#	wai/REGISTRY.md
#	wai/SPEC.md
#	wai/live-conformance/README.md
#	wai/wai-rs/src/codecs/registry.rs
Merge origin/main into wai/ad-presentation (IETF draft, timing-test fix)
All checks were successful
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m0s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 32s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 10m19s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 1m44s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 18m58s
wai-pack / wai-pack · linux (pull_request) Successful in 1m34s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m8s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 3m59s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m7s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 4m41s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 2m24s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 3m22s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 1m39s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 5m20s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 31m57s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 4m14s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 15m9s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 1m48s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 10m17s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 2m55s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 8m44s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 50m59s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 9m8s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 14m52s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 19s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m20s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 6m59s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 3m20s
wai-pack / wai-pack · macos (pull_request) Successful in 1m17s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 2m54s
b2f22ea8c4
Co-Authored-By: Claude Opus 5.5 <[email protected]>
extensions/age-assurance.md registers the record: wai-rating claims for an
object, a track, a span or a room under named schemes, each stating the
minimum age its value means, carried through every derivation (a scheme an
input is rated under is never dropped or lowered); a room's wai-age-policy of
Privacy Pass issuers with one key per threshold; admission by a token of RFC
9578's publicly verifiable type, the relay learning only the issuer and the
threshold; the serving rule, a span's rise holding from its first group; the
sink's age state and predicate with no return path; wai-age-admissions
receipts, every count under 50 suppressed and no member a session could ride
in; and the minors' profile over a descriptor's role bitmaps.

wai-rs gains the age_assurance module (feature age_assurance), reusing the
ad-presentation module's RSASSA-PSS and token reader, and the corpus
live-conformance/age/ written by wai_age_assurance_vectors from fixed seeds,
its issuer keys the published test keys of RFC 9578 A.2, RFC 9500 and RFC
7517.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The independent verifier's reading of the text found that a grant may narrow a
publishing role to publish nothing and keep its chat: §8.1 gains rule 4 over
grants, held at the first record that puts a minor's publishing grant into
effect. §2, §3.4, §3.6, §5.2, §6.2, §7.3 and §9.1 now pin the reading orders
and the attribution of claims, and the corpus covers the rules it did not
reach.

live-recording §4.4 step 5 refuses a package of a room whose recording policy
has mode 0 (recording_not_permitted); its corpus gains room_never_recorded and
live_record_verify.py the check. joule-moq's grant head reads the minor
attribute, and joule-moq-relay's DVR store keeps nothing of a room once a
session proves a minor's publishing grant of it, as it does for a sealed room,
across a restart; tests/dvr.rs shows it.

live-conformance/age_assurance_verify.py is the second implementation, written
from the text: it shares no code with wai-rs, verifies every RSASSA-PSS in its
own arithmetic, and catches every mutated copy and each of its own dropped
checks. The registry export, SPEC §5, the CHANGELOG, the corpus exclusion
reason and the wai-conformance step follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
age_assurance: a track claim's authorisation as one match guard (clippy collapsible_match)
All checks were successful
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m16s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m3s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 26s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 10m22s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 1m52s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 19m22s
wai-pack / wai-pack · linux (pull_request) Successful in 1m20s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 2m39s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 4m15s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 8m30s
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 / v2 vocabulary · manifests validate as stated (pull_request) Successful in 16s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 10s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 24s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m11s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 16m28s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 7m50s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m21s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 23s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m18s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 4m9s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 31m37s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m37s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 12m56s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 18s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 42s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 4m15s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 2m26s
wai-pack / wai-pack · macos (pull_request) Successful in 1m5s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 2m11s
101d31ee01
Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot closed this pull request 2026-10-22 12:02:15 -04:00
All checks were successful
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 4s
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 3m7s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m32s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m37s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 1m25s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 1m46s
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 4s
WAI energy receipts / does this change touch it (pull_request) Successful in 5s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 42s
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 5s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
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 1m16s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m3s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 26s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 10m22s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 1m52s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 19m22s
wai-pack / wai-pack · linux (pull_request) Successful in 1m20s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 2m39s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 4m15s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 8m30s
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 / v2 vocabulary · manifests validate as stated (pull_request) Successful in 16s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 10s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 24s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m11s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 16m28s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 7m50s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m21s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 23s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m18s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 4m9s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 31m37s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m37s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 12m56s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 18s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 42s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 4m15s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 2m26s
wai-pack / wai-pack · macos (pull_request) Successful in 1m5s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 2m11s

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!409
No description provided.