The -18 table gains a requester's FIN on namespace requests, the -16 notes the Request IDs admitted across streams; the relay README states how it ends a publisher's tracks and treats a closed control stream; -14 is listed as not implemented. The conformance workflow runs the -18 and -16 independent decoders beside -22's. Crate docs and descriptions name the three drafts. joule-code and wai CHANGELOG entries. Co-Authored-By: Claude Opus 5.5 <[email protected]>
35 KiB
Changelog
All notable changes to the Joule Code standard are recorded here. The format follows the spirit of Keep a Changelog; the standard versions independently of its siblings (tags joule-code-vX.Y.Z).
[Unreleased]
Added — joule-moq: draft-ietf-moq-transport-18 and -16 beside -22, and runs against other implementations' relays
joule-moq: codec modulesdraft18anddraft16(control messages, parameters, Key-Value-Pairs, subgroup, fetch and padding streams, datagrams), anddraft::Draft, which chooses a session's draft before its first MOQT byte by ALPNmoqt-NNor theWT-Available-Protocolsvalue, preferring -22, then -18, then -16; -22 stays the default.draft18::bridgeanddraft16::bridgemap every message and object to and from the -22 model and name what one draft can say and the other cannot (refused with REQUEST_ERROR, or left out and noted). The README tables each difference. Each draft has a vector corpus (tests/vectors/draft-18,tests/vectors/draft-16) that the crate regenerates and checks, and that a decoder written from that draft's text reads (moqt18_check.py,moqt16_check.py, run in CI); the drafts' own examples are among them.joule-moq-relay: sessions at -18 and -16 run behindtransport::bridge, which presents them to the relay as -22 sessions: at -18 stream by stream, at -16 by giving each request on the control stream a stream of its own (Request IDs renumbered, MAX_REQUEST_ID granted, cancelled streams become -16's cancel messages). The client takes a draft, and the WebTransport client a TLS configuration.tests/drafts.rsruns the relay's scenario at each draft and across drafts, and sessions written from the -18 and -16 messages themselves.joule-moq-interop, new: the interoperability runner's test cases at -22, -18 or -16 against any relay, through the runner's test-client interface (TAP 14), andwai-receipts, which publishes a WAI track, its receipts track and a catalog through a relay and verifies every object at a second session against its receipt (inclusion, the receipts chain, the properties carried in the object), with--tamperto show the check refuses altered payloads, receipts or objects in flight.scripts/public.pyruns it against the relays the runner's registry lists,scripts/clients.shruns other implementations' test clients againstjoule-moq-relay, andscripts/local.shruns both on loopback. The runs that reach the public internet are not part of CI; the results of 2026-10-21 are injoule-moq's README.
Fixed — joule-moq-relay, found by those runs
- A PUBLISH_DONE that arrives before the streams it counts no longer cuts them: the relay waits until it has read as many of the publisher's data streams as the Stream Count says, or three seconds (-22 §9.9; the request stream and data streams are not ordered).
- A track published again after TRACK_ENDED is a new instance: the ended one's cache, Largest Location and subgroup owners go, so the new publisher's objects are not dropped as duplicates (a DVR-kept track is kept whole).
- A publisher that closes its control stream as it leaves (a protocol violation) loses no object or PUBLISH_DONE it sent: the session closes once its publishing has ended, or after three seconds; the end of its session ends its tracks with the PUBLISH_DONE status already read, not GOING_AWAY.
- At -18 a requester's FIN on PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE or SUBSCRIBE_TRACKS cancels it (-18 §6.1, §6.2), where at -22 it does not (§6.4.2.2).
- At -16 a Request ID ahead of one in flight on another stream (SUBSCRIBE_NAMESPACE rides its own) is admitted; a repeated one still closes the session.
Added — end-to-end encrypted rooms in the browser (L10b): joule-moq-web's e2ee build, the room service's log
joule-moq-mls: a member behind a carrier that orders the room's MLS messages.stage_createandstage_commithold the member's GroupInfo or commit until the carrier returns it (receiveapplies it then, checked as every member checks it); another's commit first, or a refusal, withdraws it (withdraw). Before its Welcome a principal follows the group from its handshake messages, sharing the clock's code; a Welcome for another and a GroupInfo of a group it holds are ignored.Clock::key_package_ifkeeps a KeyPackage only from a principal the room admitted. Featurejsbuilds the crate forwasm32-unknown-unknown: randomness and the clock from the page's own APIs, for the MLS library and for its crypto provider's generator.joule-moq: a keyring holds the locations it opened by epoch, and drops them with the epoch's keys.joule-moq-web, featuree2ee, built as a second wasm,joule_moq_web_e2ee, under a new workspace profilewasm(release, link-time optimisation, one codegen unit, size): the member (E2eeMember: KeyPackage, lifecycle events, the log's messages in order, staged create and commit, seal and open with the reason or the epoch to wait for), the record's check,TrackSigner.signSealed(the receipt over the sealed bytes, noWAI_RECEIPT), andUniItem.properties. The room page in such a room checks the record, loads that build, leaves a KeyPackage, follows the room's log (web/room/e2ee.js), seals each frame before it is signed and published, opens each object before it is decoded, shows its epoch and roster, and offers no watching and no seeking back.e2e/room.e2e.mjsgains thee2eescenario.joule-moq-room:[room] e2ee = truegives each room aWRE1record and a clock that is the MLS group's external sender, and the room's log:GET /mls,POST /mls/key-package,POST /mls/post;GET /roomcarries the record.
Changed — joule-moq-relay: a session over WebTransport closes with its code and reason
- The relay closes a WebTransport session, not the QUIC connection under it
(
draft-ietf-moq-transport-22§6.6): a WT_CLOSE_SESSION capsule on the CONNECT stream carries the MOQT session termination code as the session's 32-bit application error code, unmapped, and the reason (draft-ietf-webtrans-http3-16§6); the QUIC connection closes after the peer has closed its side of that stream, or two seconds on. A page now reads the relay's code and reason over WebTransport as it does over the WebSocket binding: a grant presented without its proof, for one, isUNAUTHORIZED(2) with its live-rooms §10.5 reason, where the page saw only that the session was lost. - Nothing of the session overtakes its close. A stream the relay lets go once a WebTransport session is closing is held until the QUIC connection has closed, so no FIN or STOP_SENDING reaches the page ahead of the capsule (a page whose stream was stopped just before the close was seen to lose its renderer); a stream the relay refuses before SETUP is kept until then too.
- A peer's close is read the same way.
closed()over WebTransport gives the code and reason of the peer's WT_CLOSE_SESSION (by_peer: true), so a page's close code reaches the session's signed summary; a QUIC connection closed with the session still open gives no MOQT code (its code is HTTP/3's). wtransport0.7.2 is vendored (joule-code-rs/vendor/wtransport, MIT OR Apache-2.0, patched in for the joule-code workspace andjoule-moq-room), because the release reads WT_CLOSE_SESSION but cannot send it. The change gives itConnection::close_sessionandConnectionError::SessionClosed, and makesclosed()report the session's end;VENDORED.mdlists every change and why. Its unit tests, the capsule's bytes against the draft's layout among them, run in the relay's CI job.- Tests.
tests/webtransport_close.rs: the relay refusing a grant without its proof, refusing a stream that breaks a rule before SETUP (with no STOP_SENDING ahead of the close), refusing a session past its session count, and ending one at its time limit, each read by the client as the exact code and reason; and a client's own close, recorded as the peer's in the relay's summary. The browser end-to-end check of a bare grant now requires code 2 and the reason over WebTransport too.
Added — DVR on the relay (L7b): joule-moq-relay's DVR store, the seek power, the room page's Seek back
joule-moq-relaykeeps a DVR window on disk ([dvr],[[dvr.window]]; WAI live-recording §8): every track under a window's prefix, with its receipts, a group stored once the cache holds it whole, appended to the track's current segment file (a new one every ten seconds or 16 MiB) and synced at least once a second. A window by age (at most 24 hours) and by bytes, evicted lowest group first by whole group, each media group with its receipts group, so a track's window is always one run of groups. On start every object is checked again (its own check, then its receipt, or each event record's signature): one that does not verify is dropped, counted, written out of its file, and never served. A FETCH the cache does not cover is answered from the cache and the store together where they cover it, read as the fetch stream drains; what was evicted is an End of Unknown Range. TRACK_STATUS and FETCH answer from the store once a publisher is gone.- The seek power at the relay (WAI live-rooms §10.6): a FETCH or fill below the live edge
needs an authority's grant whose powers hold bit 4, or a rule with
seek = true; otherwiseUNAUTHORIZED,seek_not_granted.[[auth.grants]]rules gainseek(default false). - Moderation in the store: the relay reads
decisionevents from the events tracks it forwards, honours a room key's (dvr.room_keys, the authorities' keys) or a moderator grant's, and withholds what deny or redact target from every FETCH and fill, erasing a redacted object from disk; the decisions are kept indecisions.wdlacross a restart. - A sealed room is never kept (WAI confidentiality §9, live-recording §8.2). The store
knows the room by its first object carrying a secure-objects Key ID. That object's group is
never written. The store then deletes what it held of the room (its events and receipts, in
the clear) and drops the room's decisions from the log. It writes nothing more of the room,
and
sealed.wdlkeeps it so across a restart. WMH1version 2 from a relay with a store, with its DVR figures; the session summary's tracks gaindvr_objectsanddvr_bytes.- The cache, for every relay: a media group and its receipts group leave the cache together, and evicting one moves the other's mark, so a FETCH never serves media from the cache without its receipts.
joule-moq:GrantHeadreads a grant'spowers(moderates,seeks);receipt::verify_strictis public.joule-moq-room: every role, and so every grant, holds the seek power, so a late joiner's FETCH of earlier groups is served by a relay that checks it.joule-moq-ingestreads roles and grants with the seek power.joule-moq-web: the room page's Seek back control fetches a participant's last seconds from the relay's window, verifies every object against its receipt, plays the video from its first key frame in a view of its own, and returns to live; the browser end-to-end test'sdvrscenario covers it in a real browser against a relay whose cache is far smaller than the span sought back.
Added
-
§5.1 Grant-gated consultation (OPTIONAL) — the JCP binding: tier as granted action (
l0…l4), energy class as chained budget debit, denial as fallthrough, every consultation sealed as a chained JCR-1 receipt. Reference binding: thejoule-code-jcpcrate (JcpGate::wrapover anyStage); nol4in the chain → no remote frontier agent, structurally. Conformance gains class C5 with a published vector file (gated_cascade_vectors.json, fixed fixture identities, deterministic end to end) run by the harness against the real jcp runtime alongside C1–C4. -
joule-code-cascadeships a reference L0 (Cache) stage: a content-addressed cache of resolved coding operations keyed by(prior, intent). A hit closes the request at L0 (picojoule class) without consulting any higher tier — the first concrete cascade stage, making stop-at-first-close demonstrable in the reference library. A third party may use it or substitute its own. -
joule-code-cascadeships a genericFnStage: any tier (L1 Lawful, L2 Embed, L3 Model) can be expressed by supplying a resolver closure, so a consumer binds the JouleClaw agent schema — or any other runtime/model — to the cascade without this crate depending on it. (A test wires an L3 model stage behind an empty L0 and confirms it closes at L3.) -
examples/l3-jouleclaw/— a consumer binding showing JouleClaw's L3 (Model) tier wired into the cascade: ajouleclaw_llm_cheap::LlmBackendbehind aFnStage(L3)that generates aCodingOp+ joules-class receipt. Three backends share the slot:EchoBackend(model-free, for the unit tests);JouleclawRuntimeBackend, which runs a GGUF model in-process on JouleClaw's own from-scratch tensor runtime (jouleclaw-runtimeoverjouleclaw-loader-ggufand the reference/Apple kernel backends — no llama.cpp, no FFI); andLlamaCompletionBackend, which shells out to llama.cpp. Therunbinary generates with a local model (in-process runtime by default;JOULE_CODE_L3_ENGINE=llamafor the shell-out), content-addresses the result as aCodingOp, and meters host energy over the inference for the receipt (honest provenance:HwShunton RAPL/NVML,Estimatoron Apple). Verified live: the llama.cpp path against a local 350M GGUF, and the in-process from-scratch runtime againstternary-bonsai-1.7b-q2_0(qwen3, Q2_0) — real text, a realop_id, the cascade closing at L3 (tests/runtime_l3.rs, opt-in via--ignored). Standalone workspace, so the standard's lib stays free of a JouleClaw dependency;jouleclaw_dispatch::ModelBankselects the cheapest capable backend in production. -
joule-code-collab(Research) lands the federated-collaboration interface (Collabtrait: submit / committed / membership), a single-node reference cluster (LocalCluster), and the BFT consensus core (LockstepCluster): a multi-node lockstep deterministic simulation with BFT supermajority commit — an op commits only when ≥2f+1ofn=3f+1honest nodes independently validate it, and committed ops areop_id-ordered so every honest node derives a byte-identical log. Tested: commits with honest agreement, toleratesfByzantine nodes (safety), loses liveness only beyondf, rejects non-members despite quorum, deterministic round ordering. It also addsprotected— FROST threshold authorization of protected operations: at-of-nFROST (Ed25519) aggregate signature over an op'sop_id, verifiable against the cluster's single group key (no member holds the whole key; fewer thantcannot authorize). Tested: a 2-of-3 threshold authorizes and binds exactly one op_id; below threshold cannot. And MLS group keying (RFC 9420, viaopenmls,tests/mls_keying.rs): the cluster members form an MLS group and derive a shared group key with no central server — verified that members agree on the exported secret, that adding a member advances the epoch, and that a member an epoch behind derives a different (forward-secret) key. (SPEC §7,spec/collab.md.) -
examples/transport-moqt/— the CommunicationOS (MoQT) transport binding, the last of the four federation layers. A consumer binding that carries a collaboration's committed (consensus-ordered) coding operations overinv-moq— CommunicationOS's MoQT (Media over QUIC Transport) protocol with its JWP receipt extension. The end-to-end test wires the whole stack in-process:LockstepClustercommits ops → each is published as a MoQT object → every object carries a real Ed25519/BLAKE3 signed receipt → a subscriber receives all ops, verifies the receipts against the publisher key, and recovers the originalSignedOpintact; a second test shows a receipt-requiring subscriber rejecting an untrusted publisher's group (ownerless authenticity). Driven throughinv-moq'sMockFabric(real signatures + BLAKE3 merkle receipts, no QUIC; thequicfeature is the real-wire path behind the same trait surface). Standalone workspace, so the reference library stays free of a joulesperbit dependency. The four federation layers — consensus, threshold authorization, group keying, transport — are now all implemented and tested. (SPEC §7,spec/collab.md.)
Changed
- §6 /
spec/receipt.md§2 — a vendor counter is a hardware reading only while it is demonstrably live. A reading from a vendor energy counter (RAPL, NVML, Apple SMC/IOReport) MAY be taggedHwShunt, and supplymeasured_uj, only while the counter has been observed advancing under load for the domain the figure claims (for IOReport: the CPU energy rail itself, in at least 5 sample intervals, at least 4 of every 5, over at least 300 ms of CPU burn). A counter that is present but not live — frozen, or republished in lumps — MUST NOT produce either; the reading falls back to the estimator. A figure MUST NOT claim coverage of a domain its counter does not read. No wire or conformance-vector change.
Added — the public room (L5c-2): joule-moq-room, room authorities at the relay
joule-moq-room, a room service (a workspace of its own, onwai-rs's room, event and composition records): it holds a room's authority and clock keys (a 0600 file it refuses to read otherwise), signs short-livedWRG1grants for keys pages make (POST /grant, a publisher's or a viewer's, bounded per client address, overall and per room), authorsjoin,publishandleaveon the clock's events track from what the relay carries (its clock watches each publisher's video track), and serves the signed descriptor and grants (GET /room). A new room starts when the last publisher has left.joule-moq-relay:[[auth.authorities]]honours every grant an authority's key signs, beside its proof, within its window, mapped as live-rooms §10.2 maps it (joule-moq'sGrantHeadreads it); a field*in a rule's prefix; a door rule'strust_field;limits.max_session_ms,max_ingress_kbpsandmax_resident_mb; a rule'sunlimited; a session ends when its authority grant expires; SIGTERM stops it as Ctrl-C does.joule-moq-relayfixes found on the deployed room: the WebSocket binding's multiplexer dropped the half of a stream that arrived after its session closed while it held its state's lock, which that half's Drop takes, and the relay's runtime deadlocked (any session the relay closed while its peer still opened streams could do it); and a FETCH not yet covered only because groups are still arriving now waits for them, briefly, rather than going to a publisher that takes no requests.joule-moq-web: the room page's service mode (its own key, a grant from the service, the WebSocket binding, Join, Watch, Leave and a typed caption),Room.setGrantsinwai-live-web,grantProofByandclientSetupWithProoffor a test's proof by another key, and theserviceend-to-end scenario, which also runs against a deployment (--base).
Changed — joule-moq, joule-moq-relay and joule-moq-web: L5 hardening
ReceiptChains: a join's fill from group 0. A group fetched into the gap below a join's anchor, with anullparent, is held as history and admitted when the group above it links back to it (WAI jwp-receipts §3 step 3), where it was refusedchain_break. Anullparent outside that gap, and a fork, are stillchain_break.- Receipts in WAI's binary form (jwp-receipts §2.7).
GroupReceipt::to_binary/from_binary(the canonical bytes, and a legacy group's parent),ObjectReceipt:: to_binary/from_binary/body_binary,read_receipts,link_hash_of,Malformed,RECEIPTS_MAGIC.ReceiptBundle::to_payloadandfrom_payloadkeep their signatures and now write and readWJR1;ReceiptBundle::readalso gives the link hash over the bytes received. TheJWP_OBJECT_RECEIPTproperty carries an object receipt standing alone in that form. The JSON payload is not read any more. Serde derives on the receipt types stay, for callers' own use; nothing on the wire uses them. - Proof of possession of a room grant (WAI live-rooms §10.5).
joule-moq::grant_proof: theWRP1record (GrantProof::sign,read,to_bytes),check_proof(§10.5.4, the grant's digest and principal from the caller's own reader),grant_digest_and_principal(for a verifier with none), the Setup OptionsSETUP_OPTION_GRANT_CHALLENGE(0x3A47) andSETUP_OPTION_GRANT_PROOF(0x3A49) withadd_challenge,challenge_of,add_grant,proofs_of,proof_for, the HTTP header names,contextand base64url. New dependency:sha20.10 (already in the lockfile). joule-moq-relay. The relay sends its SETUP first, with a fresh challenge and the receipt extension declared whatever the client says. A grant rule withproof = truenames a room grant's record: a SETUP that presents it without a proof that passes is closedUNAUTHORIZED(§12.2), and a request resting on it is refused unless its session proved it. The transports give the session the target the client addressed (WebTransport:authorityand:path, the WebSocket upgrade'sHostand path; native QUIC's AUTHORITY and PATH).Authorizer::allowstakes the proven set;Authorizer::requires_proof;ClientOptionsgainsgrants,targetandsign_challenge.receipts_track_suffixdefaults to_receipts, live-rooms §6.1's.joule-moq-web. A participant presents its grant with a proof signed in wasm (clientSetupWithGrant), the per-participant secret gone; the receipts tracks are0_receipts; the verifier measures each receipts object, and the same receipts as JSON.
Changed — joule-moq 0.3.0: draft-ietf-moq-transport-22
joule-moq speaks draft-ietf-moq-transport-22 (1 October 2026), negotiated by
the ALPN moqt-22, in place of its draft-08-style codec. No byte of the old wire
form survives: -22 changed the varint, the control streams, every message and the
object framing. The crate's version moves to 0.3.0 because its public API changes:
draft22: the codec of -22, a module of its own selected throughdraft::Draft, so another draft can be added beside it. Every control message and parameter, Key-Value-Pairs, Properties, Setup Options, the Authorization Token, Request IDs and request-stream rules, subgroup streams, datagrams, fetch streams and padding, and the status and error codes. Every refusal is a typedWireErrorcarrying the draft's consequence;MoqError::Wirewraps it.MOQT_DRAFT_VERSIONis0xff000016.transport_quic::ALPN_MOQTismoqt-22.messagesre-exports -22's control messages.SetupMessage,AnnounceMessage,SubscribeMessage,SubscribeFilterand the draft-08ControlMessageare gone. SUBSCRIBE'smax_mrl_layeris an OBJECT_PROPERTY_FILTER on theMRL_LAYERproperty (messages::mrl_cap_filter);require_receiptsis the subscriber's own policy, met by subscribing to a track's receipts track.carriage(new): the crate's objects ride -22 subgroup objects, with the object receipt, theWAI_RECEIPTproperty of WAIextensions/moq-streaming-format.md§5 and the layer fields as properties in an application range; a group's receipts ride its receipts track (§4.1).ReceiptBundlemoved here frommock, which re-exports it.session: the draft by ALPN (MoqSession::select_alpn); SETUP carries options and no version or role, andbuild_setuptakes no label; Request IDs by end.transport_quic: a pair of unidirectional control streams opened by SETUP, a request stream per request, subgroup, fetch and padding streams, and datagrams.ControlStream,write_object_to_uni,read_object_from_uni,write_group_receipt_to_uniandread_group_receipt_from_uniare replaced byQuicSession::{open_control, accept_control, open_request, accept_request, open_subgroup, open_fetch, accept_data},send_object,send_receipt_bundleandDataReceiver.wire::encode_varintanddecode_varintare -22's integer; everyu64encodes.mock: objects, receipt bundles and requests cross the -22 codec; the relay reads an object's delivery timeout from its OBJECT_DELIVERY_TIMEOUT property.tests/vectors/draft-22/: a corpus of -22 vectors, good and negative, generated bytests/draft22_vectors.rs, and an independent decoder that checks it in CI.
Added — joule-moq-ingest 0.1.0: WHIP into a room's MoQ tracks, WHEP out of them
A new crate, library and binary, between WebRTC and the MoQ tracks of a WAI room, on
joule-moq's codec and joule-moq-relay's client. The mapping is WAI's
extensions/live-ingest.md.
- WHIP (RFC 9725). POST an offer for the answer,
LocationandETag; PATCH for trickle ICE and ICE restarts, withIf-Match(428,412); DELETE. The bearer token is a WAI room grant, checked against the room's signed descriptor; a grant without the publish power for a media kind of the offer is403withinsufficient_scope(RFC 6750 §3.1). Every request that carries a grant also proves possession of its key (WAI-Room-Proofover a single-useWAI-Room-Challenge, WAI live-rooms §10.5.3), on the session's PATCH, DELETE, GET and HEAD as on the POST. - Into MoQ. str0m depacketises H.264, VP8, VP9, AV1 and Opus into frames; each frame is
one
WAI1envelope in its capability's framing, video grouped at keyframes and audio in fixed windows, with the frame's room-clock time (anchored at the first frame's arrival, re-anchored pastmax_skew_ms) and, on each group's first object, the grant reference. The gateway publishes the tracks and a signed catalog at the relay as their publisher and signs every group's receipts as the first hop, chained per session. - WHEP (
draft-ietf-wish-whep-05). A player POSTs an offer for an owner's tracks; the gateway reads the owner's catalog once its receipt verifies (409withRetry-Afteruntil then), joins each track at its latest group, verifies every object against its receipt before it packetises the frame back to RTP, and drops and counts what fails. - Bounds. Sessions, tracks per session, bitrate (stated as
b=AS), frame size and the queue to the relay, each a configuration field;gateway.example.tomlholds no secret. - A signed record per session, in the shape of live metering's per-hop record: the
seam a
WMH1writer plugs into. - The WebRTC stack is
str0m0.24.1 (MIT OR Apache-2.0), pinned, with its pure-Rust crypto backend; the HTTP resources arehyper1.11.1 (MIT). Every crate new to the lockfile is MIT, Apache-2.0 or ISC. - Tests, on loopback: WHIP to a relay subscriber with every receipt verified and the bytes equal, DELETE ending the tracks; WHEP playback from the joined group; VP8, VP9 and AV1 end to end; the refusals; trickle ICE and an ICE restart with media across it; the room reader against WAI's live-rooms corpus. A CI job runs every target on linux-arm64.
Added — joule-moq-relay: the per-hop session record of a WAI room
The relay signs WAI's per-hop session record, WMH1 (wai/extensions/live-metering.md
§3), of a room from its session summaries (metering::hop_record, Relay::hop_record):
for each track of the room its sessions carried, the objects and bytes in and out, the
objects dropped for their delivery timeout or still queued at the end with those whose
receipts it forwarded, the objects refused at the door and deduplicated; its subscriber
sessions' time by layer class, from the subscriptions each held; the events it delivered
(objects of Object ID above 0 on the room's events tracks); and a ledger of the room grants
its sessions ran under, by the token each presented in SETUP ([[metering.grants]] in the
configuration). Its own energy is the unmetered marker. The record holds nothing per object
or per viewer: media tracks are counted one by one, events tracks and every other track of
the room together, subscribers by layer class.
- The session summary carries what the record is made from, and its format is
joule-moq-relay/session-summary/2: per track, as a publisher, objects and bytes received, refused at the door and deduplicated; per track, as a subscriber, fetched bytes, objects still queued at the end and objects of Object ID above 0; the session's layer spans per room; and the room grant its SETUP token maps to. A datagram's bytes are now counted among a publisher's bytes received. tests/room_metering.rs: two publishers and three subscribers of one room, one subscriber slow, over QUIC and WebTransport; the record verifies under the relay's key and every count in it is the sum of the summaries' counts. Run withWAI_METERING_FIXTURE_DIR, the test writes the record and its ledger, which WAI's metering corpus holds as recorded.
Added — joule-moq-relay 0.1.0: a draft-ietf-moq-transport-22 relay
A new crate, library and binary, on joule-moq's codec. Publishers send it tracks,
subscribers ask it for them, and it fans each object out to every subscription in the
order -22's priorities give (§5.1.2), dropping what passes a subscriber's delivery timeout
at this hop (§5.2). Whatever it drops, the object's receipts still reach the subscriber:
a track's receipts track is never given a delivery timeout.
- Three transports, one draft: native QUIC (ALPN
moqt-22) and WebTransport over HTTP/3 (WT-Available-Protocols/WT-Protocol,draft-ietf-webtrans-http3-16) on one UDP endpoint, and a WebSocket fallback that carriesdraft-ietf-webtrans-http2-15capsules, one per binary message, under the subprotocolmoqt-22. The fallback is a non-normative binding, written out in the crate's README with example bytes. - The relay protocol: session setup; namespace publish and subscribe routing; PUBLISH; SUBSCRIBE served from an upstream the relay holds or opens; FETCH and fills from its cache or forwarded upstream; TRACK_STATUS; REQUEST_UPDATE; GOAWAY on shutdown; Request-ID parity and duplicate checks; the authorization-token alias cache; several publishers of one track, deduplicated by (group, subgroup).
- What the cache covers is worked out from the groups it holds whole, each time it is asked, so a group whose streams finish after a later group's is still served from the cache once it is whole; a group never seen is a gap it does not answer across.
- A signed session summary per session. The relay's own energy is the unmetered marker, since no meter backs it.
- Not yet: SUBSCRIBE_TRACKS and Range Filters, WebTransport over HTTP/2, 0-RTT, the relay moving its own upstream subscriptions, and finding a receipts track from the catalog rather than by its name; the README lists each.
Changed — joule-moq 0.2.0: WAI's labelled JWP forms, and a strict verifier
joule-moq reads and writes the forms of WAI's extensions/jwp-receipts.md
that sign an object's energy figure, its class and its origin, and a group
total's class. Every receipt in the legacy forms keeps its bytes and verifies as
before; a verifier that predates the labelled forms refuses them. The crate's
version moves to 0.2.0 because its public API changes:
ObjectReceiptgainsenergy_class: Option<Vec<u8>>(the class in WAI energy-measurement §2.7's signed encoding). Present, the object's leaf isBLAKE3("moq-jwp:merkle-leaf\x02" ‖ content_hash ‖ u64_be(joules_micro) ‖ class ‖ u32_be(len(origin)) ‖ origin); absent, the legacy leaf. New:ObjectReceipt::leaf,ObjectReceipt::check_label,ReceiptBuilder::add_labelled_object,ReceiptBuilder::class_for.GroupReceiptgainsenergy_class,energy_coverageandparent_receipt_hash. With a class, the group is signed undermoq-jwp:group-sign\x02, which covers the class, the coverage and the parent link.energy_coverageis written only when it is non-zero, and a legacy group carrying a non-zero coverage is refused: its signature does not cover one. New:ReceiptBuilder::finalize_labelled,GroupReceipt::link_hashandcompose_group_class, the rule a labelled group's class is composed by.ReceiptVerifier::verify_groupreturns aVerifiedGroup(the root with the group's leaf count and coordinates) instead of the bare root, andverify_objectandverify_payloadtake&VerifiedGroup. An object is accepted only at the position its proof walks: its coordinates are the group's,object_idis its leaf index, the proof has the group's depth and its direction bits are the index's.MerkleProof::verifyis replaced byMerkleProof::verify_leaf, which takes the leaf in its form and the group's leaf count, andMerkleProof::walks_its_position.verify_groupholds the key and the signature to WAI jwp-receipts §3.1 step 3's rule: the publisher key must be the canonical encoding of a point not of small order (aycoordinate ≥ p is refused),Smust be below L (checked by the crate as well as byverify_strict),Rmust not be of small order, and the cofactorless equation must hold.JWP_MOQ_EXTENSION_VERSIONis 2.tests/wai_jwp_corpus.rsruns every leaf, group and object vector ofwai/jwp-conformance/vectors.jsonthrough the crate.
External JWP implementations need the same update to verify labelled leaves and groups, and to refuse what this verifier refuses.
[0.2.0] — draft
Changed
- Receipts now seal as JCR-1 envelopes. The receipt's signed form is the family-shared JCR-1 wire format — canonical det-CBOR (RFC 8949 §4.2.1, length-first key order) inside a COSE_Sign1 envelope (RFC 9052, EdDSA
alg = -8), as thejoule-code/1profile. This replaces v0.1's bespokeestimated_uj ‖ sequence ‖ noncepreimage. A Joule Code receipt now verifies under the family's existing JCR-1 tooling and its cross-language (Python/Go) reference verifiers. (SPEC §6.1.) joule-code-receiptgains ajcr1module (canonical CBOR + COSE_Sign1, vendored from the JCR-1 reference, no JouleClaw dependency) andReceipt::to_jcr1_wire/seal/verify_envelope; the bespokesign/verifyare removed.- Conformance C3 vectors regenerated as JCR-1 COSE_Sign1 envelopes; the harness verifies the envelope and that its payload is the receipt's canonical core. A byte-equality test pins the canonical encoder against a published JCR-1 vector.
[0.1.0] — draft
Initial draft standard.
Added
SPEC.md— normative specification: the cascade requirement (L0–L4, stop-at-first-close), the receipt requirement (measured + estimated,Provenance), and the no-capture requirement.- The coding-operation cargo: content-addressed (BLAKE3-256), signed (Ed25519, prior-state-bound) transitions over a source tree; deterministic CBOR canonical form.
- The receipt schema in Joule Wire Protocol form, energy-signed.
- The cascade interface and the agent-schema binding to JouleClaw; the L4 binding to remote frontier agents over ACP.
- The federated-collaboration binding, specified at interface level and classified Research.
joule-code-rs/reference library:joule-code-schema,joule-code-cargo,joule-code-receipt,joule-code-cascade,joule-code-conformance.conformance/v1/reference vector pack (C1–C4), internally consistent pending regeneration against the reference library.