open-standards/joule-code/joule-code-rs/crates/joule-moq-room
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 8da651a54b
Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 21s
wai-fleet service / does this change touch it (push) Successful in 17s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 26s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 28s
wai-app and Create / wai-app · does this change touch it (push) Successful in 22s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 15s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 50s
wai-quantum editors / does this change touch it (push) Successful in 31s
wai-quantum for JavaScript / does this change touch it (push) Successful in 29s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 40s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m23s
wai-quantum studio and desktop / does this change touch it (push) Successful in 30s
wai-fleet service / service · macos (push) Successful in 59s
wai-quantum for Python / does this change touch it (push) Successful in 31s
wai-quantum docs, course and locales / does this change touch it (push) Successful in 1m27s
wai-quantum views in agent hosts / does this change touch it (push) Successful in 21s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 2m4s
wai-key-proxy / does this change touch it (push) Successful in 1m55s
WAI operation catalogue / does this change touch it (push) Successful in 1m56s
wai-quantum command line / does this change touch it (push) Successful in 1m40s
wai-ledger service / does this change touch it (push) Successful in 1m50s
wai-quantum contract / does this change touch it (push) Successful in 1m41s
WAI Studio / does this change touch it (push) Successful in 12s
wai-sink-swift native sink package / does this change touch it (push) Successful in 25s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m43s
wai-sink-android native sink library / does this change touch it (push) Successful in 42s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 2m45s
wai-quantum editors / editors · macos (push) Successful in 2m2s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m21s
wai-fleet service / service · linux (push) Successful in 3m36s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (push) Successful in 12s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 52s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m37s
wai-quantum for Python / python · macos (push) Successful in 2m57s
wai-ledger service / service · macos (push) Successful in 1m16s
wai-quantum editors / editors · linux (push) Successful in 3m46s
wai-audit collector and player integration / collector · linux (push) Successful in 3m39s
wai-quantum docs, course and locales / the course and the command line's languages · linux (push) Successful in 3m23s
wai-quantum for Python / python · linux (push) Successful in 4m26s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 6m38s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 5m7s
wai-quantum command line / command line · macos (push) Successful in 2m33s
wai-quantum for JavaScript / javascript · linux (push) Successful in 5m35s
wai-key-proxy / proxy · linux (push) Successful in 1m12s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 7m3s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 7m40s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 7m39s
wai-ledger service / service · linux (push) Successful in 2m5s
wai-quantum contract / the policy is linked, and the deprecations page reads (push) Successful in 7s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m36s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 8m36s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m10s
wai-quantum views in agent hosts / views · a scripted host (push) Successful in 4m26s
wai-quantum views in agent hosts / views · the page and its engine (push) Successful in 6m33s
wai-quantum contract / API compatibility · linux (push) Successful in 4m44s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 13m4s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 6m46s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 14m11s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 14m12s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 13m18s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 10m49s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 11m36s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 12m10s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 16m36s
wai-quantum contract / schemas and the committed tool lists · linux (push) Successful in 9m58s
wai-quantum studio and desktop / studio · browser (push) Successful in 16m35s
WAI Studio / Studio in WebKit · linux (push) Successful in 12m22s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (push) Successful in 22m0s
wai release binaries build on Windows / the seven release binaries · windows (push) Successful in 24m31s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 28m51s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 28m59s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 29m41s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 11m58s
wai-quantum command line / command line · linux (push) Successful in 48m17s
wai: one command for the rest of the toolchain: audit, serve, quantum, cmcd, receipt create, and every record kind in wai verify
Folds, each the program it folds run as itself, so what it prints and its exit
status are the program's for the same arguments: wai audit report|check|verify
(audit.create, audit.check, audit.verify) is wai-audit, --owner its
WAI_AUDIT_OWNER; wai serve <role> [--config F] [--check] (service.start) starts
wai-<role>: relay, room, ingest and key-release from a file, audit, fleet,
ledger and quantum-service from their environment; wai quantum
run|plan|lint|verify|methods (quantum.run, .inspect, .check, .verify, .list)
is wai-quantum with that verb, and no other verb of it is reached. A program is
its flag, WAI_<NAME>_BIN, beside wai, or on PATH; one not there is exit 5,
program-not-found. report is not an N3 verb, so wai audit report is the
operation audit.create.

--check on the daemons: wai-relay, wai-room (its keys too) and wai-ingest
--config F --check, and wai-key-release serve F --check, read the
configuration as each starts from it and start nothing, exit 1 with the reason
when it does not read. The proxy is installed as wai-key-release; wai-key-proxy
is the same program under its earlier name.

wai cmcd encode|decode|verify is wai_telemetry's tool compiled in (cmcd.rs, one
module behind both; telemetry joins full). wai receipt create <file>... --from
<input>... [--key] signs the operation receipt a toolchain write leaves, for
files made elsewhere. wai verify reads every record by its kind: by name, and a
JSON record by what it holds (a delivery receipt or claim as receipt_verify
answers it, a room session, an audit bundle as wai-audit verify does with
--owner/--channel, a sealed playlist by its mr/<blake3>.json as check-playlist
does); a file of no kind is exit 2, unknown-record. wai mcp --toolset quantum
starts wai-quantum mcp and answers each quantum_* tool with that server's own
answer and schemas. wai media score build|render alias wai score. The quantum
operations' SDK names are registered labels.

Tests, each fold against what it folds: cli_folds (cmcd over the whole corpus,
receipt create against wrap's receipt, verify on the claim, JWP, room and audit
corpora and a sealed playlist, audit|serve|quantum against stand-ins);
wai-audit tests/fold.rs, wai-quantum-cli tests/wai_fold.rs and joule-moq-room
tests/cli_serve.rs against the real programs; cli_mcp's quantum toolset; the
daemons' --check (config_check.rs, check.rs). ops.json, mcp.json, the registry
export and Studio's actions regenerated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 02:57:54 -05:00
..
deploy wai release: libwai for C, the Swift package by URL and checksum, the Android library published, and the relay's and room service's images 2026-11-09 18:25:22 -05:00
src wai: one command for the rest of the toolchain: audit, serve, quantum, cmcd, receipt create, and every record kind in wai verify 2026-11-10 02:57:54 -05:00
tests wai: one command for the rest of the toolchain: audit, serve, quantum, cmcd, receipt create, and every record kind in wai verify 2026-11-10 02:57:54 -05:00
Cargo.lock wai-relay answers /metrics on its control listener, and wai-quantum-service on its API 2026-11-09 20:06:07 -05:00
Cargo.toml wai: wai dev is the one local stack, driven by control lines, with synthetic participants, room test and load, and --impair 2026-11-09 10:54:12 -05:00
openapi.json Bring wai mcp, wai setup-agent, Create and the room service's API into the catalogue, renamed to N3 2026-10-27 05:54:20 -04:00
README.md wai release: libwai for C, the Swift package by URL and checksum, the Android library published, and the relay's and room service's images 2026-11-09 18:25:22 -05:00
room.example.toml wai/toolchain-fixes, converted to the layout of the fix: no shared append point 2026-11-05 01:45:34 -05:00

joule-moq-room

A room service for WAI live rooms (wai/extensions/live-rooms.md) over joule-moq-relay. Its binary is wai-room (the crate keeps its name). It holds one room's authority and clock keys and:

  • issues grants. A visitor's page makes an Ed25519 key and asks for a grant for it (POST /grant). The service signs a short-lived WRG1 grant: a publisher's (its own audio and video, one of the room's places) or a viewer's (the room to watch). A grant is public, so it is useless without its key: the relay, configured with the authority's key ([[auth.authorities]]), honours it only beside a proof of possession over the relay's own challenge (§10.5), which only the page that holds the key can make. Grants are bounded per client address and overall, and publishers per room. Every role, and so every grant, holds the seek power (live-rooms §3.3, bit 4): a visitor may fetch a track below its live edge (§10.6), from the relay's cache or its DVR window, as a late joiner does for the groups before it joined. A relay refuses such a fetch to a grant without it.

  • authors the lifecycle (§7.2). The service's clock is a session on the relay. It watches each publisher's video track (a SUBSCRIBE that asks for no objects): when the track appears the clock signs join and publish for the publisher on its events track (live-events §8.1), and when the relay ends the track (the publisher's session closed) it signs leave. Pages author none of it. The events track is published with the room's first grant, before anything is signed on it, so a viewer who enters a room nobody has joined subscribes to a track that is there.

  • serves the room. GET /room gives the signed descriptor, the composition descriptor and the publishers' grants, which a page verifies the room with (live-rooms §9).

  • keeps remote desktop rooms ([remote], WAI extensions/remote-desktop.md). Every room is then one host's screen shared into it, its descriptor's roles a host (it publishes the screen kind and holds the direct power), a controller and a viewer, and every grant's policy permits meta, so that each principal sends the extension's statements. The host's grant carries remote_host; a controller's the control power, remote_control = control; a viewer's neither. The host is the key host names or, when it names none, the first key that presents an invitation the room's authority signed (wai-room invite), to which the invitation is then bound. Who may ask for a controller's grant is allow: all, listed (the keys controllers lists) or none. The host hands control over by a grant exchange (POST /remote/control, its grant and a proof over the service's challenge), which takes effect at a tick at least lead_ms ahead, so that every party holds the clock's grant_exchange before the instant it governs. The clock watches the host's screen track (its join and its publish of the screen when the track appears, its leave when it ends) and every other principal's events track, and follows every principal's events: it takes each record in its chain's order, under the policy of its author's grant in effect and after its join, each statement read and held to who may sign it, and the host's screen statements to §3.4 and to the age bound max_age_ms. It computes the host's floor in the merged order by wai-rs's §7.2 rule: a floor statement the rule refuses is kept, stated, and changes nothing, as the host and a verifier have it (GET /room's remote.holder). A principal whose record is out of that order is cut there and removed (leave reason 2), and the host's agent cuts it at the same record: the room the clock holds stays one a verifier accepts. The clock bounds the records it holds of a session, per principal first. At max_records_per_principal (100000 by default) it holds none of a principal's records past the bound, cuts the principal there (max_records_per_principal) and removes it (leave reason 2), and the session goes on; the host's agent reads the bound from GET /room and cuts the principal at the same record, so the two hold the same records. The host has no bound of its own: its outcomes answer the others' inputs. The default: a two-hour session at an average of ten input statements a second is 72000 records, and a principal flooding at the policy's 120 events a second reaches the bound in about 14 minutes, and is removed alone. max_records (1000000 by default) bounds the whole session, the host's records among them, and is the safety net: ten times the bound per principal, above a host and four controllers each at its bound (800000), so the bound per principal bites first for any one principal. When a record would pass the total the clock holds none past it and ends the session, every present principal's leave (reason 2) signed, the host's last, and GET /room's remote.ended states max_records; the session it holds stays whole and verifies, and the next host starts a new room. It ends the session rather than refuse inputs past the total: a record it did not hold would leave a hole in its author's chain, the host would decide inputs the clock never held, and an input's refusal is the host's to state. At the total the clock holds about a gigabyte. Key, text and clipboard actions reach the clock sealed to the host (WAI remote-desktop §5.5): it holds and serves ciphertext, and no opening key. A remote room is not also end-to-end encrypted, a watch room, a room with co-performance, one admitted by age or a world room: the service refuses such a configuration. The relay holds the host's screen to its key with a door like the publishers' (prefix = ["*", "*", "screen"]).

  • keeps end-to-end encrypted rooms ([room] e2ee = true, WAI extensions/confidentiality.md). Each room then has a confidentiality record (WRE1) the authority signs, and one MLS group whose only external sender is the room's clock: the clock proposes the Add of each publisher it records joining and the Remove of each it records leaving, and follows the group from its public messages, holding no key. The service is the room's carrier of MLS messages (§5.1, §15.2): one log per room holding the clock's lifecycle records, its proposals, and every GroupInfo, commit and Welcome a member posts, in the order the clock took them; a post the clock refuses (§5.5) is not carried.

  • admits by age ([age], WAI extensions/age-assurance.md §5.6), as the Privacy Pass origin of RFC 9577. Each room then has an age policy (§4) naming the configured issuers with one key per threshold, and a rating (§3), a room claim, both signed by the authority. A grant request without a token is answered 401 with a PrivateToken challenge for each key (WWW-Authenticate, RFC 9577 §2.1.2): its redemption context HMAC-SHA-256 over the room and a window of token_window_s under a key drawn at start (a restart turns outstanding challenges away), its origin_info the service's server name (origin, else the request's Host). A request with Authorization: PrivateToken token="…" has the token checked by wai-rs's §5.2 against the room's age, and its grant states the token's threshold in age (§5.4), from which the relay holds every track to the serving rule (§5.5). The nonces redeemed are kept for the current window and the one before, at most max_nonces a window (past it, 503 and nothing spent), and dropped as a window ages out. Each admission is counted by its window, issuer and threshold, and nothing else of it is kept: GET /age/receipt gives the room's wai-age-admissions receipt so far (§7), signed by the clock, every count under 50 suppressed, and with receipts_out each window's is appended there as it closes, and the rest of a room's when it ends. The room's rating rides the clock's events track and the grant answer's room, which a publisher's page carries on its tracks (§5.7). The clock's own session proves both room keys in its SETUP (a GRANT_PROOF naming no grant, §5.5), so the relay serves it the room's rated tracks as the room's own service: it follows each publisher and, with [co], logs every performer's objects. Operating an issuer, or the attester that establishes an age, is not the service's.

  • keeps co-performance ([co], WAI extensions/co-performance.md). With audience_input above 0, every viewer's grant carries the audience-input power (the attribute audience_input, the most inputs it may send to one selection window) and may send meta events and reactions, and the viewers' grants are listed with the room (co.audience) so a page verifies their inputs; with latency_budget_ms above 0, every publisher's grant carries that budget. Either gives the room a co-performance policy (WPP1) the authority signs (co.policy), its publishers meta events (their intents), a lead role with the direct power for the first publisher (lead), and, with ai, a grant marked as an AI participant's for a page that asks ("ai": <model digest>). The clock then keeps what only it knows (src/co.rs, co-performance §8): it records each audience member joining when its events track appears, holds the room as a verifier does (every member's chain, by FETCH where it has a gap), logs each publisher's media objects with their arrival and their due instant (GET /co/objects, the objects a verifier is given), and signs a mark (WPM1) for each object out of turn under a schedule that marks or later than its budget, the commitment (WPK1) and selection (WPS1) of each window once it closes, and the periodic fidelity statement (WPF1). A page asks the clock for a turn schedule (WPT1), a handover (WPH1) or a window (WPW1) over HTTP (POST /co/...), with its grant and a proof of possession over a challenge the service issued (live-rooms §10.5.3); its powers decide what it may ask. The relay holds the floor and the audience's inputs (its [floor], which relay-lines prints commented out).

  • keeps watch rooms ([room] watch_source, WAI extensions/co-watch.md). Each room then watches that file together: the authority signs a co-watch policy (WXP1) naming it, an object sequence of one object, by the SHA-256 of its bytes and its duration, and the service serves the bytes. A visitor enters the room's co-watch log by a proof of its grant's key over a challenge the service issued, for the request it makes (live-rooms §10.5.3), and the clock signs its join; every grant carries watch_control, the actions its principal may control. The service is the room's carrier of its viewers' events: one log per room, holding every record of the clock's chain and every control and presentation statement a viewer posts, each carried only when a verifier would accept it (its chain, its signature, its grant's policy and rate, and co-watch §5 and §7.2). Each window, the clock signs the skew statements of co-watch §8.4 from what the log holds; and a viewer silent for four seconds, or whose grant has ended, leaves. One that enters again before the clock has signed that leave has it signed first, then its join, so that no join comes while it is present. A watch room is never end-to-end encrypted (its source is served in the clear), admitted by age (its source is served without a grant) or a room with co-performance: the service refuses such a configuration.

  • keeps a shared world ([world], WAI extensions/live-worlds.md). Each room then has a world statement (WLW1) the authority signs: the world its operations replay in (for wai.world.model, the model header) and the bounds of an op and of an object, served with the room (GET /room's world). A publisher's grant is an actor's: it lets its principal publish its world track (the attribute world = act, §5.1) and no media. The clock watches each actor's world track, [room, actor, world], as it watches a publisher's video: it signs the actor's join when the track appears and its leave when the track ends, and no publish. The world's ops are the actors' own, signed by their keys and receipted on their tracks; the service neither reads nor keeps them, and a page that enters late fetches every track from the relay (§7), which the relay-lines' commented [[dvr.window]] keeps. With e2ee, the world is private: every actor is a member of the room's group, and the world tracks are sealed (§8). A world room is not also a watch room, a room with co-performance, one admitted by age or a remote desktop room: the service refuses such a configuration.

  • keeps a conversation ([messaging], WAI extensions/live-messaging.md). The service's room is then an end-to-end encrypted conversation, kept in a history store on disk (store): conversation.json, its descriptor, confidentiality record and conversation statement (WMC1), written once; and journal, every record the service took, appended and on stable storage before it answers. A key that proves itself (a proof naming no grant, over a challenge the service issued) is admitted (admit), the first as the conversation's owner, and the clock signs its join; an owner removes a member and the clock signs its leave. The clock is the MLS group's external sender, any member commits (live-messaging §7.2), and after each GroupInfo or commit the clock signs an epoch statement (WME1). Members post sealed messages and acknowledgements, which wai-rs's conversation verifier checks before the service takes them (§9.2), and read the log as §9.3 owes it: a removed member nothing past its removal. A restart replays the journal, regenerating the clock's proposals and statements and checking each against the one journalled, so the conversation goes on where it stopped. The service holds no key of the group and no plaintext. A service that keeps a conversation keeps nothing else (no watch room, co-performance, admission by age, shared world, remote desktop, [room] e2ee or call): it refuses such a configuration.

  • keeps calls ([call], live-messaging §10). A caller proves its key and places a call: the authority signs its descriptor (WCT1) and the caller's grant; the caller's invitations (WCI1), checked by wai-rs's call verifier, are held in each invitee's inbox, which it reads on a proof of its key; its answers (WCA1) are checked the same way, and an accept comes with its grant. At each invitation's deadline the clock signs a missed call. The media is the room's audio and video, as in any room. POST /grant is refused: the grants come with the call. A call room is not also a watch room, a room with co-performance, one admitted by age, a world room or a remote desktop room: the service refuses such a configuration.

A room lasts while anyone publishes in it. When the last publisher has left, the next one starts a new room: a new descriptor at a new t0, under the same authority and clock, so each room's grants and chain hold one stretch of company.

Running it

wai-room keygen <keys.toml>       new keys (mode 0600, never over a file); prints the
                                  relay configuration lines that trust them
wai-room relay-lines <keys.toml>  those lines again
wai-room invite <keys.toml> [<s>] an invitation to host a remote desktop room, signed by
                                  the room's authority, good for <s> seconds (3600)
wai-room --config <room.toml>     serve (room.example.toml shows every field)
wai-room participant --service <url> --relay <url> [--quic <addr> --cert-sha256 <hex> ...]
                                  synthetic participants of a development stack on this
                                  machine, driven by JSON control lines: what `wai dev` runs

The keys file must be readable by its owner alone; the service refuses it otherwise. Keys has no Debug, so that no log prints a seed.

The relay needs, beside its listener and limits, the lines relay-lines prints:

[[auth.authorities]]
key = "<authority>"

[[auth.grants]]                  # the clock's events track, in any room
prefix = ["*", "<clock>", "events"]
token_sha256 = "<SHA-256 of the clock's token>"
publish = true
unlimited = true                 # no session or ingress limit for the clock's session

[[auth.grants]]                  # the clock watches the publishers
prefix = []
token_sha256 = "<SHA-256 of the clock's token>"
subscribe = true

and a door that holds each publisher's media to its own key:

[[receipts.door]]
prefix = ["*", "*", "video"]
trust_field = 1

The image

deploy/Containerfile builds wai-room as an OCI image from the repository's root: the binary built with the pinned toolchain from the locked dependencies, on a slim base pinned by digest, run as the unprivileged user wai (uid 10001), its keys in the volume /var/lib/wai-room, and a configuration (deploy/room.toml) that serves HTTP on port 9682 and reaches a relay container named relay at ws://relay:4480/moq. The keys are made once, and the lines that make the relay trust them are printed:

docker run --rm -v wai-room:/var/lib/wai-room wai-room keygen /var/lib/wai-room/keys.toml
docker run -v wai-room:/var/lib/wai-room -p 9682:9682 wai-room

deploy/check-image.sh builds it and runs it beside the relay's image on one network, both with a read-only root and no capabilities, and requires /health with the clock's session to the relay up. A release pushes it to the forge's container registry as git.transaction.science/transaction-science/wai-room:<version>.

Synthetic participants

wai-room participant (src/participant.rs) is the client half of WAI's one local stack, wai dev: test principals that enter a room through a relay and a room service on this machine, each running as its own process from its configuration. It reaches the service over HTTP, for grants and for the room it serves, and the relay over its WebSocket binding or, impaired, its native QUIC binding, the relay's development certificate pinned by its SHA-256. The service and the relay must be on loopback addresses, checked before anything is sent. Each object goes out with its receipt: a group receipt signed by the publisher's key on the track's receipts track, sent before the group's first object, and the receipt as the object's WAI_RECEIPT property, so a relay that holds each publisher's tracks to its own key passes them. On the QUIC binding each session goes through a UDP proxy of its own (joule-contrib's lossy) that drops, delays and spreads its datagrams from a fixed seed; loss applies to datagrams, so to QUIC alone.

Its first line on standard output is {"ready": true, "participant": true, "development_only": true, "service", "relay", "transport", "impair"}, and it answers JSON control lines on standard input, each with an op and an optional id:

op Fields Answer
join seed (a test principal's Ed25519 seed, 64 hex), grant (a publisher's grant for that key, hex), objects (per track, 1 to 64), receipts (default true) the principal enters with its grant and a proof, publishes audio and video with that many objects on each, and the answer comes once the clock has signed its join and both publishes: {"principal", "room", "tracks", "receipts", "objects_published", "objects_served", "events"}
leave principal (64 hex) its session closes; the answer comes once the clock has signed its leave
session the room as live-rooms §9 gives it to a verifier: {"room", "t0_us", "descriptor", "grants", "key_log_head": null, "chains"}
test objects (50), every_ms (20) a publisher and a viewer of fresh keys: {"transport", "impair", "objects_sent", "objects_received", "objects_lost", "latency_ms", "path", "impaired", "seconds"}; path is QUIC's own count of packets sent and declared lost and its round trip, impaired what the proxy dropped
load viewers (8, at most 64), objects, every_ms one publisher and that many viewers: {"viewers", "deliveries_owed", "deliveries_made", "delivered", "latency_ms", "enter_s", "path", ...}, the client half alone
describe the ready line's fields

Standard input closing ends it. tests/participants.rs holds it to a relay with doors: a participant without receipts is dropped there (and served where there is no door), impairment changes the loss QUIC measures, and a load is delivered in full.

src/dev.rs is a test fixture, not a mode of the binary: the relay and the service in one process on loopback, keys in memory, for this crate's tests.

HTTP

openapi.json describes this API (OpenAPI 3.1): every path, its requests, answers and statuses, in every mode the service keeps. Each operation's operationId is its name in the WAI toolchain's catalogue, noun.verb (room.show is GET /room, room.grant is POST /grant), the name the other doors derive theirs from (wai mcp's tool room_grant). tests/openapi_contract.rs holds it to the code: every operation is named as the catalogue names it, the routes the routers match are exactly its operations, every status it lists for an operation is one that operation's handlers return and every status they return is listed, and the service, run in each mode, answers every operation with a status it lists.

Request Answer
GET /, GET /health {"ok", "relay": the clock's session is open, "room"}
GET /room the room: room, t0_us, descriptor, compose, grants (hex), present, publishers, max_publishers, e2ee ({"record": <hex>, "log"}, or null), co ({"policy": <hex>, "audience": [<hex>]}, or null), and world ({"statement": <hex>, "receipt_alias": 4}, or null)
POST /grant {"principal": <64 hex>, "role": "publisher" | "viewer"} {"grant", "digest", "exp", "role", "room": <as GET /room>}; 409 full or busy; 429 rate with retry_after_s; 503 relay while the clock has no session
GET /mls?room=<id>&from=<n>[&wait_ms=<ms>] an end-to-end encrypted room's log from entry n: {"room", "entries": [{"seq", "event" or "mls": <hex>}], "next", "end"}, held up to 8 s while nothing is past n; 410 for a room since replaced, 404 for a room that is not end-to-end encrypted
POST /mls/key-package {"room", "key_package": <hex>} kept for the principal's Add: {"ok", "principal"}; 403 for a principal the room has not granted
POST /mls/post {"room", "frames": [<hex>]} a GroupInfo, or a commit and its Welcome, checked by the clock as a member checks it, then carried: {"ok", "seq"}; 409 {"error": <reason>} when the clock refuses it
POST /grant in a room that admits by age without a token: 401 {"error": "token_required", "age"} and a WWW-Authenticate: PrivateToken challenge=…, token-key=…, max-age=… for each key; with Authorization: PrivateToken token="…": the grant, its age the token's threshold, or 403 {"error": <age-assurance §11's reason>} and the challenges again; 503 busy when the window's nonces are at their bound. GET /room's age is {"policy", "ratings", "age", "token_window_s"}
GET /age/receipt {"receipt", "policy"}: the room's admission receipt so far, signed by its clock
GET /watch/time a watch room: {"now_us", "t0_us"}, the service's clock, for a page's estimate of the room clock
GET /watch/source a watch room's source, its bytes whole, of its media type
GET /watch/challenge {"challenge": <hex>}: single-use, good for a minute
POST /watch/join {"room", "grant": <hex>, "proof": <hex>} the proof (live-rooms §10.5.1) over a challenge, for POST //<host><public_prefix>/watch/join; the clock signs the principal's join, after its leave if the room took it for gone and that is not signed yet: {"ok", "t"}; 401 {"error": <reason>} for a proof that fails
POST /watch/post {"room", "events": [<hex>]} (at most 8) the viewer's events, each checked and carried in order: {"ok", "carried", "log"}; 409 {"error": <reason>, "carried"} at the first refused
GET /watch?room=<id>&from=<n>[&wait_ms=<ms>] the watch room's log from entry n: {"room", "entries": [{"seq", "event": <hex>}], "next", "end"}, held up to 8 s while nothing is past n
GET /watch/room the room as a co-watch verifier is given it (descriptor, grants, chains, policies, sources), the latest window's skew statements read, and the refusals by reason
POST /grant in a remote desktop room {"principal", "role": "host" | "controller" | "viewer", "invitation"?} the grant; 403 {"error": "not_the_host" | "invitation_required" | "invitation_used" | "invitation_expired" | "invitation_bad_signature" | "control_not_allowed"}; 409 hosted while the room's host holds its place. GET /room's remote is {"host", "allow", "control", "max_age_ms", "screen": {"digest", "statement"} or null, "holder", "floor_refused", "inputs", "floor_statements", "cut", "refused", "records", "max_records", "max_records_per_principal", "records_over", "ended"}
POST /remote/control {"to", "control": 0 to 15} the host's grant exchange, its grant and proof presented as a co-performance request presents them: {"ok", "grant", "digest", "effective"}; 403 for a grant that is not the host's, 409 {"error": "not_present"}
GET /remote/room the session as the clock holds it: {"room", "descriptor", "grants", "key_log", "chains"}, what a remote desktop verifier is given. The records hold what controllers typed and pasted, so they are the room's: a request presents a grant this room issued and its proof over the service's challenge, as a co-performance request does; 401 with a challenge otherwise, 403 for a grant of another room. Bounded per client
GET /remote/state that session verified by wai-rs, to a grant of this room and its proof as /remote/room: the outcome, the screens, each input's fate, the floor's changes and the floor statements §7.2 refused, the latencies, each host's totals, the clock's own floor, and each principal the clock cut and why; bounded per client

Every answer is JSON with Cache-Control: no-store. The client's address is CF-Connecting-IP, else the first X-Forwarded-For, else the connection's: the service listens on loopback behind the front that sets them.

GET /metrics is the operator's: the service's counters in the text exposition format (wai/wai-metrics), answered before the routes of the API, so openapi.json does not describe it and the catalogue does not name it. It holds the requests by route, method and status (wai_room_requests_total), the grants issued at POST /grant by role (wai_room_grants_total; a call's and a conversation's grants come with their own requests, and are not among them), the rooms kept since the start, whether the clock's session at the relay is open, and the room now: the principals present (wai_room_present: a watch room's viewers who joined among them, and in a conversation its members), the publishers holding a place, the audience members holding one (a room with co-performance's, or a remote desktop room's controllers and viewers), and the viewers' grants, its fan-out (wai_room_viewer_grants: a remote desktop room's controllers among them; a conversation has none). No room, principal, grant or client is a label. It is read at the service's own address: a request that came through a front, one carrying X-Forwarded-For, CF-Connecting-IP or another of the fields a front sets, is answered 404, and a front that sets none of them is configured not to pass the path on.

A conversation's requests (each, but GET /msg and GET /msg/challenge, with a grant the conversation issued and its proof, or for /msg/join a proof naming no grant):

Request Answer
GET /msg the conversation: room, t0_us, descriptor, record, statement, clock, grants, members, the latest epoch, the log's length
GET /msg/challenge {"challenge": <hex>}, single-use
POST /msg/join {} {"grant", "digest", "exp", "role": "owner" | "member", "conversation"}; 403 not_listed or removed, 409 full
POST /msg/key-package {"key_package"} kept for the member's Add
POST /msg/mls {"frames": [<hex>]} a GroupInfo, or a commit and its Welcome: {"ok", "seq", "end"}, or 409 with the clock's reason
POST /msg/post {"group", "properties", "payload", "receipts"} a message, taken after §9.2's checks: {"ok", "seq"}, or 409 with the reason
POST /msg/ack {"ack"} an acknowledgement, taken after §8's checks
POST /msg/leave, POST /msg/remove {"principal"} the clock's leave, reason 1, or reason 2 by an owner
GET /msg/log?from=<n>[&wait_ms=<ms>] the entries owed from n ({"entries": [{"seq", "event" | "mls" | "epoch" | "message" | "ack"}], "withheld", "next", "end"}), held up to 8 s while nothing is new; 403 not_a_member

A call's requests (/call/ring with the caller's grant and proof, the rest on a proof naming no grant): GET /call/challenge; POST /call/place {"invitees": [<64 hex>]} (the call descriptor, its digest and the caller's grant, 409 busy while a call has its places); POST /call/ring {"invitations": [<hex>]}; GET /inbox; POST /call/answer {"answer"} (an accept answered with the callee's grant); GET /call, to the call's parties.

Tests

tests/openapi_contract.rs holds openapi.json to the service (above). tests/mcp_dev_room.rs builds wai with the feature mcp, in a target directory of its own, and drives wai mcp --toolset core,dev with an MCP client: the tool room_grant and the API's POST /grant named alike (room.grant), a development room started on loopback, grants for two test principals, each joining and publishing, the session verified as live-rooms §9 verifies a room, a changed record refused, a leave verified, and a grant from a service off the machine refused.

cargo test (this crate is a workspace of its own, with its own lock file, since it builds on wai-rs): the keys file, the grants checked against the descriptor and read by the relay's reader, the places, the limiter, and tests/room_service.rs, the service against a relay on loopback: a grant and its proof open a session and the clock announces the publisher, the grant alone opens none, the room verifies, a third publisher is refused, clients are bounded, and a new room starts when the last publisher has left; and an end-to-end encrypted room on a relay, two members keying through the service as pages do (a GroupInfo, the clock's Add, a commit and its Welcome), the posts the clock refuses not carried, each opening what the other seals, and a member that leaves removed by the next commit. Admission by age: unit tests of src/age.rs over published RSA test keys (tests/common/rfc_test_keys.rs: RFC 7517 A.2's, RFC 9500's testRSA2048 and RFC 9578 A.2's), whose tokens they sign directly, as the blind protocol's output is the same signature: the challenges by window, a token admitted once and at the room's age, the window before accepted and the one before that, another origin's and another service's challenge refused, the nonces bounded and dropped by window, and the receipt's cells the reference's and its signature the clock's; and tests/age_admission.rs, the service with [age] and a relay: the 401 and its challenges, tokens at 16 and 18 admitted and their grants' age read by the relay, one of 13 refused below_threshold, one spent twice token_replayed, one for another origin token_wrong_challenge, one that does not read token_malformed, a publisher challenged too, the relay serving the viewer at 16 the audio and refusing it the video the publisher rates 18, and the receipt read and checked by wai-rs. A room with co-performance: its policy, the audience-input power and the budgets on its grants, and two viewers' votes in a window the clock opens, commits and selects, verified as co-performance §9 verifies a room. A watch room against a relay on loopback: its source served and named by its policy, viewers admitted by a proof for the request they make (a proof for another target, or presented again, refused), their controls and statements carried and a control in the past refused, the clock's skew statements of each window, each viewer's leave once silent, and the room the log carries verified as co-watch §11 verifies it, every viewer in sync. A remote desktop room (tests/remote_room.rs, with the host agent and headless viewer of joule-moq-remote): the host reaches the service over https:// and the relay over wss:// through TLS fronts, enters by an invitation, and a second key presenting it is refused, two viewers make their keys and get viewers' grants, the host hands control to one by a grant exchange it proves its key for (the grant with no proof, a proof by another key, and a proven request by a principal that is not the host, each refused), her click under the floor is applied and the viewer's move refused input_not_permitted, her typing goes sealed to the host, is injected as typed, and neither a viewer following her events nor the session the service serves to a viewer holds a trace of it, her text in the clear refused input_unsealed, control and the floor pass to the second, a give by a controller that does not hold the floor and a release that crosses the host's take are each stated and change nothing (the clock, the host and the verifier hold the same floor, and neither author is cut), a record out of its chain's order is cut and its author removed by the clock and by the host, the session's records are refused with no grant (401) and with another room's (403) and read with a viewer's, a controller's and the host's grant and proof, and the session verified by wai-rs as the clock serves it and as the host holds it, at the commitment level and at the opened level under the host's opening key; a viewer flooding past a bound of 24 records of its own, cut there and removed by the clock and by the host while the host and a controller go on and the session verifies at both levels; a session the clock ends at a bound of 40 records, every principal removed, the host last, the store within the bound, the session whole and verified, and a new room for the next host; and unit tests of src/remote.rs: the grants' attributes checked against the descriptor and read by the relay's reader, an exchange chained to the grant it replaces on a tick, invitations, the configuration's rules, and the grant in effect from the clock's lifecycle.

A conversation (tests/messaging_room.rs), its members keying through real MLS groups and sealing with joule-moq's secure objects: three members message; one goes offline, the service restarts from its history store and the log is as it was, a fourth joins and a member other than the lowest commits; the one offline catches up, opening every message of both epochs; a thread, reactions, an edit and a retraction verify, the retracted message stated retracted; acknowledgements taken and refused; a member removed opens nothing sealed after its removal, is served nothing past it (not_a_member), and the service takes nothing more from it; every record of the log verifies offline, and the store holds no plaintext. And wai-rs's sealing of a message object and joule-moq's secure objects open what the other seals. A call (tests/call_room.rs) through a relay: placed, an invitation with a bad signature and one to a principal not invited refused, the callee's inbox read and verified, the call accepted, both parties publishing audio and video and each receiving the other's, the clock's join and publish for each; then a call among three in which one declines and the other is missed, signed by the clock and in its inbox, and a late accept refused; each call verified as live-messaging §10 verifies it.

Licence

Apache-2.0.