joule-moq-room and the public room (L5c-2): grants with proof of possession, the lifecycle by the room's clock, room authorities at the relay, and two relay fixes #334

Merged
dcharlot merged 10 commits from joule-moq/room-live into main 2026-10-19 11:05:07 -04:00
Owner

L5c-2: the public room. Builds on #325 (joule-moq/ingest, with #327 and #324), merged into main at 58093c5d; this branch has origin/main (046575ea) merged in.

What it adds

joule-moq-room (new; joule-code/joule-code-rs/crates/joule-moq-room, a workspace of its own with its own lock file, because it builds on wai-rs, whose serde_json features would otherwise reach every crate of the joule-code workspace). A room service that holds a room's authority and clock keys and:

  • issues grants: POST /grant {principal, role} signs a short-lived WRG1 grant (a publisher's, or a viewer's) for a key the page made. Grants are bounded per client address, overall, and per room. The grant alone is useless: the relay honours it only beside the key's proof of possession (live-rooms §10.5);
  • authors the lifecycle: the service's clock is a session on the relay. It watches each publisher's video track (a SUBSCRIBE with FORWARD 0 and a rendezvous), and signs join and publish when the track appears and leave when the relay ends it. Records go on the clock's events track in live-events §8.1's MSF form;
  • serves the room: GET /room gives the signed descriptor, the composition descriptor and the publishers' grants;
  • starts a new room (new t0, same keys) once the last publisher has left.

Its keys file must have mode 0600, and the service refuses it otherwise. Keys has no Debug.

joule-moq-relay:

  • [[auth.authorities]]: honours every grant an authority's key signs, beside its proof and within its window, mapped as live-rooms §10.2 maps it. joule-moq's new GrantHead reads the grant under the authority's key.
  • A * field in a rule's or a door rule's prefix matches any field.
  • A door rule's trust_field holds each [room, principal, kind] namespace to the key it names.
  • New limits: limits.max_session_ms, max_ingress_kbps (with ingress_burst_ms) and max_resident_mb (the binary exits with 70).
  • A rule's unlimited exempts a session from those limits; the room's clock uses it.
  • A session ends with EXPIRED_AUTH_TOKEN when its authority grant expires.
  • SIGTERM stops the relay as Ctrl-C does.

Two relay fixes, found on the deployed room:

  • A mux self-deadlock (pre-existing, from #324). In the WebSocket binding's multiplexer, a stream that arrived after its session closed found nobody accepting. Its receiving half was dropped after the state lock was taken again, and that half's Drop takes the same lock. The worker deadlocked, and every other worker after it. Any session the relay closes while its peer still opens streams (the session and ingress limits, a refused proof) could hang the whole relay. Now the half is dropped before the lock is retaken. The regression test hangs without the fix and passes with it (checked on the build host).
  • A FETCH waits for groups still arriving. When a FETCH is not covered only because groups are still in flight, the relay waits for them, at most the fill budget or one second, instead of passing it to a publisher that takes no requests. A page's audio is one group per 20 ms, each on its own stream, so a late joiner's FETCH from group 0 through the front met one in flight. That failed 3 of 4 production runs before the fix. Covered by tests/fetch_in_flight.rs.

joule-moq-web:

  • The room page's service mode: its own key, a grant from the service, the WebSocket binding only, Join / Watch / Leave, and a typed human caption.
  • wai-live-web's Room.setGrants.
  • grantProofBy / clientSetupWithProof, so a test can present a proof by another key.
  • A late joiner's refused FETCH is retried.
  • The service end-to-end scenario. With --base <origin> (and --between to pace runs) it runs against a deployment.

CI: a new joule-moq-room workflow. joule-moq-web's browser workflow now also runs on room-service changes.

Tests (build host, aarch64-apple-darwin, at dfb24a1a)

  • cargo test --locked -p joule-moq-relay --all-targets passes, including:
    • tests/room_authority.rs: authority grants with and without proof, a forged authority, the door's trust field, the session limit, an unlimited session, grant expiry, and the ingress limit;
    • tests/fetch_in_flight.rs;
    • the mux regression test.
      The two integration tests passed 5 runs out of 5.
  • joule-moq --lib (84), joule-moq-web (8), and joule-moq-room (cargo test --locked: 4 unit tests + tests/room_service.rs) all pass.
  • e2e/run.sh (compose, webtransport, websocket, goaway, service) in the host's Chrome: 107 of 107 checks pass.

Deployment

Live at https://wai.transaction.science/room/. Two LaunchAgents run on prod, built from dfb24a1a:

  • science.transaction.wai-room-relay on 127.0.0.1:9681, WebSocket only, no UDP;
  • science.transaction.wai-room on 127.0.0.1:9682.

They sit behind Caddy's @wai block (handle /moq, handle_path /room-api/*) and the existing tunnel; the configuration and plists are in site-ops (deploy/wai-room/).

Production e2e: from the build host, in Chrome with fake media, against the live page and relay. 4 of 4 runs, 120 of 120 checks, after 3 of 3 runs (90 checks) in the batch before. Each run checks:

  • two participants publish and see each other;
  • every received object verifies against its receipt;
  • the page's layout equals the host's;
  • a machine caption and a typed human caption show their labels;
  • the clock authors the lifecycle;
  • a late joiner fetches groups from 0, each verified;
  • a viewer watches;
  • a grant without a proof (pop_missing), or with another key's proof (pop_wrong_key), is refused the session;
  • a leave is recorded.

🤖 Generated with Claude Code

L5c-2: the public room. Builds on #325 (joule-moq/ingest, with #327 and #324), merged into main at 58093c5d; this branch has origin/main (046575ea) merged in. ## What it adds **`joule-moq-room`** (new; `joule-code/joule-code-rs/crates/joule-moq-room`, a workspace of its own with its own lock file, because it builds on wai-rs, whose serde_json features would otherwise reach every crate of the joule-code workspace). A room service that holds a room's authority and clock keys and: - **issues grants**: `POST /grant {principal, role}` signs a short-lived `WRG1` grant (a publisher's, or a viewer's) for a key the page made. Grants are bounded per client address, overall, and per room. The grant alone is useless: the relay honours it only beside the key's proof of possession (live-rooms §10.5); - **authors the lifecycle**: the service's clock is a session on the relay. It watches each publisher's video track (a SUBSCRIBE with FORWARD 0 and a rendezvous), and signs `join` and `publish` when the track appears and `leave` when the relay ends it. Records go on the clock's events track in live-events §8.1's MSF form; - **serves the room**: `GET /room` gives the signed descriptor, the composition descriptor and the publishers' grants; - **starts a new room** (new `t0`, same keys) once the last publisher has left. Its keys file must have mode 0600, and the service refuses it otherwise. `Keys` has no `Debug`. **`joule-moq-relay`**: - `[[auth.authorities]]`: honours every grant an authority's key signs, beside its proof and within its window, mapped as live-rooms §10.2 maps it. `joule-moq`'s new `GrantHead` reads the grant under the authority's key. - A `*` field in a rule's or a door rule's prefix matches any field. - A door rule's `trust_field` holds each `[room, principal, kind]` namespace to the key it names. - New limits: `limits.max_session_ms`, `max_ingress_kbps` (with `ingress_burst_ms`) and `max_resident_mb` (the binary exits with 70). - A rule's `unlimited` exempts a session from those limits; the room's clock uses it. - A session ends with `EXPIRED_AUTH_TOKEN` when its authority grant expires. - SIGTERM stops the relay as Ctrl-C does. **Two relay fixes, found on the deployed room:** - **A mux self-deadlock (pre-existing, from #324).** In the WebSocket binding's multiplexer, a stream that arrived after its session closed found nobody accepting. Its receiving half was dropped after the state lock was taken again, and that half's Drop takes the same lock. The worker deadlocked, and every other worker after it. Any session the relay closes while its peer still opens streams (the session and ingress limits, a refused proof) could hang the whole relay. Now the half is dropped before the lock is retaken. The regression test hangs without the fix and passes with it (checked on the build host). - **A FETCH waits for groups still arriving.** When a FETCH is not covered only because groups are still in flight, the relay waits for them, at most the fill budget or one second, instead of passing it to a publisher that takes no requests. A page's audio is one group per 20 ms, each on its own stream, so a late joiner's FETCH from group 0 through the front met one in flight. That failed 3 of 4 production runs before the fix. Covered by `tests/fetch_in_flight.rs`. **`joule-moq-web`**: - The room page's service mode: its own key, a grant from the service, the WebSocket binding only, Join / Watch / Leave, and a typed human caption. - `wai-live-web`'s `Room.setGrants`. - `grantProofBy` / `clientSetupWithProof`, so a test can present a proof by another key. - A late joiner's refused FETCH is retried. - The `service` end-to-end scenario. With `--base <origin>` (and `--between` to pace runs) it runs against a deployment. CI: a new `joule-moq-room` workflow. `joule-moq-web`'s browser workflow now also runs on room-service changes. ## Tests (build host, aarch64-apple-darwin, at dfb24a1a) - `cargo test --locked -p joule-moq-relay --all-targets` passes, including: - `tests/room_authority.rs`: authority grants with and without proof, a forged authority, the door's trust field, the session limit, an unlimited session, grant expiry, and the ingress limit; - `tests/fetch_in_flight.rs`; - the mux regression test. The two integration tests passed 5 runs out of 5. - `joule-moq --lib` (84), `joule-moq-web` (8), and `joule-moq-room` (`cargo test --locked`: 4 unit tests + `tests/room_service.rs`) all pass. - `e2e/run.sh` (compose, webtransport, websocket, goaway, service) in the host's Chrome: **107 of 107 checks** pass. ## Deployment Live at https://wai.transaction.science/room/. Two LaunchAgents run on prod, built from dfb24a1a: - `science.transaction.wai-room-relay` on 127.0.0.1:9681, WebSocket only, no UDP; - `science.transaction.wai-room` on 127.0.0.1:9682. They sit behind Caddy's `@wai` block (`handle /moq`, `handle_path /room-api/*`) and the existing tunnel; the configuration and plists are in site-ops (`deploy/wai-room/`). **Production e2e:** from the build host, in Chrome with fake media, against the live page and relay. **4 of 4 runs, 120 of 120 checks**, after 3 of 3 runs (90 checks) in the batch before. Each run checks: - two participants publish and see each other; - every received object verifies against its receipt; - the page's layout equals the host's; - a machine caption and a typed human caption show their labels; - the clock authors the lifecycle; - a late joiner fetches groups from 0, each verified; - a viewer watches; - a grant without a proof (`pop_missing`), or with another key's proof (`pop_wrong_key`), is refused the session; - a leave is recorded. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- auth.authorities: a WRG1 grant signed by a listed authority is honoured as
  live-rooms §10.2 maps it (its own media, catalog and events namespaces to
  publish, the room to subscribe), only beside a proof of possession and within
  its window; joule-moq's GrantHead reads it under the authority's key.
- A field "*" in a grant rule's or a door rule's prefix matches any field; a
  door rule's trust_field names the namespace field whose key it trusts there.
- limits.max_session_ms, max_ingress_kbps (with ingress_burst_ms) and
  max_resident_mb; a session also ends when its authority grant expires.
- The binary stops on SIGTERM as on Ctrl-C.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
It holds a room's authority and clock keys (a 0600 file outside every
repository). POST /grant signs a short-lived WRG1 grant for a key a page made,
a publisher's or a viewer's, bounded per client and per room; the relay honours
it only beside the key's proof (live-rooms §10.5). The clock's session on the
relay watches each publisher's video track and signs join, publish and leave on
its events track (§7.2, live-events §8.1); GET /room serves the signed
descriptor, the composition descriptor and the grants. A new room starts when
the last publisher has left.

A workspace of its own: it builds on wai-rs, whose serde_json features would
otherwise reach every crate of the joule-code workspace.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The page makes its own Ed25519 key, asks the room service for a grant for it
(a publisher's or a viewer's) from its Join and Watch buttons, presents the
grant with a proof by that key over the WebSocket binding (WebTransport not
tried), and takes the lifecycle from the service's clock, reading the room's
grants again when someone joins under a grant it did not hold
(wai-live-web's Room.setGrants). Leave, a typed human caption, the session's
end, and a proof by another key (tryWrongKey) for the tests.

e2e: the service scenario runs the relay (WebSocket only, trusting the
service's authority, its door holding each publisher to its own key), the
room service and a same-origin proxy on loopback; with --base it runs against
a deployment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A session whose SETUP presents a token whose rule says unlimited is exempt from
max_session_ms and max_ingress_kbps: the room service's clock is a long-lived
session of the deployment's own, and the session limit closed it every ten
minutes (seen in the service scenario). joule-moq-room's relay-lines mark its
clock's rule so.

joule-moq-web: grantProofBy and clientSetupWithProof, so a test presents a page's
grant with a proof by another key (the page's own path signs with the
principal's key alone); the service scenario's teardown is bounded and its
proxy ends upgraded sockets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-room CI; READMEs, the example configuration and the changelog for L5c-2
Some checks failed
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
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-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
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (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
bbbb6b7829
A workflow of its own runs joule-moq-room's tests (its own workspace), on a
change to it, the relay, joule-moq or wai-rs; joule-moq-web's browser suite
runs on a change to the room service too, and gains the service scenario.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-web e2e: the service scenario compares the layout once every tile shows video; killStrays's pattern without its dashes
Some checks failed
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
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-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
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (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
779597de02
Against the deployment, a late joiner's first frame reached the canvas after
its tile was laid out, and the comparison ran in between. pkill on macOS read
the pattern's leading -- as an option, so strays were never ended.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-web e2e: --between, a pause between runs, for a deployment's grant rate per address
Some checks failed
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 5s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 5s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m6s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 3m58s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 1m41s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 12s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 1m54s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m51s
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Has been cancelled
9c78299d5e
Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-relay: a mux self-deadlock fixed; a FETCH waits for groups still arriving
Some checks failed
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
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-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
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (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
dfb24a1a2b
- transport::mux: a stream that arrived after the session closed (its peer not
  yet told) found nobody accepting, and its receiving half was dropped after the
  state was locked again: its Drop takes that lock, so the worker deadlocked, and
  every other worker after it. Any session the relay closes while its peer still
  opens streams (the session and ingress limits, a refused proof) could hang the
  relay. The half is now dropped before the lock is taken again; the regression
  test hangs without the fix and passes with it.
- A FETCH the cache does not yet cover, of a track whose publisher is still
  sending, waits (within its fill budget, at most a second) for the groups still
  arriving before the relay asks upstream: a page's audio is a group per 20 ms,
  each on its own stream, and a late joiner's fetch from group 0 through an HTTP
  front met one in flight and was passed to the publisher, which takes no
  requests (seen on the deployed room).
- room_authority: the ingress limit read from the relay's record when the client
  loses the race to see the close.

joule-moq-web: a late joiner's refused FETCH is tried again (a refusal comes
before any object).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-relay README and the changelog: the fetch's wait for groups still arriving, and the mux fix
All checks were successful
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 1m2s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 1m20s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 47s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m8s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 6s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 5s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m7s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m36s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 59s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 4m30s
aa6e920fdc
Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot changed title from joule-moq-room and the public room (L5c-2): grants with proof of possession, the lifecycle authored by the room's clock, room authorities at the relay to joule-moq-room and the public room (L5c-2): grants with proof of possession, the lifecycle by the room's clock, room authorities at the relay, and two relay fixes 2026-10-19 07:56:44 -04:00
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!334
No description provided.