Skip to main content

WAI Extension: Remote Desktop

Mirrored from the canonical text at commit 260cbd09 ().

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’s screen track, 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, as meta payloads 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 the remote_desktop module of wai-rs (feature remote_desktop); the reference host agent and its browser viewer are joule-moq-remote and the room page of joule-moq-web; the corpus is live-conformance/remote/, and its second implementation is live-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 code values (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

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):

keyvaluemeaning
remote_hosttrue or falsewhether the principal may host: sign screen statements and outcomes (§4.3) and receive input
remote_controla decimal integer from 1 to 15the 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

fieldsizemeaning
magic, version5WDS1, 1
prev32the digest of the event of the host’s screen statement this one replaces; 32 zero bytes for its first
state11 live: the host captures and accepts input; 2 paused: it captures nothing and accepts no input (§3.5)
track32the track digest of the host’s screen (§3.2)
width, height2 eachu16: the frames’ size in pixels, which is the coordinate space of every action (§5.2); each above 0
fps2u16: the most frames a second it captures, above 0
scope1what it captures: 1 a whole display, 2 one window, 3 a region of a display
region8for 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
redactions1 + 8 × na 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
pointer11 the frames show the host’s pointer, 0 they do not
accepts1the action types the host injects: bit k − 1 for type k of §5.2; no bit above bit 6
clip_max2u16: the most bytes of text one clipboard action may carry; above 0 exactly when accepts holds type 7
max_age_ms4u32: the oldest an action may be when the host decides on its input (§5.3 step 7), above 0
floor11 open: every controller may send input; 2 floor: only the controller holding the floor (§7)
capabilityshortthe frames’ payload capability (SPEC §5), a token
codecshortthe frames’ codecs value (RFC 6381) as the track’s catalog entry states it, a token
encodershortthe encoder’s name and version (§3.6): a token holding exactly one @, with a byte before it and one after
api1the encoder’s interface, by role (§3.6): 1 to 5
hardware1where the encoder runs (§3.6): 0 general-purpose processors, 1 a fixed-function media engine, 2 not stated
seal0 or 32the 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

3.4 Rules and the chain

A screen statement whose fields break §3.3’s table is rd_statement_malformed. Then:

  1. its track is the track digest of its author’s screen (§3.2): otherwise screen_wrong_track;
  2. its prev is 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: otherwise screen_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:

apithe interface
1the operating system’s media framework
2a video acceleration interface whose driver drives a GPU’s encode engine
3the encode interface a GPU driver exports from its own library
4a software encoder library
5an 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

magicstatementsectionwho signs it
WDS1screen statement§3.3a host
WDI1input statement§5.1any principal that is not a room key
WDO1outcome§6.1the host its input names
WDF1floor statement§7.2as §7.2 states for its action
WDL1latency record§8.1any 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

fieldsizemeaning
magic, version5WDI1, 1
host32the key of the host the actions are for
screen32the digest of the event of the host’s screen statement whose frame the actions’ coordinates are in
count1the 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

typenameclassfieldsmeaning
1movepointerx, y (u16 each)the pointer moved to (x, y)
2buttonpointerbutton (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
3wheelpointerdx, 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
4keykeysusage (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
5textkeystext: a u8 length, 1 to 64, and that many bytes of UTF-8text composed on the controller’s side, as an input method composes it, typed as it is
6touchtouchid (u8), phase (u8), x, y (u16 each)touch point id began (phase 1), moved (2), ended (3) or was cancelled (4) at (x, y)
7clipboardclipboardtext: a u16 length and that many bytes of UTF-8text the host puts on its clipboard
8sealedthe class of ofof (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:

  1. its host is not the host’s key, or its screen is not the digest of the host’s screen in effect at the input’s place in the merged order: input_wrong_screen (1);
  2. P’s grant in effect at t does not carry the control power with the class of every action: input_not_permitted (2);
  3. the host’s screen in effect keeps a floor (floor 2) and P does not hold it at the input’s place in the merged order (§7.3): input_no_floor (3);
  4. the screen in effect is paused: input_screen_paused (4);
  5. an action’s type is not among the screen’s accepts: input_unsupported (5);
  6. 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’s clip_max bytes: input_out_of_bounds (6);
  7. n less the instant of its first action is more than the screen’s max_age_ms × 1000: input_stale (7);
  8. 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);
  9. 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:

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):

  1. the screen statement the input names states no sealing key: seal_no_key;
  2. OpenBase fails: the seal was made to another key, by another author or in another room, or was altered: seal_not_opened;
  3. 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;
  4. 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

fieldsizemeaning
magic, version5WDO1, 1
input32the digest of the input statement’s event
outcome11 applied, 2 refused
reason10 when applied; when refused, the number of the rule of §5.3 that refused it, 1 to 9
applied_at8u64: when applied, the instant on the host’s estimate it injected the last action; 0 when refused
captured_at8u64: when applied, the instant it captured the frame the event’s anchor names; 0 when refused
content32when applied, the content hash of that frame’s object as its receipt states it (jwp-receipts); 32 zero bytes when refused
rtt_us4u32: 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:

  1. its input is the digest of an input statement the room accepted: otherwise outcome_unknown_input;
  2. its author is that input’s host (§4.3): otherwise rd_not_permitted;
  3. no outcome before it in the merged order names the same input: otherwise outcome_duplicate;
  4. its fields agree with its outcome and with one another: an applied outcome has reason 0, an anchor whose track is the track digest of the host’s screen and whose object is 0, and a content not all zero, and the input’s t ≤ applied_at ≤ captured_at ≤ its own t; a refused one has a reason from 1 to 9, no anchor, applied_at, captured_at and content zero, and a t at or after the input’s. Otherwise outcome_invalid;
  5. 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. Otherwise outcome_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.

7.2 The floor statement

fieldsizemeaning
magic, version5WDF1, 1
host32the host whose floor it is
action11 request, 2 give, 3 release, 4 take
to32for 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:

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

fieldsizemeaning
magic, version5WDL1, 1
room32the room id
viewer32the key that signs it, its author’s
host32the host
input32the digest of the event of the viewer’s input statement
action1the index in that statement of the action measured, from 0
outcome32the digest of the event of the host’s outcome of that input
track32the frame’s track, as the outcome’s anchor names it
group, object8 eachu64: the frame’s group and object
content32the frame’s content hash, as its receipt states it
input_at8u64: the action’s instant (§5.1)
presented_at8u64: the instant, on the viewer’s estimate, the frame was presented
source1what 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_us4u32: presented_at − input_at
rtt_us4u32: the round trip of the clock measurement the viewer’s estimate rests on (§8.4); 2^32 − 1 when it states none
sig64Ed25519 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:

  1. its room is the room id: otherwise wrong_room;
  2. its viewer is its event’s principal: otherwise latency_wrong_key;
  3. its signature verifies under viewer (§8.1): bad_key, bad_signature;
  4. its input is the digest of an input statement the room accepted, by viewer, whose host is the record’s host, with an action at index action: otherwise latency_unknown_input;
  5. its outcome is the digest of the outcome of that input, which the room accepted, and which states it applied; its track, group and object are that outcome’s anchor’s, and its content that outcome’s content; its input_at is the input’s t less the action’s dt_us: otherwise latency_mismatch;
  6. its presented_at is at most its event’s t: a record states what was presented, never what will be. Otherwise latency_ahead;
  7. its presented_at is at least its input_at, and its latency_us is the difference: otherwise latency_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:

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:

This extension adds no record for any of these.

10. Where each rule is enforced

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:

  1. The room (live-rooms §9): a refusal, at room with 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.
  2. The grants’ attributes (§3.1), in the grants’ order: at the grant’s index.
  3. The statements, in the merged order: each meta event 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.
  4. The outcome (§11.3).

11.3 Outcomes and what a verification states

With accept or gap, the verifier states:

11.4 Two levels

A remote desktop verification is made at one of two levels.

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:

reasonraised 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:

numberreasonrule
1input_wrong_screen§5.3 step 1
2input_not_permitted§5.3 step 2
3input_no_floor§5.3 step 3
4input_screen_paused§5.3 step 4
5input_unsupported§5.3 step 5
6input_out_of_bounds§5.3 step 6
7input_stale§5.3 step 7
8input_unsealed§5.3 step 8
9input_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):

reasonraised 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:

reasonraised 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.

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