live room train D: co-watch rooms, the room's own service under age rules, same-origin room endpoints (#432) #451

Merged
dcharlot merged 21 commits from train/live-room-d into main 2026-10-25 00:42:25 -04:00
Owner

Live-room train D: co-watch (#432) merged onto #428's live room, the age × co rule, and the room page's unclosed section.

Base: #428's head (793bd478, then its fix 9f63561b, merged in).

part what
#432 wai.live.cowatch: watch rooms, merged by meaning
age × co age-assurance §5.5: the room's own service is not held to the serving rule
room page <section id="co"> closed; a markup test and an e2e check

How #432 was merged

#432 conflicted in the room service, the page, the e2e suite, both room workflows, and the registry lists.

One config reader.

  • Config::settings() now returns a Result, because it reads the watch source's file. It carries the following:
    • #428's age, audience_input, latency_budget_ms, lead and ai;
    • the watch room's watch (from watch_source and watch_duration_ms), watch_control and watch_window_ms.
  • There is one public_prefix. Both sides had added it, so the auto-merge had left two fields in Config and two keys in room.example.toml. The example config test caught the duplicate key.
  • The service refuses a watch room that is also e2ee, [age] or [co]:
    • with e2ee, the source is served in the clear;
    • with [age], the source is served without a grant, so no age check could apply to it;
    • co-performance and co-watch are not specified together.

One path for the clock's records.

  • #428's co-performance has Item::{Life, Meta} and emit_items, which keeps the clock's rate.
  • #432 had added a second path, Lifecycle::Meta. It is dropped:
    • skew statements go out through emit_meta;
    • leaves go out through emit_after, which carries a least t;
    • every record of the clock's chain reaches the watch log through watch_record.
  • Service::start spawns run_watch for a watch room.

Roles and grants.

  • A participant gets meta in a room with co-performance or in a watch room.
  • A watch room's viewer gets WATCHER_KINDS (meta, chat). An audience member keeps AUDIENCE_KINDS.
  • watch_control is appended last among the grant attributes, which keeps their key order.

Page and e2e.

  • app.js has the co-watch mode beside co-performance and the binding.
  • The e2e suite keeps every scenario and adds cowatch.
  • run.sh runs all the page's node tests.

Workflows and lists.

  • Workflow steps are kept whole, and paths: lists are unioned.
  • CHANGELOG and REGISTRY rows come from both sides.
  • capabilities.json and v2.ttl were regenerated by registry export.

The age × co rule (age-assurance §5.5)

The problem. In a room with both [age] and [co], the clock follows and logs every performer's objects over a relay session of its own. That session holds no grant and no token, so the relay served it nothing rated. Its forwarding SUBSCRIBE to the rated video was refused, so it never recorded the lead's join and never opened its events track. Every page then waited for the clock, and the room never started. The falsified e2e below shows exactly this.

The rule. A session that proves possession of a room key is the room's own service, and the relay serves it every object of the room.

  • The proof. It is a GRANT_PROOF (live-rooms §10.5.2) whose grant is 32 bytes of zero, made over the session's challenge.
  • The check. The relay checks it as live-rooms §10.5.4 checks a proof, with the zero digest as the grant's and the proof's key as the principal.
    • A proof that does not pass refuses the session with that section's reason (UNAUTHORIZED).
    • A proof that passes by a key that is no room key gives the session nothing.
  • What is not enough. No name, token or flag that a client sets makes a session the room's.

Spec.

  • age-assurance.md §5.5 states the rule; §11 names the reasons it uses and §12 the new vectors.
  • co-performance.md §8 points to it from the paragraph on the clock's own session.

Relay (joule-moq-relay).

  • In SETUP, the relay checks every proof of a room key (grant_proof::room_key_proofs).
  • SessionCtx::service holds the rooms each proved key covers:
    • an authority of [[auth.authorities]] covers its own rooms;
    • a key in [floor] room_keys or [dvr] room_keys covers every room.
  • threshold() is past every age for the room's own service.
  • ClientOptions::room_keys presents such proofs.

Room service (joule-moq-room). The clock's session proves both room keys it holds, so a relay that holds either of them recognises it. relay-lines and the README say so.

wai-rs, the corpus and the Python verifier.

  • age_assurance::room_service implements the rule.
  • The serving case room_service has 13 requests:
    • the clock's proof and the authority's, served at threshold 0;
    • no proof, and the performer's proof, both served by their thresholds;
    • proofs refused with the reason in brackets, at any threshold: a forged signature (pop_bad_signature), another session's challenge (pop_wrong_challenge), another relay (pop_wrong_context), a named grant (pop_wrong_grant) and a cut proof (pop_malformed).
  • age_assurance_verify.py learned the rule, with three new checks and four new mutations. joule-moq's tests/rating_vectors.rs, the corpus's third reader, checks those requests as the relay does.

Tests.

  • tests/age_room_service.rs is the relay test with age and co both in play: a room rated 18 and a floor held by the clock's key.
    • Sessions proving the clock's key and the authority's are served every group, and those groups verify.
    • The clock's token with no proof, and a proof by a key that is no room key, are refused below_threshold.
    • A proof that names the clock's key under another key's signature closes the session with UNAUTHORIZED. So does the clock's proof over another session's challenge.
  • The e2e scenario ageco is a room with both [age] and [co]:
    • both performers enter at 18;
    • the relay logs the clock's session as the room's own service;
    • the clock logs both tracks of both performers, the lead's video rated 18 among them, and marks an object played out of turn;
    • a viewer at 16 is still refused that video.

The room page's unclosed section

The cause. Train B's merge of #424 with #413 (173981739) left <section id="co"> without its </section>. The page opened five sections and closed four, so the parser nested #binding-debug and the Log inside the hidden #co. In every room without co-performance, neither ever showed.

The fix (984d95b2).

  • index.html has the missing </section>.
  • web/room/markup.test.mjs, run by run.sh, checks three things:
    • every element is closed, in order;
    • #log, #binding-debug and #cowatch are outside #co;
    • each mode's controls are inside its own section.
  • The e2e checks the same thing in the browser:
    • in service, a room without [co], the Log shows;
    • in cowatch, the co-watch section shows outside #co.

The room page takes only its own origin's endpoints (security fix, cb766e71)

The problem. The room client took its endpoints from its query string as given: the relay
(ws, wt), the room service (service), the test room (fixture), certificate hashes
(hashes) and the issuer directory (issuer_directory). It also followed a relay's GOAWAY to
any URI. A crafted link and a press of Join would send a joiner's camera and microphone to
another origin.

The rule (web/room/endpoint.js).

  • An endpoint is taken only on the page's own origin: https://<host>, or wss://<host> for a
    relay.
  • A URL carrying user information is never taken.
  • A page served from the loopback interface (a fixture page, or the e2e harness) takes loopback
    addresses alone.
  • hashes are taken on a loopback page alone.

The page (app.js).

  • A refused endpoint is recorded in snapshot().refused and shown to the visitor.
  • Join stays off, and the page connects to nothing.
  • enter() checks the relay before it asks for the camera, a grant or a session.
  • A GOAWAY naming another origin is refused: the page closes its session and says so.

Tests.

  • web/room/endpoint.test.mjs (run by run.sh) covers:
    • same origin;
    • another host or port;
    • scheme-relative //evil;
    • the page's host as user information;
    • javascript: and data:;
    • a plain scheme on a secure page;
    • an HTTP page;
    • loopback for the harness.
  • The e2e scenario origin covers three cases:
    • a foreign ?ws= is refused at Join, and nothing connects: no WebSocket, no request after
      the press, no grant, no camera;
    • a foreign ?service= (scheme-relative, behind user information, or javascript:) is
      refused, Join stays off, and no request leaves the origin;
    • a GOAWAY to another origin is refused, and the session ends with a status.
  • The service scenario now checks that every request and WebSocket of every page stays on
    the page's origin. That is the production connect-src 'self' wss://<host> blob:.

Falsified. The check was dropped, so every URL that parses was taken:

  • endpoint.test.mjs exits 1 (5 of 6 fail);
  • the e2e origin scenario exits 1 (the page takes the foreign relay).

One production note. A room with [age] fetches its Privacy Pass token from the issuer's
origin by design. connect-src must name each issuer before [age] runs in production.

Main merged, and a stale lock file

origin/main (41c5bafb, #428 landed as aa61ab3b) merged cleanly.

Main's wai-quantum 0.6.14 release left joule-moq-room/Cargo.lock at 0.6.12. With it, the
room service's --locked build refused to run. ea7efb97 refreshes it with cargo metadata;
the one version line changes. After that:

  • cargo metadata --locked --offline holds in joule-moq-room, joule-code-rs, wai-live-web and
    wai-rs;
  • scripts/check-lockfiles.sh reports 47 lockfiles, all current;
  • the room service builds --locked in run.sh, and its 17 tests pass.

Checks (build host, through rb)

At the final head ea7efb97:

check result
browser suite, run.sh --runs 1, with origin 1 of 1 runs, 269 of 269 checks; 46 node tests pass
joule-moq-room cargo test --locked --all-targets 17 passed
scripts/check-lockfiles.sh 47 lockfiles, all current
wfsteps.py HEAD origin/main pr/432 3 workflow files, 0 problem(s)
ruby -ryaml over .github/workflows/*.yml 46 parsed
the 8 repo checks all exit 0
public names (changed files, commit messages) only the existing BROWSER_BIN line

At 5aacacfa, before the fix and the main merge:

The full cargo and verifier run was at 298eaa70. The commits after it touch only joule-moq/tests/rating_vectors.rs and wai-conformance.yml (from 9f63561b). Three things were run again after them:

  • at 9a7f953f: joule-moq's tests and the e2e, twice;
  • at the final head 5aacacfa, which changes only one assertion's form in that test: joule-moq's clippy and rating_vectors;
  • the workflow checks.
check command result
browser e2e, twice e2e/run.sh --runs 2: compose, webtransport, websocket, goaway, dvr, service, e2ee, age, co, ageco, binding, cowatch 2 of 2 runs, 506 of 506 checks
page modules node --test over capsule, media, presentation, privacy-pass, binding, cowatch, markup pass
joule-moq cargo test -p joule-moq --features quic --all-targets; --features secure-objects 141; 148 passed
joule-moq-relay cargo test -p joule-moq-relay --all-targets 90 passed
relay timing tests ×5 --test dvr, floor, age_rising, age_serving, age_room_service, publish_done_streams 30 of 30 runs pass
joule-moq-room cargo test --all-targets 17 passed
joule-moq-web, ingest, interop --lib; --features e2ee --lib; ingest --all-targets; interop --all-targets 9; 11; 33; 0 (no tests)
joule-moq-web wasm32 cargo build -p joule-moq-web --target wasm32-unknown-unknown builds
clippy joule-moq, joule-moq-relay, joule-moq-web, joule-moq-room --all-targets; wai-rs age_assurance,co_watch --all-targets no new notes
MOQT decoders moqt22_check.py, moqt18_check.py, moqt16_check.py pass
wai-rs, new or touched age_assurance lib, test; co_watch lib, test 9, 3; 7, 3 passed
corpora regenerate wai_age_assurance_vectors verify; wai_co_watch_vectors verify; wai_browser_sink_vectors verify byte for byte
wai-rs, the other slices co_performance, takedown, browser_sink, live_room, live_record, live_events (lib and test); --lib codecs:: (both feature sets); corpus_provenance, cli_registry; int_kernels digests all pass
include/wai.h regenerates unchanged pass
registry wai registry export --check up to date
wai-live-web --all-targets; --features int_video --lib 7; 8 passed
independent verifiers every live-conformance/*_verify.py (26 of them), co_watch_verify.py among them all PASS. Age: 114 cases, 47 mutated copies, 87 of 93 dropped checks caught; the 6 it cannot catch were already noted
lock files cargo metadata --locked --offline in wai-rs, wai-live-web, joule-code-rs, joule-moq-room all hold, no lock changed
repo checks check-tests-in-ci, check-workflow-concurrency, check-spec-refs, check-wai-section-refs, check-no-vanity-metrics, test-public-text-checks, check-comparative-prose, check-orphan-sources all exit 0
workflow steps wfsteps.py HEAD 9f63561b pr/432 3 workflow files, 0 problem(s)
workflows parse ruby -ryaml -e 'ARGV.each{|f| YAML.load_file(f)}' .github/workflows/*.yml 46 parsed
public names check-public-names.sh over the 56 changed files; commit messages and this body one line: joule-moq-web.yml's BROWSER_BIN path, unchanged from #428 and outside the script's own scope

Falsifications

Each was committed first, then broken on the build host's copy, then restored.

broken result
The exemption removed from threshold() age_room_service exits 101: the session proving the clock's key is refused its SUBSCRIBE. The ageco e2e exits 1: the clock never opens its events track, and the lead's page times out (no publisher within RENDEZVOUS_TIMEOUT)
The exemption widened to every session age_room_service exits 101: the ordinary session is served
wai-rs room_service never the room's tests/age_assurance.rs exits 101: the corpus no longer regenerates
wai-rs room_service by any key tests/age_assurance.rs exits 101
Python verifier: each of service_room_key, service_proof, service_no_grant dropped each caught
Python verifier: four mutated copies of room_service (a byte of the clock's proof; no proof, the performer's proof and the forged proof each stated as the room's service) all four fail
index.html's </section> removed again markup.test.mjs exits 1. The e2e service scenario (unit test taken out of run.sh) exits 1 on the markup check alone, in_co: ["log", "binding-debug"] and log_shows: false, 41 of 42 checks ok

Decisions

  • The clock proves both room keys. A relay always holds the authority for a room whose grants it honours, and holds the clock only with [floor].
  • A proof that passes by a key that is no room key is passed over, not refused. A service proving both keys then works whichever key a relay holds. Any proof that fails its checks still refuses the session.
  • The relay recognises a proof of a room key by its zero grant. In the relay, pop_wrong_grant never applies to such a proof. The corpus keeps that reason for a session that presents a grant-naming proof as its room-key proof.
  • A watch room is refused together with [age], [co] or e2ee. That is at configuration time, not left to undefined behaviour.

Not done

  • Not deployed. The deploy needs are in traind-deploy.md: the page files and the sync-room.mjs mapping (co.js, binding.js, privacy-pass.js, cowatch.js), the room.astro sections for co-performance, binding and co-watch, three wasm packages, the config keys, and the binaries.
  • Age × watch. The source served at /watch/source would have to be gated by a grant before a watch room can admit by age. The service refuses that configuration until then.
  • The site's room.astro needs the co-watch markup. The site worktree that adds co-performance and binding does not have it.

Main merged again (head c6c3bce4): main's 76504e21 (#443, the advisory backlog across every workspace, and the cargo audit gate) merged cleanly.

  • scripts/check-lockfiles.sh: 47 lockfiles, all current.
  • cargo metadata --locked --offline holds in joule-code-rs, joule-moq-room, wai-rs and wai-live-web.
  • joule-moq-relay --all-targets, joule-moq-room --all-targets and wai-live-web --lib pass.
  • joule-moq-web builds for wasm32.
  • The room and relay binaries built from this branch carry #443's fixed rustls-webpki 0.103.16 and quinn-proto 0.11.18.

🤖 Generated with Claude Code

Live-room train D: co-watch (#432) merged onto #428's live room, the age × co rule, and the room page's unclosed section. Base: #428's head (`793bd478`, then its fix `9f63561b`, merged in). | part | what | |---|---| | #432 | `wai.live.cowatch`: watch rooms, merged by meaning | | age × co | age-assurance §5.5: the room's own service is not held to the serving rule | | room page | `<section id="co">` closed; a markup test and an e2e check | ## How #432 was merged #432 conflicted in the room service, the page, the e2e suite, both room workflows, and the registry lists. **One config reader.** - `Config::settings()` now returns a `Result`, because it reads the watch source's file. It carries the following: - #428's `age`, `audience_input`, `latency_budget_ms`, `lead` and `ai`; - the watch room's `watch` (from `watch_source` and `watch_duration_ms`), `watch_control` and `watch_window_ms`. - There is one `public_prefix`. Both sides had added it, so the auto-merge had left two fields in `Config` and two keys in `room.example.toml`. The example config test caught the duplicate key. - The service refuses a watch room that is also `e2ee`, `[age]` or `[co]`: - with `e2ee`, the source is served in the clear; - with `[age]`, the source is served without a grant, so no age check could apply to it; - co-performance and co-watch are not specified together. **One path for the clock's records.** - #428's co-performance has `Item::{Life, Meta}` and `emit_items`, which keeps the clock's rate. - #432 had added a second path, `Lifecycle::Meta`. It is dropped: - skew statements go out through `emit_meta`; - leaves go out through `emit_after`, which carries a least `t`; - every record of the clock's chain reaches the watch log through `watch_record`. - `Service::start` spawns `run_watch` for a watch room. **Roles and grants.** - A participant gets `meta` in a room with co-performance or in a watch room. - A watch room's viewer gets `WATCHER_KINDS` (`meta`, chat). An audience member keeps `AUDIENCE_KINDS`. - `watch_control` is appended last among the grant attributes, which keeps their key order. **Page and e2e.** - `app.js` has the co-watch mode beside co-performance and the binding. - The e2e suite keeps every scenario and adds `cowatch`. - `run.sh` runs all the page's node tests. **Workflows and lists.** - Workflow steps are kept whole, and `paths:` lists are unioned. - CHANGELOG and REGISTRY rows come from both sides. - `capabilities.json` and `v2.ttl` were regenerated by `registry export`. ## The age × co rule (age-assurance §5.5) **The problem.** In a room with both `[age]` and `[co]`, the clock follows and logs every performer's objects over a relay session of its own. That session holds no grant and no token, so the relay served it nothing rated. Its forwarding SUBSCRIBE to the rated video was refused, so it never recorded the lead's `join` and never opened its events track. Every page then waited for the clock, and the room never started. The falsified e2e below shows exactly this. **The rule.** A session that proves possession of a room key is the room's own service, and the relay serves it every object of the room. - **The proof.** It is a `GRANT_PROOF` (live-rooms §10.5.2) whose `grant` is 32 bytes of zero, made over the session's challenge. - **The check.** The relay checks it as live-rooms §10.5.4 checks a proof, with the zero digest as the grant's and the proof's key as the principal. - A proof that does not pass refuses the session with that section's reason (`UNAUTHORIZED`). - A proof that passes by a key that is no room key gives the session nothing. - **What is not enough.** No name, token or flag that a client sets makes a session the room's. **Spec.** - `age-assurance.md` §5.5 states the rule; §11 names the reasons it uses and §12 the new vectors. - `co-performance.md` §8 points to it from the paragraph on the clock's own session. **Relay (`joule-moq-relay`).** - In SETUP, the relay checks every proof of a room key (`grant_proof::room_key_proofs`). - `SessionCtx::service` holds the rooms each proved key covers: - an authority of `[[auth.authorities]]` covers its own rooms; - a key in `[floor] room_keys` or `[dvr] room_keys` covers every room. - `threshold()` is past every age for the room's own service. - `ClientOptions::room_keys` presents such proofs. **Room service (`joule-moq-room`).** The clock's session proves both room keys it holds, so a relay that holds either of them recognises it. `relay-lines` and the README say so. **`wai-rs`, the corpus and the Python verifier.** - `age_assurance::room_service` implements the rule. - The serving case `room_service` has 13 requests: - the clock's proof and the authority's, served at threshold 0; - no proof, and the performer's proof, both served by their thresholds; - proofs refused with the reason in brackets, at any threshold: a forged signature (`pop_bad_signature`), another session's challenge (`pop_wrong_challenge`), another relay (`pop_wrong_context`), a named grant (`pop_wrong_grant`) and a cut proof (`pop_malformed`). - `age_assurance_verify.py` learned the rule, with three new checks and four new mutations. `joule-moq`'s `tests/rating_vectors.rs`, the corpus's third reader, checks those requests as the relay does. **Tests.** - `tests/age_room_service.rs` is the relay test with age and co both in play: a room rated 18 and a floor held by the clock's key. - Sessions proving the clock's key and the authority's are served every group, and those groups verify. - The clock's token with no proof, and a proof by a key that is no room key, are refused `below_threshold`. - A proof that names the clock's key under another key's signature closes the session with `UNAUTHORIZED`. So does the clock's proof over another session's challenge. - The e2e scenario `ageco` is a room with both `[age]` and `[co]`: - both performers enter at 18; - the relay logs the clock's session as the room's own service; - the clock logs both tracks of both performers, the lead's video rated 18 among them, and marks an object played out of turn; - a viewer at 16 is still refused that video. ## The room page's unclosed section **The cause.** Train B's merge of #424 with #413 (`173981739`) left `<section id="co">` without its `</section>`. The page opened five sections and closed four, so the parser nested `#binding-debug` and the Log inside the hidden `#co`. In every room without co-performance, neither ever showed. **The fix (`984d95b2`).** - `index.html` has the missing `</section>`. - `web/room/markup.test.mjs`, run by `run.sh`, checks three things: - every element is closed, in order; - `#log`, `#binding-debug` and `#cowatch` are outside `#co`; - each mode's controls are inside its own section. - The e2e checks the same thing in the browser: - in `service`, a room without `[co]`, the Log shows; - in `cowatch`, the co-watch section shows outside `#co`. ## The room page takes only its own origin's endpoints (security fix, `cb766e71`) **The problem.** The room client took its endpoints from its query string as given: the relay (`ws`, `wt`), the room service (`service`), the test room (`fixture`), certificate hashes (`hashes`) and the issuer directory (`issuer_directory`). It also followed a relay's GOAWAY to any URI. A crafted link and a press of Join would send a joiner's camera and microphone to another origin. **The rule** (`web/room/endpoint.js`). - An endpoint is taken only on the page's own origin: `https://<host>`, or `wss://<host>` for a relay. - A URL carrying user information is never taken. - A page served from the loopback interface (a fixture page, or the e2e harness) takes loopback addresses alone. - `hashes` are taken on a loopback page alone. **The page** (`app.js`). - A refused endpoint is recorded in `snapshot().refused` and shown to the visitor. - Join stays off, and the page connects to nothing. - `enter()` checks the relay before it asks for the camera, a grant or a session. - A GOAWAY naming another origin is refused: the page closes its session and says so. **Tests.** - `web/room/endpoint.test.mjs` (run by `run.sh`) covers: - same origin; - another host or port; - scheme-relative `//evil`; - the page's host as user information; - `javascript:` and `data:`; - a plain scheme on a secure page; - an HTTP page; - loopback for the harness. - The e2e scenario `origin` covers three cases: - a foreign `?ws=` is refused at Join, and nothing connects: no WebSocket, no request after the press, no grant, no camera; - a foreign `?service=` (scheme-relative, behind user information, or `javascript:`) is refused, Join stays off, and no request leaves the origin; - a GOAWAY to another origin is refused, and the session ends with a status. - The `service` scenario now checks that every request and WebSocket of every page stays on the page's origin. That is the production `connect-src 'self' wss://<host> blob:`. **Falsified.** The check was dropped, so every URL that parses was taken: - `endpoint.test.mjs` exits 1 (5 of 6 fail); - the e2e `origin` scenario exits 1 (the page takes the foreign relay). **One production note.** A room with `[age]` fetches its Privacy Pass token from the issuer's origin by design. `connect-src` must name each issuer before `[age]` runs in production. ## Main merged, and a stale lock file `origin/main` (`41c5bafb`, #428 landed as `aa61ab3b`) merged cleanly. Main's wai-quantum 0.6.14 release left `joule-moq-room/Cargo.lock` at 0.6.12. With it, the room service's `--locked` build refused to run. `ea7efb97` refreshes it with `cargo metadata`; the one version line changes. After that: - `cargo metadata --locked --offline` holds in joule-moq-room, joule-code-rs, wai-live-web and wai-rs; - `scripts/check-lockfiles.sh` reports 47 lockfiles, all current; - the room service builds `--locked` in `run.sh`, and its 17 tests pass. ## Checks (build host, through rb) **At the final head `ea7efb97`:** | check | result | |---|---| | browser suite, `run.sh --runs 1`, with `origin` | 1 of 1 runs, 269 of 269 checks; 46 node tests pass | | joule-moq-room `cargo test --locked --all-targets` | 17 passed | | `scripts/check-lockfiles.sh` | 47 lockfiles, all current | | `wfsteps.py HEAD origin/main pr/432` | 3 workflow files, 0 problem(s) | | `ruby -ryaml` over `.github/workflows/*.yml` | 46 parsed | | the 8 repo checks | all exit 0 | | public names (changed files, commit messages) | only the existing `BROWSER_BIN` line | **At `5aacacfa`, before the fix and the main merge:** The full cargo and verifier run was at `298eaa70`. The commits after it touch only `joule-moq/tests/rating_vectors.rs` and `wai-conformance.yml` (from `9f63561b`). Three things were run again after them: - at `9a7f953f`: joule-moq's tests and the e2e, twice; - at the final head `5aacacfa`, which changes only one assertion's form in that test: joule-moq's clippy and `rating_vectors`; - the workflow checks. | check | command | result | |---|---|---| | browser e2e, twice | `e2e/run.sh --runs 2`: compose, webtransport, websocket, goaway, dvr, service, e2ee, age, co, ageco, binding, cowatch | 2 of 2 runs, 506 of 506 checks | | page modules | `node --test` over capsule, media, presentation, privacy-pass, binding, cowatch, markup | pass | | joule-moq | `cargo test -p joule-moq --features quic --all-targets`; `--features secure-objects` | 141; 148 passed | | joule-moq-relay | `cargo test -p joule-moq-relay --all-targets` | 90 passed | | relay timing tests ×5 | `--test` dvr, floor, age_rising, age_serving, age_room_service, publish_done_streams | 30 of 30 runs pass | | joule-moq-room | `cargo test --all-targets` | 17 passed | | joule-moq-web, ingest, interop | `--lib`; `--features e2ee --lib`; ingest `--all-targets`; interop `--all-targets` | 9; 11; 33; 0 (no tests) | | joule-moq-web wasm32 | `cargo build -p joule-moq-web --target wasm32-unknown-unknown` | builds | | clippy | joule-moq, joule-moq-relay, joule-moq-web, joule-moq-room `--all-targets`; wai-rs `age_assurance,co_watch --all-targets` | no new notes | | MOQT decoders | `moqt22_check.py`, `moqt18_check.py`, `moqt16_check.py` | pass | | wai-rs, new or touched | `age_assurance` lib, test; `co_watch` lib, test | 9, 3; 7, 3 passed | | corpora regenerate | `wai_age_assurance_vectors verify`; `wai_co_watch_vectors verify`; `wai_browser_sink_vectors verify` | byte for byte | | wai-rs, the other slices | co_performance, takedown, browser_sink, live_room, live_record, live_events (lib and test); `--lib codecs::` (both feature sets); corpus_provenance, cli_registry; int_kernels digests | all pass | | `include/wai.h` | regenerates unchanged | pass | | registry | `wai registry export --check` | up to date | | wai-live-web | `--all-targets`; `--features int_video --lib` | 7; 8 passed | | independent verifiers | every `live-conformance/*_verify.py` (26 of them), `co_watch_verify.py` among them | all PASS. Age: 114 cases, 47 mutated copies, 87 of 93 dropped checks caught; the 6 it cannot catch were already noted | | lock files | `cargo metadata --locked --offline` in wai-rs, wai-live-web, joule-code-rs, joule-moq-room | all hold, no lock changed | | repo checks | check-tests-in-ci, check-workflow-concurrency, check-spec-refs, check-wai-section-refs, check-no-vanity-metrics, test-public-text-checks, check-comparative-prose, check-orphan-sources | all exit 0 | | workflow steps | `wfsteps.py HEAD 9f63561b pr/432` | 3 workflow files, 0 problem(s) | | workflows parse | `ruby -ryaml -e 'ARGV.each{\|f\| YAML.load_file(f)}' .github/workflows/*.yml` | 46 parsed | | public names | `check-public-names.sh` over the 56 changed files; commit messages and this body | one line: `joule-moq-web.yml`'s `BROWSER_BIN` path, unchanged from #428 and outside the script's own scope | ## Falsifications Each was committed first, then broken on the build host's copy, then restored. | broken | result | |---|---| | The exemption removed from `threshold()` | `age_room_service` exits 101: the session proving the clock's key is refused its SUBSCRIBE. The `ageco` e2e exits 1: the clock never opens its events track, and the lead's page times out (`no publisher within RENDEZVOUS_TIMEOUT`) | | The exemption widened to every session | `age_room_service` exits 101: the ordinary session is served | | `wai-rs` `room_service` never the room's | `tests/age_assurance.rs` exits 101: the corpus no longer regenerates | | `wai-rs` `room_service` by any key | `tests/age_assurance.rs` exits 101 | | Python verifier: each of `service_room_key`, `service_proof`, `service_no_grant` dropped | each caught | | Python verifier: four mutated copies of `room_service` (a byte of the clock's proof; no proof, the performer's proof and the forged proof each stated as the room's service) | all four fail | | `index.html`'s `</section>` removed again | `markup.test.mjs` exits 1. The e2e `service` scenario (unit test taken out of `run.sh`) exits 1 on the markup check alone, `in_co: ["log", "binding-debug"]` and `log_shows: false`, 41 of 42 checks ok | ## Decisions - **The clock proves both room keys.** A relay always holds the authority for a room whose grants it honours, and holds the clock only with `[floor]`. - **A proof that passes by a key that is no room key is passed over, not refused.** A service proving both keys then works whichever key a relay holds. Any proof that fails its checks still refuses the session. - **The relay recognises a proof of a room key by its zero `grant`.** In the relay, `pop_wrong_grant` never applies to such a proof. The corpus keeps that reason for a session that presents a grant-naming proof as its room-key proof. - **A watch room is refused together with `[age]`, `[co]` or `e2ee`.** That is at configuration time, not left to undefined behaviour. ## Not done - **Not deployed.** The deploy needs are in `traind-deploy.md`: the page files and the `sync-room.mjs` mapping (`co.js`, `binding.js`, `privacy-pass.js`, `cowatch.js`), the `room.astro` sections for co-performance, binding and co-watch, three wasm packages, the config keys, and the binaries. - **Age × watch.** The source served at `/watch/source` would have to be gated by a grant before a watch room can admit by age. The service refuses that configuration until then. - **The site's `room.astro`** needs the co-watch markup. The site worktree that adds co-performance and binding does not have it. **Main merged again (head `c6c3bce4`):** main's `76504e21` (#443, the advisory backlog across every workspace, and the `cargo audit` gate) merged cleanly. - `scripts/check-lockfiles.sh`: 47 lockfiles, all current. - `cargo metadata --locked --offline` holds in joule-code-rs, joule-moq-room, wai-rs and wai-live-web. - joule-moq-relay `--all-targets`, joule-moq-room `--all-targets` and wai-live-web `--lib` pass. - joule-moq-web builds for wasm32. - The room and relay binaries built from this branch carry #443's fixed rustls-webpki 0.103.16 and quinn-proto 0.11.18. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The extension text (wai/extensions/co-watch.md), the co_watch module of wai-rs behind its own
feature, and the corpus live-conformance/cowatch/ written by wai_co_watch_vectors from fixed
seeds: three accepted rooms, a gap, and refused vectors covering every reason of §12.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
It verifies each room with live_rooms_verify.py, recomputes the controlled position, samples,
percentiles and verdicts from §5 to §9, requires every mutated copy of the corpus to fail and
catches each of its own checks dropped. Standard library only, run isolated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The room service serves a watch room's source, admits a viewer by a proof of its grant's key
over the service's challenge, carries its controls and presentation statements in a log when
co-watch §5 and §7.2 accept them, signs each window's skew statements and a viewer's leave
when it falls silent. web/room/cowatch.js holds the rules the page applies (the policy, the
controlled position, the catch-up rule, statements written byte for byte) and the player;
its tests run them against live-conformance/cowatch/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The page fetches a watch room's source and checks it by its content hash, enters the room's
co-watch log by a proof of its key, applies each control at its instant, keeps the source at
the controlled position by the catch-up rule, states what it presents and shows the room's
sync. e2e scenario cowatch: three viewers, a play, a pause, a seek, a late joiner, the clock's
skew statements in sync within the bound, and the room verified by co_watch_verify.py. The room
service's test drives a watch room against a relay on loopback.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
co-watch: reflow two paragraphs of §8
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
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, 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
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
15561bbdda
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-watch merged by meaning onto the integrated live room (#428):

- joule-moq-room: one config reader, `Config::settings()`, now fallible, carries
  `age`, `audience_input`, `latency_budget_ms`, `lead`, `ai` and the watch room's
  `watch` (from `watch_source`/`watch_duration_ms`), `watch_control` and
  `watch_window_ms`; one `public_prefix`. A watch room is refused together with
  `e2ee`, `[age]` or `[co]`.
- One path for the clock's records: co-performance's `Item` (`Life`/`Meta`) and
  `emit_items`, with the clock's rate kept; co-watch's skew statements go out as
  `emit_meta`, its leaves through `emit_after` (a least `t`), and every record of
  the clock's chain reaches the watch log through `watch_record`. The separate
  `Lifecycle::Meta` is dropped.
- Roles and grants: `meta` for participants in a room with co-performance or a
  watch room, `WATCHER_KINDS` for a watch room's viewers, `watch_control` the last
  attribute in key order.
- Page: the co-watch mode beside co-performance and the binding; the e2e suite
  gains `cowatch` and keeps every scenario; run.sh runs all five node test files.
- Workflows: each step whole, `paths:` unioned. CHANGELOG and REGISTRY rows
  kept from both; capabilities.json and v2.ttl regenerated by `registry export`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Train B's merge of #424 (co-performance in the room) with #413 (the browser
sink, 173981739) left `<section id="co">` without its `</section>`: the page
opened five sections and closed four. The parser then nested the binding table
(`#binding-debug`) and the Log inside the hidden `#co`, so in a room without
co-performance neither ever showed.

- index.html: the `</section>` after the `#co-lead` controls.
- web/room/markup.test.mjs (run by e2e/run.sh): every element the page opens is
  closed in order; the Log, the binding table and the co-watch section are not
  inside `#co`; each mode's controls are inside its own section.
- e2e: in the service scenario (a room without `[co]`), `#log`, `#binding-debug`
  and `#cowatch` are not descendants of `#co`, `#co` is hidden and the Log shows;
  in the cowatch scenario, the co-watch section and the Log show outside `#co`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A room with both `[age]` and `[co]` broke: co-performance's clock follows and
logs every performer's objects over a relay session of its own, which holds no
grant and no token, so age-assurance §5.5 served it nothing rated.

- age-assurance §5.5: a session that proves possession of a room key (the
  authority's or the clock's) is the room's own service, served every object of
  the room. The proof is a GRANT_PROOF naming no grant (32 bytes of zero) over
  the session's challenge, checked as live-rooms §10.5.4 checks a proof; one
  that does not pass refuses the session with that section's reason; one that
  passes by a key that is no room key gives nothing. Co-performance §8 points
  to it; §11 and §12 name the reasons and the new vectors.
- joule-moq: `grant_proof::NO_GRANT`, `add_room_key_proof`, `room_key_proofs`.
- joule-moq-relay: SETUP checks room-key proofs; a session proving a key the
  relay holds as a room key (an authority's, for its rooms; `[floor]` or `[dvr]`
  room keys, for every room) has `SessionCtx::service`, and its threshold there
  is past every age. `ClientOptions::room_keys` presents such proofs.
  tests/age_room_service.rs: a room rated 18 with the floor held; the clock's
  and the authority's proofs served every group, the clock's token alone and a
  non-room key refused `below_threshold`, a forged clock proof and one over
  another challenge closed UNAUTHORIZED.
- joule-moq-room: the clock's session proves both room keys it holds.
- wai-rs `age_assurance::room_service`, the corpus case `room_service`, and
  `age_assurance_verify.py` taught the rule, with three new checks and four
  mutations, all caught.
- e2e: scenario `ageco`, a room with both `[age]` and `[co]`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
tests/rating_vectors.rs reads every request of the age corpus's serving cases.
The new `room_service` case gives some of them a session's proof of a room key
(age-assurance §5.5): checked here as the relay checks it in SETUP, with the
room's authority and clock as the room keys, and held to the corpus's outcome
(the room's own service, served by threshold, or refused with the proof's
reason).

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 room client took its relay, room service, fixture, WebTransport URL,
certificate hashes and issuer directory from its query string as given, and
followed a relay's GOAWAY to any URI. A crafted link and a press of Join would
send a joiner's camera and microphone to another origin.

- web/room/endpoint.js: an endpoint is taken only on the page's own origin
  (`https://<host>`, or `wss://<host>` for a relay), never with user
  information; a page served from the loopback interface (a fixture page, the
  e2e harness) takes loopback addresses alone.
- app.js: `service`, `ws`, `wt`, `fixture` and `issuer_directory` are held to
  it; `hashes` only on a loopback page. A refused endpoint is recorded
  (`snapshot().refused`) and shown, Join stays off, and the page connects to
  nothing: `enter()` checks the relay before it asks for the camera, a grant or
  a session. A GOAWAY naming another origin is refused: the page closes its
  session and says so.
- web/room/endpoint.test.mjs (run by e2e/run.sh): own origin, another origin,
  scheme-relative, the page's host as user information, javascript:, a plain
  scheme on a secure page, an HTTP page, and loopback for the harness.
- e2e scenario `origin`: a foreign `?ws=` is refused at Join and nothing
  connects (no WebSocket, request, grant or camera); a foreign `?service=`,
  scheme-relative, behind user information or as javascript:, is refused with
  Join off; a GOAWAY to another origin is refused and the session ends. The
  `service` scenario checks that every request and WebSocket of every page
  stays on the page's origin, as the production `connect-src` requires.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's wai-quantum 0.6.14 release updated every lock that carries it but this
one (wai-rs's optional dependency reaches it), so `cargo build --locked` of the
room service refused to run after the merge. Resolved with `cargo metadata`:
the one version line changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merge remote-tracking branch 'origin/main' into train/live-room-d
Some checks failed
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m24s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 23m59s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 8s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 37s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 9s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 7s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m44s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m58s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 4m39s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m34s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m24s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 30s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 10m18s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 2m41s
wai-pack / wai-pack · linux (pull_request) Successful in 1m36s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 51m44s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 2m47s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 20m32s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m52s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 4m36s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m51s
standards workspaces build + test / macOS energy backends (pull_request) Failing after 4m55s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 12m17s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 17s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 40s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 3m55s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 3m50s
wai-pack / wai-pack · macos (pull_request) Successful in 58s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 1m58s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 10m4s
c6c3bce468
relay tests: wait on what the relay holds, not on fixed sleeps and windows
All checks were successful
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 28s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 11m8s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 19m52s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 2m18s
wai-pack / wai-pack · linux (pull_request) Successful in 1m44s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 2m55s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 6m11s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m38s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m48s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 15s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 12s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 26s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m30s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 7m19s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 10m55s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m12s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 15m12s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 18s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m10s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 3m51s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 8m51s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 13m38s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 21s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 49s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 4m51s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 41m53s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 5m9s
wai-pack / wai-pack · macos (pull_request) Successful in 1m12s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 2m23s
626719c5fb
floor.rs `the_relay_refuses_invalid_audience_inputs` failed under load (2 of 10
runs at load ~40-70), and not for want of readiness: the window was held and
the subscriptions live. The relay forwarded the member's *second* vote and
refused the first `input_over_limit`. Each group rides a stream of its own,
which the relay reads in a task of its own, so two votes sent back to back
can be taken in either order, and the limit counts them in the order taken.

- floor.rs (inputs): the fixed 300 ms sleep becomes `Floor::holds_window`
  (new, minimal), and the second vote is sent once the watcher has the first,
  which proves its subscription live. The refusals are waited for by
  `refused_inputs()`, then a second of quiet catches anything forwarded wrongly.
- floor.rs (the floor): the 3 s collect windows wait for the object in turn and
  for `refused()`, then a second of quiet.
- age_serving, age_rising, age_room_service: each collect waits for every group
  it expects, since the groups arrive on their own streams in any order,
  rather than for the last group alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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!451
No description provided.