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]>
417 lines
35 KiB
Markdown
417 lines
35 KiB
Markdown
# 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 modules `draft18` and `draft16` (control messages, parameters,
|
||
Key-Value-Pairs, subgroup, fetch and padding streams, datagrams), and `draft::Draft`, which
|
||
chooses a session's draft before its first MOQT byte by ALPN `moqt-NN` or the
|
||
`WT-Available-Protocols` value, preferring -22, then -18, then -16; -22 stays the default.
|
||
`draft18::bridge` and `draft16::bridge` map 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 behind `transport::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.rs` runs 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), and
|
||
`wai-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 `--tamper` to show the check
|
||
refuses altered payloads, receipts or objects in flight. `scripts/public.py` runs it
|
||
against the relays the runner's registry lists, `scripts/clients.sh` runs other
|
||
implementations' test clients against `joule-moq-relay`, and `scripts/local.sh` runs both
|
||
on loopback. The runs that reach the public internet are not part of CI; the results of
|
||
2026-10-21 are in `joule-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_create` and `stage_commit` hold the member's GroupInfo or commit until the carrier
|
||
returns it (`receive` applies 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_if` keeps a KeyPackage only
|
||
from a principal the room admitted. Feature `js` builds the crate for
|
||
`wasm32-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`**, feature `e2ee`, built as a second wasm, `joule_moq_web_e2ee`, under a new
|
||
workspace profile `wasm` (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, no `WAI_RECEIPT`), and
|
||
`UniItem.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.mjs` gains the `e2ee` scenario.
|
||
- **`joule-moq-room`**: `[room] e2ee = true` gives each room a `WRE1` record 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 /room` carries 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, is `UNAUTHORIZED` (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).
|
||
- **`wtransport` 0.7.2 is vendored** (`joule-code-rs/vendor/wtransport`, MIT OR
|
||
Apache-2.0, patched in for the joule-code workspace and `joule-moq-room`), because the
|
||
release reads WT_CLOSE_SESSION but cannot send it. The change gives it
|
||
`Connection::close_session` and `ConnectionError::SessionClosed`, and makes `closed()`
|
||
report the session's end; `VENDORED.md` lists 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-relay` keeps 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`; otherwise
|
||
`UNAUTHORIZED`, `seek_not_granted`. `[[auth.grants]]` rules gain `seek` (default false).
|
||
- **Moderation in the store**: the relay reads `decision` events 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 in `decisions.wdl` across 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.wdl` keeps it so across a restart.
|
||
- **`WMH1` version 2** from a relay with a store, with its DVR figures; the session summary's
|
||
tracks gain `dvr_objects` and `dvr_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`**: `GrantHead` reads a grant's `powers` (`moderates`, `seeks`);
|
||
`receipt::verify_strict` is 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-ingest`** reads 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's `dvr` scenario 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: the `joule-code-jcp` crate (`JcpGate::wrap` over any `Stage`); no `l4` in 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-cascade` ships 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-cascade` ships a generic **`FnStage`**: 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: a `jouleclaw_llm_cheap::LlmBackend` behind a `FnStage(L3)` that generates a `CodingOp` + 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-runtime` over `jouleclaw-loader-gguf` and the reference/Apple kernel backends — no llama.cpp, no FFI); and `LlamaCompletionBackend`, which shells out to llama.cpp. The `run` binary generates with a local model (in-process runtime by default; `JOULE_CODE_L3_ENGINE=llama` for the shell-out), content-addresses the result as a `CodingOp`, and **meters host energy over the inference** for the receipt (honest provenance: `HwShunt` on RAPL/NVML, `Estimator` on Apple). Verified live: the llama.cpp path against a local 350M GGUF, and the in-process from-scratch runtime against `ternary-bonsai-1.7b-q2_0` (qwen3, Q2_0) — real text, a real `op_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::ModelBank` selects the cheapest capable backend in production.
|
||
- `joule-code-collab` (Research) lands the **federated-collaboration interface** (`Collab` trait: 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+1` of `n=3f+1` honest nodes independently validate it, and committed ops are `op_id`-ordered so every honest node derives a byte-identical log. Tested: commits with honest agreement, tolerates `f` Byzantine nodes (safety), loses liveness only beyond `f`, rejects non-members despite quorum, deterministic round ordering. It also adds `protected` — **FROST threshold authorization** of protected operations: a `t`-of-`n` FROST (Ed25519) aggregate signature over an op's `op_id`, verifiable against the cluster's single group key (no member holds the whole key; fewer than `t` cannot authorize). Tested: a 2-of-3 threshold authorizes and binds exactly one op_id; below threshold cannot. And **MLS group keying** (RFC 9420, via `openmls`, `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 over [`inv-moq`](../../joulesperbit/crates/inv-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: `LockstepCluster` commits 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 original `SignedOp` intact; a second test shows a receipt-requiring subscriber rejecting an untrusted publisher's group (ownerless authenticity). Driven through `inv-moq`'s `MockFabric` (real signatures + BLAKE3 merkle receipts, no QUIC; the `quic` feature 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 tagged `HwShunt`, and supply `measured_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, on `wai-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-lived `WRG1` grants for keys pages make
|
||
(`POST /grant`, a publisher's or a viewer's, bounded per client address, overall and per
|
||
room), authors `join`, `publish` and `leave` on 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`'s `GrantHead` reads it); a field `*` in a rule's prefix; a door rule's
|
||
`trust_field`; `limits.max_session_ms`, `max_ingress_kbps` and `max_resident_mb`; a rule's
|
||
`unlimited`; a session ends when its authority grant expires; SIGTERM stops it as Ctrl-C
|
||
does.
|
||
- **`joule-moq-relay` fixes 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.setGrants` in
|
||
`wai-live-web`, `grantProofBy` and `clientSetupWithProof` for a test's proof by another
|
||
key, and the `service` end-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 a `null` parent, is held as history and admitted when the group
|
||
above it links back to it (WAI jwp-receipts §3 step 3), where it was refused
|
||
`chain_break`. A `null` parent outside that gap, and a fork, are still `chain_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_payload` and `from_payload` keep their
|
||
signatures and now write and read `WJR1`; `ReceiptBundle::read` also gives the link
|
||
hash over the bytes received. The `JWP_OBJECT_RECEIPT` property 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`:
|
||
the `WRP1` record (`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 Options `SETUP_OPTION_GRANT_CHALLENGE` (`0x3A47`)
|
||
and `SETUP_OPTION_GRANT_PROOF` (`0x3A49`) with `add_challenge`, `challenge_of`,
|
||
`add_grant`, `proofs_of`, `proof_for`, the HTTP header names, `context` and base64url.
|
||
New dependency: `sha2` 0.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 with `proof = true`
|
||
names a room grant's record: a SETUP that presents it without a proof that passes is
|
||
closed `UNAUTHORIZED` (§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 `:authority` and `:path`, the WebSocket upgrade's `Host` and path; native
|
||
QUIC's AUTHORITY and PATH). `Authorizer::allows` takes the proven set;
|
||
`Authorizer::requires_proof`; `ClientOptions` gains `grants`, `target` and
|
||
`sign_challenge`. `receipts_track_suffix` defaults 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 are
|
||
`0_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 through `draft::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 typed `WireError` carrying the
|
||
draft's consequence; `MoqError::Wire` wraps it.
|
||
- `MOQT_DRAFT_VERSION` is `0xff000016`. `transport_quic::ALPN_MOQT` is `moqt-22`.
|
||
- `messages` re-exports -22's control messages. `SetupMessage`, `AnnounceMessage`,
|
||
`SubscribeMessage`, `SubscribeFilter` and the draft-08 `ControlMessage` are gone.
|
||
SUBSCRIBE's `max_mrl_layer` is an OBJECT_PROPERTY_FILTER on the `MRL_LAYER`
|
||
property (`messages::mrl_cap_filter`); `require_receipts` is 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, the `WAI_RECEIPT` property of WAI `extensions/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). `ReceiptBundle` moved here from `mock`,
|
||
which re-exports it.
|
||
- `session`: the draft by ALPN (`MoqSession::select_alpn`); SETUP carries options
|
||
and no version or role, and `build_setup` takes 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_uni` and `read_group_receipt_from_uni` are replaced by
|
||
`QuicSession::{open_control, accept_control, open_request, accept_request,
|
||
open_subgroup, open_fetch, accept_data}`, `send_object`, `send_receipt_bundle`
|
||
and `DataReceiver`.
|
||
- `wire::encode_varint` and `decode_varint` are -22's integer; every `u64` encodes.
|
||
- `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
|
||
by `tests/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, `Location` and `ETag`; PATCH for
|
||
trickle ICE and ICE restarts, with `If-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 is `403` with `insufficient_scope` (RFC 6750
|
||
§3.1). Every request that carries a grant also proves possession of its key
|
||
(`WAI-Room-Proof` over a single-use `WAI-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 `WAI1` envelope 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 past `max_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 (`409` with `Retry-After`
|
||
until 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.toml` holds no secret.
|
||
- **A signed record per session**, in the shape of live metering's per-hop record: the
|
||
seam a `WMH1` writer plugs into.
|
||
- **The WebRTC stack** is `str0m` 0.24.1 (MIT OR Apache-2.0), pinned, with its pure-Rust
|
||
crypto backend; the HTTP resources are `hyper` 1.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 with
|
||
`WAI_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 carries `draft-ietf-webtrans-http2-15`
|
||
capsules, one per binary message, under the subprotocol `moqt-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:
|
||
|
||
- `ObjectReceipt` gains `energy_class: Option<Vec<u8>>` (the class in WAI
|
||
energy-measurement §2.7's signed encoding). Present, the object's leaf is
|
||
`BLAKE3("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`.
|
||
- `GroupReceipt` gains `energy_class`, `energy_coverage` and
|
||
`parent_receipt_hash`. With a class, the group is signed under
|
||
`moq-jwp:group-sign\x02`, which covers the class, the coverage and the
|
||
parent link. `energy_coverage` is 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_hash`
|
||
and `compose_group_class`, the rule a labelled group's class is composed by.
|
||
- `ReceiptVerifier::verify_group` returns a `VerifiedGroup` (the root with the
|
||
group's leaf count and coordinates) instead of the bare root, and
|
||
`verify_object` and `verify_payload` take `&VerifiedGroup`. An object is
|
||
accepted only at the position its proof walks: its coordinates are the
|
||
group's, `object_id` is its leaf index, the proof has the group's depth and
|
||
its direction bits are the index's.
|
||
- `MerkleProof::verify` is replaced by `MerkleProof::verify_leaf`, which takes
|
||
the leaf in its form and the group's leaf count, and
|
||
`MerkleProof::walks_its_position`.
|
||
- `verify_group` holds 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 (a `y` coordinate ≥ p is refused), `S` must be below L
|
||
(checked by the crate as well as by `verify_strict`), `R` must not be of
|
||
small order, and the cofactorless equation must hold.
|
||
- `JWP_MOQ_EXTENSION_VERSION` is 2.
|
||
- `tests/wai_jwp_corpus.rs` runs every leaf, group and object vector of
|
||
`wai/jwp-conformance/vectors.json` through 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 the `joule-code/1` profile. This replaces v0.1's bespoke `estimated_uj ‖ sequence ‖ nonce` preimage. 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-receipt` gains a `jcr1` module (canonical CBOR + COSE_Sign1, vendored from the JCR-1 reference, no JouleClaw dependency) and `Receipt::to_jcr1_wire` / `seal` / `verify_envelope`; the bespoke `sign`/`verify` are 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.
|