[remote] max_records_per_principal (100000 by default): the clock holds the records of seq 0 up to the bound of each principal but the host. When a record past it comes, the clock takes the principal's records up to the bound, cuts it there (max_records_per_principal, by the cut path a record out of order takes) and removes it (leave reason 2); the session goes on for everyone else and verifies. The host agent reads the bound from GET /room and its gate 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. max_records stays as the safety net on the whole session and rises to 1000000, ten times the bound per principal: a host and four controllers each at its bound hold 800000. The default per principal: a two-hour session at ten input statements a second is 72000 records; a flood at the policy's 120 events a second reaches the bound in about 14 minutes. remote-desktop §10 says a service bounds per principal first, which bites first and why; the configuration docs say the same. tests/remote_room.rs: bob, a viewer, floods input statements the host refuses past a bound of 24; he is cut at seq 24 and removed by the clock and by the host, alice's input after the cut is applied, the session's total is not reached, and the sessions the clock and the host hold verify at both levels. gate::tests: a participant cut at its bound. Co-Authored-By: Claude Opus 5.5 <[email protected]>
60 KiB
WAI Extension: Remote Desktop
Status: Draft. Registers the record
wai.live.remote: one principal's screen shared into a room and controlled from it. The host publishes its screen as the room'sscreentrack, each frame an object with its WAI receipts, and signs a screen statement,WDS1: what it captures, the frame size that is the coordinate space of every input, the actions it injects, and the encoder that made the frames, named as packaging §7 (its paragraph 7.5) requires of an encoder that is not a pure function of its input, and read by no step that accepts or refuses anything. A principal whose grant carries the control power, a grant attribute, sends input statements,WDI1: pointer, key, text, touch and clipboard actions in the frame's coordinates. The host injects an input only when every rule of §5.3 holds at the input's instant, and signs an outcome,WDO1, for every input it receives: applied, naming the first frame it captured after it injected the actions, or refused, with the rule that refused it. Control passes by the room's grants alone, or, in a room that keeps a floor, by floor statements,WDF1, that request, give, release and take it back. A viewer that presents the frame an outcome names signs a latency record,WDL1: the instant of its action and the instant its platform reports the frame presented, both on its own estimate of the room clock, with the round trip that estimate rests on. Everything rides in the room's events, asmetapayloads their authors sign; a verifier recomputes every outcome from the rules and refuses a host that applied what the rules refuse. A host that states a sealing key takes key, text and clipboard actions sealed to it (§5.5): their fields travel in one HPKE ciphertext a statement, which the host alone opens, beside a commitment to each action, while pointer, wheel and touch actions stay in the clear, as the screen shows them anyway. A viewer verifies every outcome without the plaintext; a verifier given the host's opening key also opens every seal and checks every commitment (§11.4). No new transport. The reference implementation is theremote_desktopmodule ofwai-rs(featureremote_desktop); the reference host agent and its browser viewer arejoule-moq-remoteand the room page ofjoule-moq-web; the corpus islive-conformance/remote/, and its second implementation islive-conformance/remote_desktop_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.Texts read for this revision: the live family this extension builds on, as merged (live-events, live-rooms, moq-streaming-format, live-metering, live-recording, confidentiality and co-watch), packaging §7 (its paragraph 7.5) and jwp-receipts; RFC 8032, FIPS 180-4 and RFC 6381; RFC 9180 (§4, §5.1, §6.1, §7.1) and RFC 7748 §5, for sealed actions; HID Usage Tables 1.5 §10, the Keyboard/Keypad page (0x07); and the W3C UI Events specifications for pointer buttons, wheel deltas in pixels and keyboard
codevalues (uievents,uievents-code). No other outside text is pinned: the rules of this extension are WAI's own.
1. Scope
In scope: the grant attributes this extension reads (§3.1); the host's screen track and its screen statement, what the host captures and the encoder it names (§3); how the statements ride in a room's events, how they are read, and who signs each (§4); input statements, their actions, the host's rules and how it applies an input (§5), and key, text and clipboard actions sealed to the host (§5.5); outcomes (§6); handover of control, by grant change and by the floor (§7); measured input-to-photon latency (§8); session records and metering (§9); where each rule is enforced (§10); and verification at two levels (§11).
Out of scope: a transport; how a host captures its screen or injects an action, which is its
platform's; how a codec encodes a frame (§3.6); how a sink decodes and draws one; the host's
clipboard and files sent to viewers, which this revision does not define; audio from the host,
which is a room audio track like any other (live-rooms §6.1); and a recording of the session,
which the room's recording policy governs (live-rooms §3.7, live-recording).
Relationship to the core spec: wai.live.remote is a record (SPEC §5): signed
records a WAI tool emits and verifies. It names no payload format and is not dispatchable. The
frames are payloads of capabilities SPEC §5 already registers (wai.video.avc,
wai.video.hevc, wai.video.av1, wai.video.vp9), one access unit an object.
Relationship to live-events: this extension changes nothing in it. Its
statements are payloads of the meta kind (§4.2 there), which live-events leaves to
applications, as co-watch's are, so every statement is signed by its author,
chained, stamped with the room clock and rate-bounded by its author's policy (live-events §5,
§6). An outcome that names a frame anchors it (live-events §3.1).
Relationship to live-rooms: the descriptor's and the grant's layouts are
unchanged. The screen is the room's screen media kind (live-rooms §6.1, publish bit 2), and
its tracks are named as live-rooms §6.1 names every track. The host mark and the control power
are grant attributes (§3.1), which live-rooms §4.4 signs with the grant and leaves to the
extensions that read them: a new power bit would be refused by every room verifier of the
current revision (live-rooms §3.4: no power above bit 4). The direct power (live-rooms §3.3,
bit 1) is the power to give and take back the floor (§7.2).
Relationship to packaging §7 (its paragraph 7.5): a frame is made by an encoder that is not a pure function of its input, so the host names that encoder and its version. The name is a statement about where the bytes came from; the bytes are bound by their digests in their receipts, and nothing recomputes them (§3.6).
2. Terms
- Room, room clock, principal, event, chain, grant, grant in effect, room key, merged order, present: as in live-events §2 and live-rooms §2, §9.2 and §9.3. A room key is the room authority's key or the clock's.
- Statement: the payload of a
metaevent whose first four bytes are one of the magics of §4.1. It is named by the digest of its event (live-events §3.2). - Host: a principal whose grant in effect carries
remote_hostwith the valuetrue(§3.1). Its screen is what it captures and publishes (§3.5). - Controller: a principal whose grant in effect carries the control power (§3.1).
- Frame: one object of the host's screen track: one access unit of the capability its
screen statement names, in a
WAI1envelope (moq-streaming-format §3), with its receipts (§3.2). - Action: one pointer, key, text, touch or clipboard input (§5.2). Its class is the bit of the control power that allows it.
- Sealing key: an X25519 public key (RFC 7748 §5) a host states in its screen statement (§3.3). Its opening key is the matching private key, which the host holds.
- Sealed action: a key, text or clipboard action whose fields are sealed to the host's sealing key (§5.2, §5.5). Its opening is a salt and its fields; its commitment is the digest that binds them (§5.5). A statement's seal holds the openings of its sealed actions.
- Estimate: a principal's estimate of the room clock, on which it stamps its events'
t. - A short field is a u8 length and that many bytes; a token is 1 or more bytes of
!to~(0x21–0x7E), as in live-rooms §2.
Integers are little-endian; a signed integer is two's complement. A digest is SHA-256 (FIPS
180-4), 32 bytes. Ed25519 is RFC 8032's, verified strictly (SPEC §7
Signatures): a key that fails step 1 or 2 is bad_key, and a signature that fails a later
step is bad_signature. µs is microseconds of room clock.
3. The host and its screen
3.1 The attributes
This extension reads two grant attributes (live-rooms §4.4):
| key | value | meaning |
|---|---|---|
remote_host |
true or false |
whether the principal may host: sign screen statements and outcomes (§4.3) and receive input |
remote_control |
a decimal integer from 1 to 15 | the control power: the classes of action the principal may send. Bit 0 pointer (types 1 to 3 of §5.2), bit 1 keys (types 4 and 5), bit 2 touch (type 6), bit 3 clipboard (type 7) |
A decimal integer here is 1 or 2 of the digits 0–9, the first not 0. A grant whose
remote_host is neither true nor false, or whose remote_control is not of that form or is
above 15, is refused rd_attr_malformed. A grant without remote_host does not host, and one
without remote_control carries no control power. A host's grant MUST also let it publish
the screen media kind (live-rooms §3.3, publish bit 2); one that does not cannot sign a screen
statement (§4.3).
The authority states who hosts and who controls in the grants it signs, and changes either by
exchanging a grant (live-rooms §4.6, §7.1 here). The value of remote_control is the classes:
1 pointer alone, 3 pointer and keys, 15 everything.
3.2 The screen track
The host publishes its screen as its own track of the screen kind (live-rooms §6.1): the Track
Namespace of the room id, its key and screen, the Track Name its layer, 0 for a single
layer. The track's catalog entry declares the capability its frames carry (moq-streaming-format
§4, wai_capabilities), its codecs value (codec, RFC 6381), its size, wai_receipts true and
its receipts track (moq-streaming-format §4.1), and a sink verifies each frame against its
receipt before it decodes it, as it verifies every object of a room. Each frame is one group of
one object, so a group receipt covers one frame; a frame that is an intra-coded picture
(an IDR access unit, or an AV1 or VP9 key frame) carries key true in its manifest's target.
The track digest of the screen is the anchor track of its layer 0 (live-events §3.1):
SHA-256 over the namespace's rendering <room>-<host>-screen (the room id and the host's key
as 64 lowercase hexadecimal digits each), one zero byte, and 0.
A host SHOULD send an intra-coded frame at least every two seconds, and at once when a principal joins, so that a viewer that joins decodes within that time.
3.3 The screen statement
| field | size | meaning |
|---|---|---|
magic, version |
5 | WDS1, 1 |
prev |
32 | the digest of the event of the host's screen statement this one replaces; 32 zero bytes for its first |
state |
1 | 1 live: the host captures and accepts input; 2 paused: it captures nothing and accepts no input (§3.5) |
track |
32 | the track digest of the host's screen (§3.2) |
width, height |
2 each | u16: the frames' size in pixels, which is the coordinate space of every action (§5.2); each above 0 |
fps |
2 | u16: the most frames a second it captures, above 0 |
scope |
1 | what it captures: 1 a whole display, 2 one window, 3 a region of a display |
region |
8 | for scope 3, the region on the display, in the display's pixels: x, y, w, h (u16 each), w and h above 0; for scope 1 and 2, eight zero bytes |
redactions |
1 + 8 × n | a u8 count, at most 16, then each rectangle the host blanks in every frame before it encodes it: x, y, w, h (u16 each) in frame pixels, w and h above 0, within the frame |
pointer |
1 | 1 the frames show the host's pointer, 0 they do not |
accepts |
1 | the action types the host injects: bit k − 1 for type k of §5.2; no bit above bit 6 |
clip_max |
2 | u16: the most bytes of text one clipboard action may carry; above 0 exactly when accepts holds type 7 |
max_age_ms |
4 | u32: the oldest an action may be when the host decides on its input (§5.3 step 7), above 0 |
floor |
1 | 1 open: every controller may send input; 2 floor: only the controller holding the floor (§7) |
capability |
short | the frames' payload capability (SPEC §5), a token |
codec |
short | the frames' codecs value (RFC 6381) as the track's catalog entry states it, a token |
encoder |
short | the encoder's name and version (§3.6): a token holding exactly one @, with a byte before it and one after |
api |
1 | the encoder's interface, by role (§3.6): 1 to 5 |
hardware |
1 | where the encoder runs (§3.6): 0 general-purpose processors, 1 a fixed-function media engine, 2 not stated |
seal |
0 or 32 | the host's sealing key (§5.5): an X25519 public key (RFC 7748 §5); absent when the host takes key, text and clipboard actions in the clear. The statement ends after hardware, or after these 32 bytes |
A screen statement is a meta payload of at most 5 + 32 + 1 + 32 + 2 × 3 + 1 + 8 + 1 + 16 × 8
- 1 + 1 + 2 + 4 + 1 + 3 × 256 + 2 + 32 bytes, within the default
max_payloadof live-events §5.
3.4 Rules and the chain
A screen statement whose fields break §3.3's table is rd_statement_malformed. Then:
- its
trackis the track digest of its author's screen (§3.2): otherwisescreen_wrong_track; - its
previs the digest of the event of its author's screen statement before it in the merged order, or 32 zero bytes when it has none: otherwisescreen_unlinked.
The host's screen in effect at a point of the merged order is its last screen statement the room accepted before that point. A statement is replaced, never amended: a new size, a new scope, a pause, a resume or a new encoder is a new statement, which takes effect at its place in the merged order.
3.5 What is captured
What a host captures is the host's choice, and the screen statement states it: whether the
frames hold a whole display, one window or a region of a display (scope, region); the
rectangles it blanks before it encodes (redactions); whether its pointer is drawn
(pointer); which actions it injects (accepts) and how much clipboard text it takes
(clip_max); whether it takes key, text and clipboard actions sealed (seal, §5.5); and whether
it captures at all (state). A host MUST capture nothing outside
what its screen in effect states, MUST blank every redaction in every frame before the
frame reaches its encoder, and MUST NOT publish a frame while its screen in effect is
paused. A sink SHOULD show its viewer the scope, the redactions and the state.
A host that stops sharing for a moment pauses (a statement with state 2) rather than leaving;
one that resumes signs a live statement, and its next frame is intra-coded.
3.6 The encoder
encoder names the encoder that made the frames, and its version, as packaging §7 (paragraph 7.5) has a
floor name an encoder that is not a pure function of its input: the encoder's name, @, and
its version as the encoder reports it. api names the interface the host drove it through, by
role:
api |
the interface |
|---|---|
| 1 | the operating system's media framework |
| 2 | a video acceleration interface whose driver drives a GPU's encode engine |
| 3 | the encode interface a GPU driver exports from its own library |
| 4 | a software encoder library |
| 5 | an encoder that is a pure function of its input |
hardware states where it ran, as the interface reports it: 0 general-purpose processors, 1 a
fixed-function media engine, 2 not stated. A host that cannot tell states 2, never 1.
The three fields describe how the frames were made, and nothing else. No step of this extension reads them to accept or refuse a frame, an input, an outcome or a latency record; no digest a verifier recomputes covers them, beyond the statement's own event; and no verifier re-encodes a frame. Each frame is bound by the content hash its receipt states (jwp-receipts), and a viewer that holds a frame and its receipt holds the bytes the host published, whatever made them. A host that changes its encoder signs a new screen statement.
4. Statements
4.1 Carriage
| magic | statement | section | who signs it |
|---|---|---|---|
WDS1 |
screen statement | §3.3 | a host |
WDI1 |
input statement | §5.1 | any principal that is not a room key |
WDO1 |
outcome | §6.1 | the host its input names |
WDF1 |
floor statement | §7.2 | as §7.2 states for its action |
WDL1 |
latency record | §8.1 | any principal that is not a room key |
Each statement is the payload of a meta event: its magic, a version byte of 1, and the
fields its section lists. A meta payload whose first four bytes are none of these magics is
not this extension's, and a sink that does not implement it shows none of them: they are timed
metadata for its application (live-events §4.2). Every statement is bounded by its author's
policy (live-events §5, max_payload): an authority that gives a principal the clipboard class
gives its grant a max_payload that holds the clipboard text it allows.
4.2 Reading
A statement that ends before a field, that holds bytes after its last field, whose version is
not 1, or whose field holds a value its section does not allow, is rd_statement_malformed.
4.3 Who signs
A screen statement whose author's grant in effect at its t does not carry remote_host with
the value true or does not let it publish the screen kind, an outcome whose author is not
the host of the input it names, and an input statement or a latency record by a room key, are
rd_not_permitted. A floor statement's author is checked by §7.2. A grant's powers and
attributes are those of the author's grant in effect at its event's t (live-rooms §9.2 step
4). Since a statement is a meta event, the room verifier has already held it to its author's
grant: a principal whose policy does not permit meta cannot send one (live-rooms §9.2 step 4,
kind_not_permitted).
5. Input
5.1 The input statement
| field | size | meaning |
|---|---|---|
magic, version |
5 | WDI1, 1 |
host |
32 | the key of the host the actions are for |
screen |
32 | the digest of the event of the host's screen statement whose frame the actions' coordinates are in |
count |
1 | the number of actions, 1 to 32 |
actions |
… | each action: dt_us (u32), type (u8), and the fields of its type (§5.2) |
seal |
… | present exactly when an action is sealed (type 8 of §5.2): enc, 32 bytes, then the ciphertext, to the statement's end (§5.5) |
dt_us is how long before its event's t the action happened, so the action's instant is
t − dt_us; it is at most t. The actions are in the order they happened: each one's dt_us
is at most the one's before it. A statement that breaks this, or whose count is outside 1 to
32, is rd_statement_malformed. So is one with a sealed action whose ciphertext is not exactly
16 bytes longer than the openings of its sealed actions (§5.5), and one with no sealed action
that holds any byte after its last action. A controller batches the actions of a short interval (a pointer
moving over one display refresh, a key and its release) into one statement, so that the rate
its grant's policy bounds is a rate of statements, not of actions.
5.2 Actions
| type | name | class | fields | meaning |
|---|---|---|---|---|
| 1 | move |
pointer | x, y (u16 each) |
the pointer moved to (x, y) |
| 2 | button |
pointer | button (u8), down (u8), x, y (u16 each) |
a button went down (down 1) or up (0) at (x, y); button 0 primary, 1 auxiliary, 2 secondary, 3 fourth, 4 fifth, as UI Events numbers a mouse event's button |
| 3 | wheel |
pointer | dx, dy (i16 each), x, y (u16 each) |
the wheel turned by dx, dy pixels with the pointer at (x, y), as UI Events states a wheel event's deltas in pixels; not both 0 |
| 4 | key |
keys | usage (u16), down (u8), modifiers (u8) |
a key went down (1) or up (0): usage its usage ID on the HID Keyboard/Keypad page (0x07), 0x04 to 0xE7; modifiers the modifiers held: bit 0 shift, bit 1 control, bit 2 alt, bit 3 meta, no other bit |
| 5 | text |
keys | text: a u8 length, 1 to 64, and that many bytes of UTF-8 |
text composed on the controller's side, as an input method composes it, typed as it is |
| 6 | touch |
touch | id (u8), phase (u8), x, y (u16 each) |
touch point id began (phase 1), moved (2), ended (3) or was cancelled (4) at (x, y) |
| 7 | clipboard |
clipboard | text: a u16 length and that many bytes of UTF-8 |
text the host puts on its clipboard |
| 8 | sealed |
the class of of |
of (u8), length (u16), commit (32 bytes) |
a key, text or clipboard action sealed to the host's sealing key (§5.5): of the type it seals, 4, 5 or 7; length the clipboard text's length in bytes for 7, and 0 for 4 and 5; commit its commitment. Its fields are in the statement's seal |
Coordinates are in the frame of the screen statement the input names, from (0, 0) at its top
left, x below width and y below height; the host maps them to what it captures (its
scope, its region and any scaling). UTF-8 is RFC 3629's: no overlong form, no surrogate, nothing
above U+10FFFF. A field outside its row is rd_statement_malformed; a coordinate outside the
frame and clipboard text longer than clip_max are not malformed but out of bounds (§5.3 step
6), since they depend on the screen. A controller in a page maps a keyboard event's code
(uievents-code) to its HID usage. A sealed action is of the type it seals wherever a rule reads
an action's type (accepts, §3.3 and §5.3 step 5), and of that type's class; accepts has no
bit for type 8.
5.3 The host's rules
A host decides on an input statement when it receives it. With t the statement's event's
t, P its author and n the instant the host decides (the t of the outcome it signs, §6),
the host refuses it at the first of these that holds, with the reason and its number:
- its
hostis not the host's key, or itsscreenis not the digest of the host's screen in effect at the input's place in the merged order:input_wrong_screen(1); - P's grant in effect at t does not carry the control power with the class of every
action:
input_not_permitted(2); - the host's screen in effect keeps a floor (
floor2) and P does not hold it at the input's place in the merged order (§7.3):input_no_floor(3); - the screen in effect is paused:
input_screen_paused(4); - an action's type is not among the screen's
accepts:input_unsupported(5); - an action's coordinate is outside the frame, or a clipboard action's text (a sealed clipboard
action's
length) is longer than the screen'sclip_maxbytes:input_out_of_bounds(6); - n less the instant of its first action is more than the screen's
max_age_ms× 1000:input_stale(7); - the screen in effect states a sealing key, and an action is a key, text or clipboard action in
the clear (type 4, 5 or 7):
input_unsealed(8); - it carries a seal, and the screen in effect states no sealing key, or the seal does not open
under the host's opening key, does not read, or does not keep its commitments (§5.5):
input_bad_seal(9).
Otherwise it applies the input (§5.4). It applies all of a statement's actions or none of them. A host MUST NOT apply an input these rules refuse, and MUST sign one outcome for every input statement addressed to it that it receives (§6).
The rules are made at the input's place in the merged order: the grant in effect, the screen in effect and the floor are those every party holding the room computes there, so a verifier recomputes the host's decision (§11.2). Rule 9 reads what a seal holds, which the opening key alone opens: a verification at the commitment level makes it only where the screen states no sealing key, and one at the opened level makes it in full (§11.4). A host decides as soon as it holds the input and every record before it that the rules read: its own screen statements, the clock's lifecycle, and the floor statements. An input that crosses a grant change, a new screen or a floor change on the wire is decided by its place, and refused if the rules then refuse it; its author sends it again under what is in effect.
5.4 Applying
A host applies an input by injecting its actions into what it captures, in order, each sealed action as the action it seals (§5.5), each at its coordinates mapped from the frame. Text and clipboard text are taken as given; the host injects no key or button state the actions do not state, and SHOULD release, when a controller loses control (§7), every key and button that controller left down.
5.5 Sealed actions
Keys typed and text composed or pasted can be passwords. A host that states a sealing key in its
screen statement (seal, §3.3) takes key, text and clipboard actions sealed to it, so that no
party but the host reads them: not a relay, not the room service, not a viewer. Pointer, wheel
and touch actions stay in the clear: the host's screen shows where the pointer goes and what it
does, and the screen is the room's. A sealed action keeps in the clear what rules 1 to 8 of §5.3
read: the type it seals, which gives its class (rule 2) and is held to accepts (rule 5); its
dt_us (rule 7, and the latency records of §8); and a clipboard text's length (rule 6). A
text's length is not in the clear: its opening pads it to 64 bytes.
The opening of a sealed action is 16 bytes of salt, then the fields of the type it
seals: for a key, its usage (u16), down and modifiers (u8 each), as §5.2 lays them out;
for a text, its length (u8, 1 to 64), then 64 bytes, the text and zero bytes after it; for a
clipboard text, its length bytes of UTF-8, with no length before them. A sender draws each salt
afresh from a cryptographically secure random source.
The commitment of a sealed action at index k among its statement's actions, from 0, is
SHA-256 over the 18 bytes wai:remote-commit (ASCII) and 0x01, the room id, the author's key,
the statement's host and screen, k (u8), the action's dt_us (u32), of (u8) and its
opening. It binds the input (its fields, and the room, the host and the screen statement it is
sent to), its order (its index among the statement's actions and its instant) and its author.
The salt keeps it from being guessed: a key's fields take a few thousand values, and a commitment
over them alone would name the key to anyone who tried each.
The seal is HPKE's single-shot SealBase (RFC 9180 §6.1), base mode, with DHKEM(X25519, HKDF-SHA256), HKDF-SHA256 and AES-128-GCM (RFC 9180 §7.1: KEM 0x0020, KDF 0x0001, AEAD 0x0001), and:
pkR: the sealing key of the screen statement the input names (screen);info: the 16 byteswai:remote-seal(ASCII) and 0x01, the room id and the host's key;aad: the author's key, then the statement frommagicthrough its last action;pt: the openings of its sealed actions, in their order among its actions.
The statement holds the seal after its last action: enc, 32 bytes, then the ciphertext, which
is 16 bytes longer than pt. A sender draws a new ephemeral key for every statement (RFC 9180
§4.1: GenerateKeyPair, or DeriveKeyPair of fresh random bytes).
Opening. The host opens a seal with OpenBase under its opening key, the same info and
aad, and reads each sealed action's opening from what it opens to, in order, each as long as
its of and length make it. Rule 9 of §5.3 holds, for the first of these reasons, which a
verification at the opened level states (§11.4; §12's fourth table):
- the screen statement the input names states no sealing key:
seal_no_key; - OpenBase fails: the seal was made to another key, by another author or in another room, or
was altered:
seal_not_opened; - what it opens to does not read as the openings: a key's fields outside §5.2's row, a text's
length outside 1 to 64, a padding byte that is not zero, text that is not UTF-8, or bytes
left over:
seal_unreadable; - a sealed action's commitment is not the commitment of its opening:
seal_commitment.
The aad binds the seal to its statement and its author: a principal that copies another's
sealed input into a statement of its own sends a seal that does not open.
A controller MUST seal every key, text and clipboard action it sends to a host whose screen in effect states a sealing key, to that key, and MUST NOT send one in the clear: the host refuses it (rule 8). A host MUST open a sealed input before it injects it, and inject only what rule 9 accepts. A host SHOULD make a sealing key for each session and discard its opening key when the session ends, so that a later compromise of its own keys opens none of the session's inputs; it MAY give the opening key to a verifier it wants to check the session at the opened level (§11.4).
6. Outcomes
6.1 Layout
| field | size | meaning |
|---|---|---|
magic, version |
5 | WDO1, 1 |
input |
32 | the digest of the input statement's event |
outcome |
1 | 1 applied, 2 refused |
reason |
1 | 0 when applied; when refused, the number of the rule of §5.3 that refused it, 1 to 9 |
applied_at |
8 | u64: when applied, the instant on the host's estimate it injected the last action; 0 when refused |
captured_at |
8 | u64: when applied, the instant it captured the frame the event's anchor names; 0 when refused |
content |
32 | when applied, the content hash of that frame's object as its receipt states it (jwp-receipts); 32 zero bytes when refused |
rtt_us |
4 | u32: the round trip of the clock measurement the host's estimate rests on (§8.4); 2^32 − 1 when it states none |
An applied outcome's event carries an anchor (live-events §3.1): the screen track, the group and the object of the first frame the host captured after it injected the last action, which is the first frame that can show what the actions did. A host signs it once that frame is published. A refused outcome's event carries no anchor, and the host signs it at once.
6.2 Rules
An outcome is checked, in this order:
- its
inputis the digest of an input statement the room accepted: otherwiseoutcome_unknown_input; - its author is that input's
host(§4.3): otherwiserd_not_permitted; - no outcome before it in the merged order names the same input: otherwise
outcome_duplicate; - its fields agree with its
outcomeand with one another: an applied outcome hasreason0, an anchor whosetrackis the track digest of the host's screen and whoseobjectis 0, and acontentnot all zero, and the input'st≤applied_at≤captured_at≤ its ownt; a refused one has areasonfrom 1 to 9, no anchor,applied_at,captured_atandcontentzero, and atat or after the input's. Otherwiseoutcome_invalid; - the rules of §5.3, made at the input's place with n the outcome's
t, give what it states: applied when none refuses, and refused with the reason of the first that does. At the commitment level (§11.4), rule 9 is made only for an input whose screen states no sealing key: a sealed input to a screen that states one, which rules 1 to 8 accept, takes an outcome that states applied or refused with reason 9. Otherwiseoutcome_mismatch.
An input statement with no outcome is unanswered. A verifier states it; it is not a refusal, since a host may never have received it.
7. Handover of control
7.1 Two ways
Control passes in one of two ways, as the host's screen in effect states.
- By grant (
floor1, open). Every controller may send input, and the classes it may send are its grant's. Control is handed over by exchanging grants (live-rooms §4.6): the authority exchanges the grant of the principal giving up control for one withoutremote_control, and that of the principal taking it for one with it, each effective at a tick of the room clock. From that tick the rules of §5.3 refuse the first and accept the second. - By the floor (
floor2). Controllers may send input only while they hold the floor, which floor statements pass (§7.2). The grant still bounds what the holder may send: the floor selects one controller, and its grant's classes are what it may do.
7.2 The floor statement
| field | size | meaning |
|---|---|---|
magic, version |
5 | WDF1, 1 |
host |
32 | the host whose floor it is |
action |
1 | 1 request, 2 give, 3 release, 4 take |
to |
32 | for give, the principal given the floor; otherwise 32 zero bytes |
A floor statement whose action is outside 1 to 4, or whose to is not zero exactly when its
action is give, is rd_statement_malformed. Its author, with t its event's t:
- request: a controller at t; it changes nothing, and tells the host and the holder that the author asks for the floor;
- give: the host, a room key, a principal whose grant in effect holds the direct power, or
the floor's holder;
tois a present controller at t, and not the host: otherwisefloor_not_eligible; - release: the floor's holder;
- take: the host, a room key or a principal whose grant in effect holds the direct power.
An author who may not send its action is floor_not_permitted, but a release by a principal
that does not hold the floor is floor_not_holder. A floor statement whose host is not a
principal that has signed a screen statement before it in the merged order is
floor_not_permitted.
A floor statement these rules refuse at its place is not a refusal of the room. It is recorded: it stays in its author's chain, held to the room's rules as every event is (live-rooms §9.2), and it changes nothing: the floor in effect after it is the floor in effect before it. A verifier states it with its reason (§11.3) and computes the floor without it, and a host gives it no effect. So two floor statements that cross on the wire, such as a holder's release that takes its place after the host has taken the floor back, leave the floor where the statements these rules accept put it, and the room verifies.
7.3 The floor in effect
A host's floor starts held by no one. In the merged order, a give that §7.2 accepts passes it to
to, and a release or a take that §7.2 accepts leaves it held by no one; a floor statement §7.2
refuses changes nothing. The holder also loses it, and it is held by no one,
at the leave of the holder, at a grant_exchange that makes the holder's grant in effect one
without the control power, and at the leave of the host. The floor in effect at a point
of the merged order is the floor after every statement and lifecycle event before that point.
A floor is kept whatever the host's screen in effect states: a host that moves from open to floor and back keeps its floor's holder, and its rules (§5.3 step 3) read the floor only while its screen keeps one.
8. Input-to-photon latency
8.1 The latency record
| field | size | meaning |
|---|---|---|
magic, version |
5 | WDL1, 1 |
room |
32 | the room id |
viewer |
32 | the key that signs it, its author's |
host |
32 | the host |
input |
32 | the digest of the event of the viewer's input statement |
action |
1 | the index in that statement of the action measured, from 0 |
outcome |
32 | the digest of the event of the host's outcome of that input |
track |
32 | the frame's track, as the outcome's anchor names it |
group, object |
8 each | u64: the frame's group and object |
content |
32 | the frame's content hash, as its receipt states it |
input_at |
8 | u64: the action's instant (§5.1) |
presented_at |
8 | u64: the instant, on the viewer's estimate, the frame was presented |
source |
1 | what reported presented_at: 1 the platform's frame-timing interface, 2 a light sensor watching the display, 3 no display: the instant the viewer held the frame verified against its receipt |
latency_us |
4 | u32: presented_at − input_at |
rtt_us |
4 | u32: the round trip of the clock measurement the viewer's estimate rests on (§8.4); 2^32 − 1 when it states none |
sig |
64 | Ed25519 by viewer over the 19 bytes wai:remote-latency (ASCII) and 0x01, followed by the record from magic through rtt_us |
A record whose source is outside 1 to 3 is rd_statement_malformed. Its digest, where a
party names it apart from its event, is SHA-256 over the record from magic through rtt_us.
It carries its own signature, beside its event's, so that it stands on its own: a viewer that
shows a third party the latency it measured presents its records and its grant, with no other
event of the room.
8.2 Rules
A latency record is checked, in this order:
- its
roomis the room id: otherwisewrong_room; - its
vieweris its event's principal: otherwiselatency_wrong_key; - its signature verifies under
viewer(§8.1):bad_key,bad_signature; - its
inputis the digest of an input statement the room accepted, byviewer, whosehostis the record'shost, with an action at indexaction: otherwiselatency_unknown_input; - its
outcomeis the digest of the outcome of that input, which the room accepted, and which states it applied; itstrack,groupandobjectare that outcome's anchor's, and itscontentthat outcome'scontent; itsinput_atis the input'stless the action'sdt_us: otherwiselatency_mismatch; - its
presented_atis at most its event'st: a record states what was presented, never what will be. Otherwiselatency_ahead; - its
presented_atis at least itsinput_at, and itslatency_usis the difference: otherwiselatency_mismatch.
8.3 What it measures
A latency record measures from an action to the presentation of the first frame that can show it. Both instants are on the viewer's own estimate, so the room clock's offset cancels from the difference; what remains is the estimate's drift over the interval, which the viewer keeps small by measuring its clock against a room key's as co-watch §7.3 has a player do. The first frame that can show an input is the first the host captured after injecting it (§6.1): the host's operating system and the application under the pointer may take longer to draw the effect, so the record is a lower bound on the time until the effect is seen, and is exact when they draw it in time for that frame.
A viewer SHOULD state as presented_at the instant its platform's frame-timing interface
reports for the frame it drew: the presentation time of a video frame callback, or the start of
the display refresh after the one in which the frame was drawn, never the instant it was
decoded. A light sensor (source 2) watching the display measures the photon itself, and is the
record a laboratory makes. A viewer with no display, such as an agent that reads the screen or a
test harness, states source 3 and the instant it held the frame verified against its receipt:
no photon is involved, and the record measures the path to the viewer alone.
A viewer measures the actions it chooses: every input, or one in so many. A record is a sample, and a verifier states each one; it states no percentile over a room, since which actions were measured is the viewer's choice.
8.4 The legs and their uncertainty
From a latency record and the outcome it names, a verifier states three legs:
- up: the outcome's
applied_atless the record'sinput_at: from the action to its injection; - host: the outcome's
captured_atless itsapplied_at: from the injection to the capture of the first frame after it; - down: the record's
presented_atless the outcome'scaptured_at: from the capture to the presentation.
They sum to latency_us. The host leg is on the host's estimate alone and the total on the
viewer's alone. The up and down legs cross from one estimate to the other, and each is stated
with an uncertainty of half the sum of the two rtt_us, rounded down: the most a clock
measured over a round trip of that length can be off, when the path's two directions take
equal time. When either estimate states no round trip, the uncertainty is not stated, and the
crossing legs are stated without it. A leg that crosses estimates may come out negative by up to
its uncertainty; it is stated as it is.
9. Session records and metering
A remote desktop session is the room's session, and its records are the room's:
- the session: from the host's
jointo itsleave, the clock's lifecycle (live-rooms §7.2), and in it every principal's presence, its grant in effect, and each grant exchange that handed control over; - what happened: the host's screen statements, every input, its outcome and the floor, in the chains of their authors, which a verifier replays (§11) and states;
- what it cost: the host's screen track is a track of the room, and every hop that carried it signs its per-hop session record, as it does for every track; the room-session receipt totals them, its publisher-seconds and subscriber-seconds included (live-metering §3, §4). The input statements, the outcomes and the latency records are events of their authors, which live-metering counts as events sent and delivered. An action costs its share of an event's bytes; a frame its object's;
- what was kept: a recording of the session, when the room's recording policy records it (live-rooms §3.7), is a recording of the room (live-recording); its manifest holds the screen track and the events, so a recording of a session verifies as the session does.
This extension adds no record for any of these.
10. Where each rule is enforced
- The host applies only what §5.3 accepts, signs an outcome for every input it receives (§6), captures only what its screen statement states (§3.5), and names its encoder (§3.6). It is the rules' enforcement point: it is the one party whose machine an action reaches, and the one that holds the opening key of its sealing key (§5.5).
- A relay or a room service that carries the room's events carries each principal's events in its chain order, as it carries every event, and a sealed action as it is: it holds no opening key.
- A room service that keeps a session's records (§9, §11.1) SHOULD bound how many it
keeps, and SHOULD bound them per principal first. A bound per principal falls on the
principal that reaches it: the service keeps none of its records past the bound, cuts its chain
there and removes it (live-rooms §7.2,
leavereason 2), and the session goes on for everyone else; a host that knows the bound takes none of that principal's records past it either, so the host and the service hold the same records and the session verifies. The host itself is held to no bound of its own, since its records answer the others' inputs and grow with the room. A bound on the whole session is a safety net, set well above the bound per principal times a room's usual number of active principals, so that the bound per principal is the one any single principal meets first: when the session's bound is reached the service ends the session, every principal'sleavesigned, the host's last. Either way the records kept are whole chains, and a verifier accepts them. It MAY decline to forward an input statement to the host whose author carries no control power at itst(§5.3 step 2), which it can tell from the grants it holds; it then never reaches the host, and stays unanswered. - The room authority signs the grants that mark the host and give the control power (§3.1), and exchanges them to hand control over (§7.1).
- Controllers send actions in the coordinates of the screen in effect, and send again what was refused once what refused it has changed.
- Viewers verify each frame against its receipt before they decode it (§3.2), and state latency as §8.3 has them.
After the fact, a verifier (§11) checks every one of these against the room: an input a host applied that §5.3 refuses, an outcome that states another reason than the rule that refuses, and a latency record that names another frame than the host's outcome, are each found out. A floor statement by a principal that may not send it is stated, and passes nothing (§7.2).
11. Verification
11.1 Inputs
A remote desktop verifier is given what a room verifier is given (live-rooms §9.1: a descriptor, grants, a key-log head or none, and the chains), and at the opened level the opening keys of the hosts' sealing keys (§11.4). Every statement this extension defines is in the chains.
11.2 Steps
It runs these steps and stops at the first refusal, which it reports with the reason and where:
- The room (live-rooms §9): a refusal, at
roomwith the room verifier's own place. A gap is not a refusal; the steps go on, and the checks that look up another statement (§6.2 steps 1 and 5, §8.2 steps 4 and 5) are not made for a statement whose lookup fails: the statement looked for may be in the gap. - The grants' attributes (§3.1), in the grants' order: at the grant's index.
- The statements, in the merged order: each
metaevent the room accepted whose payload begins with a magic of §4.1: §4.2, then §4.3, then the rules of its section in the order the section gives, with the screen in effect, the floor in effect and each grant in effect at its place. A floor statement §7.2 refuses is not a refusal: it is stated (§11.3) and changes nothing. The checks of an outcome and of a latency record that look up another statement (§6.2, §8.2) are made after every statement has been read, since the statement they name may follow them in the merged order. At the record. - The outcome (§11.3).
11.3 Outcomes and what a verification states
- refuse: the reason and where (§11.2).
- gap: no refusal, and the room has a gap: the gaps, under the code
delivery_gap, and what accept states, computed from what the verifier holds, with each outcome and latency record whose lookup failed statedunchecked. - accept: no refusal and no gap.
With accept or gap, the verifier states:
- each screen statement, in the merged order: its digest, host,
state, size,scope,floor,accepts,capability,codec,encoder,apiandhardware; - each input statement, in the merged order: its digest, author, host, the classes of its
actions, the number of its sealed actions when it has any, and its outcome:
applied(with the frame its outcome names),refusedwith the reason, orunanswered; and at the opened level, for a sealed input, what its seal gave (§11.4); - the floor: each change of each host's floor in effect, in the merged order, with the instant and the holder, or none;
- each floor statement §7.2 refuses, in the merged order: its digest, author, host,
t, action and reason (§12's third table); - each latency record, in the merged order: its digest, viewer,
source,latency_us, and its legs (§8.4) with their uncertainty, orunchecked; - for each host, the count of inputs applied, refused for each reason of rules 1 to 7, and for rules 8 and 9 where that count is not zero, and unanswered.
11.4 Two levels
A remote desktop verification is made at one of two levels.
- The commitment level is what every party that holds the room verifies, with no key beyond the room's. It makes every step of §11.2 and reads no seal's plaintext: it makes rule 9 of §5.3 only where the screen statement a sealed input names states no sealing key (§6.2 step 5). It checks every statement's form, every outcome against rules 1 to 8, the floor, every latency record and every chain. A host that applies a key, text or clipboard action sent in the clear to a screen with a sealing key, or a sealed input to a screen with none, is found out at this level; one that applies a seal that does not open, or refuses one that does, is not.
- The opened level is a verification also given an opening key for each sealing key the
room's screen statements state. For each sealed input it opens the seal with the opening key of
the sealing key of the screen statement the input names, makes rule 9 in full, and requires
every outcome to state what all nine rules give. It states what each sealed input's seal gave:
opened, with the input statement as it reads opened (each sealed action replaced by the action it seals, and no seal); the reason rule 9 holds (§12's fourth table); orunopened, when it was given no opening key for that sealing key, and then that input is checked as at the commitment level.
The opened level is optional: the opening key is the host's to give, and a room verifies without it. A room the opened level accepts, the commitment level accepts.
12. Reasons
The codes this extension adds that refuse a room, each with where it is raised:
| reason | raised at |
|---|---|
rd_attr_malformed |
§3.1, §11.2 step 2 |
rd_statement_malformed |
§3.3, §4.2, §5.1, §5.2, §7.2, §8.1 |
rd_not_permitted |
§4.3, §6.2 step 2 |
screen_wrong_track |
§3.4 step 1 |
screen_unlinked |
§3.4 step 2 |
outcome_unknown_input |
§6.2 step 1 |
outcome_duplicate |
§6.2 step 3 |
outcome_invalid |
§6.2 step 4 |
outcome_mismatch |
§6.2 step 5 |
latency_wrong_key |
§8.2 step 2 |
latency_unknown_input |
§8.2 step 4 |
latency_mismatch |
§8.2 steps 5 and 7 |
latency_ahead |
§8.2 step 6 |
The reasons a host states in a refused outcome, each with its number and the rule of §5.3 it names; a verifier refuses none of them, and states each input refused with them:
| number | reason | rule |
|---|---|---|
| 1 | input_wrong_screen |
§5.3 step 1 |
| 2 | input_not_permitted |
§5.3 step 2 |
| 3 | input_no_floor |
§5.3 step 3 |
| 4 | input_screen_paused |
§5.3 step 4 |
| 5 | input_unsupported |
§5.3 step 5 |
| 6 | input_out_of_bounds |
§5.3 step 6 |
| 7 | input_stale |
§5.3 step 7 |
| 8 | input_unsealed |
§5.3 step 8 |
| 9 | input_bad_seal |
§5.3 step 9 |
The reasons a floor statement is refused at its place (§7.2); a verifier refuses none of them, states each floor statement refused with them, and computes the floor without it (§7.3):
| reason | raised at |
|---|---|
floor_not_permitted |
§7.2 |
floor_not_eligible |
§7.2 |
floor_not_holder |
§7.2 |
The reasons rule 9 of §5.3 holds for a seal (§5.5), which a verification at the opened level states of each sealed input it does not open (§11.4); a verifier refuses none of them:
| reason | raised at |
|---|---|
seal_no_key |
§5.5 |
seal_not_opened |
§5.5 |
seal_unreadable |
§5.5 |
seal_commitment |
§5.5 |
A remote desktop verifier also raises every code a room verifier raises (live-rooms §11 and the
live-events codes it lists), at the room; wrong_room, bad_key and bad_signature for a
latency record (§8.2); and delivery_gap for a gap, which is not a refusal.
13. Conformance
live-conformance/remote/ holds the corpus, in one file,
vectors.json, written from fixed seeds by wai_remote_desktop_vectors gen. It has rooms, each
a room (descriptor, grants, key-log head or none, chains) and the outcome §11.3 gives it with
everything it states.
- Accepted: a session of a host that keeps a floor, a controller of pointer, keys and touch,
a controller of every class but touch, a viewer with no control power, and a lead with the
direct power: the host gives the floor and the holder's click and key are applied; the
viewer's input is refused
input_not_permittedand the second controller'sinput_no_floor; the second controller requests the floor, the host gives it, and its clipboard text is applied, while the first's input is refusedinput_no_floor; the holder releases it and the lead gives it back; the host resizes its screen, and an input in the old coordinates is refusedinput_wrong_screen; a touch it does not inject is refusedinput_unsupported, a pointer off the frameinput_out_of_bounds, and an input decided too lateinput_stale; an input the host never answers is unanswered; it pauses, and an input is refusedinput_screen_paused; it resumes, and a wheel turn and composed text are applied; it takes the floor back; and latency records from both controllers, with their legs. And an open session in which control passes by grant exchange: the first controller's input before the exchange is applied and its input after it refusedinput_not_permitted, and the second controller's refused before it and applied after it. - Accepted, with a floor statement §7.2 refuses: each reason of §12's third table at least once, each stated and changing nothing: the floor given by the viewer and given to the viewer, released by a controller that does not hold it, requested by the viewer, taken by a controller, and stated for a principal that is no host; and three whole floor sessions, each with the holder after the refused statement the one before it: a controller that does not hold the floor gives it to itself, a controller releases the floor another holds, and the holder's release takes its place after the host took the floor back, a race on the wire.
- Gap: the floor session with one record of a controller not delivered.
- Refused: each reason of §12's first table at least once, each vector a session of one defect: among them a host that applied an input that each rule of §5.3 refuses, a host that refused a good input or stated another rule than the one that refuses, an outcome that names no frame, and latency records naming another frame, signed by another key, or presented after they were signed.
- Sealed (§5.5), after every vector above, each stating what the opened level gives it under
the corpus's opening key (
opening_key) beside what the commitment level does: a session of a host that states a sealing key, in which keys, a composed text and a clipboard text are applied sealed, a text in the clear is refusedinput_unsealed, a text sealed to the viewer's key, one whose commitment is another text's, one whose opening does not read and another controller's copy of a sealed input are each refusedinput_bad_seal, and, on a screen with no sealing key, a sealed text is refusedinput_bad_sealand keys in the clear are applied; three sessions the commitment level accepts and the opened level refuses (the host applies the text sealed to the viewer's key, applies the text whose commitment is another's, refuses a seal that opens); and vectors both levels refuse (the host applies a text in the clear to a screen with a sealing key, applies a sealed text to a screen with none, refuses keys in the clear by rule 9; a ciphertext one byte short, a sealed action that seals a touch, a sealing key of 31 bytes). Every other vector holds no seal, and the opened level gives it what the commitment level does.
A conforming verifier gives every room the outcome, the reason, the place, and everything else
the corpus states, at both levels. wai_remote_desktop_vectors verify and the test
tests/remote_desktop.rs check that the file regenerates byte for byte, that wai-rs gives
every room its outcome at both levels, that every reason of the four tables of §12 appears, and
that §12 lists exactly the reference's codes.
live-conformance/remote_desktop_verify.py is the second implementation, written from this
text: it shares no code with wai-rs; it verifies each room with the live-rooms corpus's second
implementation (live_rooms_verify.py), every signature with WAI's strict rule
(claim-conformance/_ed25519.py), and every seal with the HPKE, X25519, HKDF and AES-128-GCM the
aggregation corpus's second implementation writes from their texts
(telemetry_aggregate_verify.py); and it mutates copies of the corpus and requires every copy
to fail. The browser viewer's module (web/room/remote.js of joule-moq-web) writes the
corpus's input statements, sealed ones among them, floor statements and latency records byte for
byte, and gives every input of the corpus the outcome the corpus states.
14. Security and privacy considerations
- Input reaches a machine. An applied action types and clicks on the host as its user would.
The host is the enforcement point (§10): it applies only what the grant in effect allows, at
the input's place, and states every decision, so a host that applies more than the grant
allows is found out by any verifier holding the room (§6.2 step 5). The authority decides who
controls, in the grants it signs; the host decides what it injects at all (
accepts) and how old an action may be (max_age_ms). A room key cannot send input (§4.3): the room's services never act on a host's machine. - Every statement is signed by its author and rides in its author's chain. A relay can withhold an input but alter none, and a withheld input is a gap in its author's chain and is unanswered. A replayed input is a duplicate (live-events §6.2) and is decided once.
- What is captured is the host's. The screen statement states the scope, the redactions, whether the pointer is drawn and whether capture is paused (§3.5), and a sink shows them. A frame can hold whatever the host's screen holds at that instant, including personal data and credentials; a host that shares a window rather than a display, or that redacts, shares less. A room that needs its frames unreadable by its relays is confidential (confidentiality): its objects and events are sealed end to end, and the rules here are unchanged. A recording of the room keeps its events as they are, a sealed action sealed: the room's recording policy decides (live-rooms §3.7).
- What a controller types is sealed to a host that states a sealing key (§5.5): a relay, the room service, every viewer and a recording of the room hold a ciphertext and a commitment for each key, text and clipboard action, and only the holder of the opening key reads them. A commitment is salted, so it cannot be guessed from the few values a key takes, and it binds the input, its order and its author, so a host that opens a seal can show a third party one action's opening without the others, and that party checks it against the record. What stays in the clear is what rules 1 to 8 read and what the screen shows: that an input holds key, text or clipboard actions and how many, their instants, a clipboard text's length, and every pointer, wheel and touch action. The instants of keystrokes can say something of what was typed; a controller that needs them hidden sends text as one composed text action, whose length is hidden too. The host reads what it injects, as it must: a host is trusted with what is typed into it.
- The encoder's name is a statement. It says what made the frames, as the host reports it, and is not evidence of it; nothing is accepted or refused on it (§3.6). The receipts bind the bytes.
- A latency record is a viewer's claim: it proves that the holder of the viewer's key stated it, not that it is true, as a presentation statement of co-watch §14 is. A modified viewer can state any instant. The record names the frame by its content hash and the host's outcome, so it cannot claim a frame the host did not name, and its presentation can be no later than its own event (§8.2 step 6). A light sensor's record is as good as the sensor.
- Keys and timing. A controller's statements say when and where it acted, which is personal
data, and to a host with no sealing key what it typed too. A principal that needs its sessions
unlinkable across rooms uses a key per room, as live-events §11 has every principal do. Taking part in a room under a grant is taking part in its mechanics, and this
extension adds no consent step to them. Where a regime asks the operator for a lawful basis
for processing what a room carries, the operator records it as a
lawful-basisclaim (jwp-receipts §2.4.6).