wai.live.remote: what a controller types is sealed to the host; TLS in the host agent; a bound on the clock's record store (CX2c, stacked on #489 and #481) #490

Closed
dcharlot wants to merge 0 commits from wai/remote-input-seal into main
Owner

What a controller types is sealed to the host (CX2c)

This branch contains #489 (wai/room-remote, head d1e8965f) and #481 (wai/remote-desktop), and lands after them. The base is main. This slice's commits are the ones after d1e8965f.

In a remote desktop room every input statement (WDI1) reached every principal of the room as a meta event, so a viewer received what a controller typed and pasted, in the clear. This slice seals key, text and clipboard actions to the host, keeps a commitment to each in the record, and verifies a session at two levels: what a viewer can check without the plaintext, and what a verifier given the host's opening key can check. It also gives the host agent TLS to the room service and the relay, and bounds the room service clock's per-session record store.

The standard (wai/extensions/remote-desktop.md)

  • The sealing key (§3.3). A host states an X25519 public key (RFC 7748 §5) as the screen statement's last field, seal. A statement without it reads as before and means the host takes key, text and clipboard actions in the clear.
  • The sealed action (§5.2, type 8 sealed). Its class is the class of the type it seals. In the clear it keeps three things: of (4, 5 or 7), length (a clipboard text's byte length, 0 otherwise) and commit (32 bytes). Rules 1 to 8 read only those and the action's dt_us.
  • The seal (§5.5). One HPKE seal per statement, written after the last action as enc and then the ciphertext. It is SealBase (RFC 9180 §6.1), base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM:
    • pkR: the sealing key of the screen statement the input names;
    • info: wai:remote-seal 0x01, the room id and the host's key;
    • aad: the author's key, then the statement through its last action;
    • pt: the sealed actions' openings, in order.
  • The opening (§5.5) of a sealed action is a fresh 16-byte salt and then its fields:
    • a key: usage, down, modifiers;
    • a text: its length, padded with zero bytes to 64, so a text's length is not in the clear;
    • a clipboard text: its bytes.
  • The commitment is SHA-256 over wai:remote-commit 0x01, the room, the author, the host, the screen statement, the action's index and dt_us, the type it seals, and the opening. It binds the input, its order and its author. The salt keeps it from being guessed from the few thousand values a key can take. Because a commitment covers one action, a host can show a third party one action's opening without revealing the others.
  • Pointer, wheel and touch actions stay in the clear. The screen shows them anyway, and §5.5 says so.
  • Two new rules (§5.3). Rule 8, input_unsealed: a key, text or clipboard action sent in the clear to a screen that states a sealing key. Rule 9, input_bad_seal: a seal that cannot be accepted. Its four reasons are §12's new fourth table:
    • seal_no_key: the screen states no sealing key;
    • seal_not_opened: OpenBase fails (another key, another author, another room, or an altered seal);
    • seal_unreadable: what it opens to does not read;
    • seal_commitment: an opening does not match its commitment.
  • Malformed seals are refused rd_statement_malformed: a ciphertext that is not exactly 16 bytes longer than its openings, a sealed action whose of is not 4, 5 or 7, a non-zero length on a key or text, sealed actions with no seal, and a sealing key that is not 32 bytes.
  • Two verification levels (§11.4, §6.2 step 5).
    • The commitment level is what every party holding the room checks. It reads no plaintext. It checks every outcome against rules 1 to 8, and against rule 9 where the screen states no sealing key. For a sealed input to a screen that states one, it accepts either applied or refused by rule 9.
    • The opened level is given the host's opening key. It opens every seal, makes all nine rules, and states each sealed input opened (with the statement as it reads opened) or the reason it was not.
  • §12's tables, SPEC §5 and the CHANGELOG are updated. An outcome's reason now runs to 9. A host's totals state rules 8 and 9 only where they are not zero, so every earlier vector's statement is unchanged. Every other field of every record is as it was.

The reference (wai-rs) and the corpus

  • Input::seal, Input::open, remote_desktop::commitment, decide_opened (all nine rules; returns the actions to inject), verify_opened, SealFault and SealFact.
  • HPKE moves from telemetry_aggregate::hpke to a crate module, wai::hpke, behind its own feature hpke. Both modules use it. Its RFC 9180 A.1.1 test moves with it.
  • The corpus. live-conformance/remote/vectors.json keeps its 42 vectors as #489 left them, an unchanged prefix byte for byte, and appends 10 sealed vectors. Each sealed vector states what the opened level gives under the corpus's opening_key.
    • sealed_session: keys, a text and a clipboard text applied sealed; a text in the clear refused 8; a seal to the viewer's key, a commitment that does not match, an opening that does not read, and another controller's copy each refused 9, which the opened level finds seal_not_opened, seal_commitment, seal_unreadable and seal_not_opened; on a screen with no sealing key, a sealed text refused 9 (seal_no_key) and keys in the clear applied.
    • Accepted at the commitment level and refused at the opened level: sealed_applies_wrong_key, sealed_applies_commitment_mismatch, sealed_refuses_good_seal.
    • Refused at both levels: sealed_applies_unsealed, sealed_applies_no_key, sealed_refuses_clear_by_seal, sealed_malformed_length, sealed_malformed_type, screen_seal_malformed.
    • For every vector that holds no seal, both verifiers require the opened level to give what the commitment level gives.
  • remote_desktop_verify.py, written from the spec text, checks both levels. It opens seals with the HPKE, X25519, HKDF and AES-128-GCM of telemetry_aggregate_verify.py (the aggregation corpus's second implementation, written from the RFCs and self-tested here against RFC 9180 A.1.1). It adds 11 mutations and 7 dropped checks: sealed_of, seal_length, rule_unsealed, seal_no_key, seal_open, seal_read, seal_commit. --room <file> --opening-key <hex> verifies a deployment's session at the opened level.

The page (joule-moq-web)

  • remote.js seals with WebCrypto (X25519, HMAC, AES-GCM). Its HPKE is tested against RFC 9180 A.1.1. Its tests reseal every corpus input that opens, byte for byte, from the corpus's opened statement and seeds.
  • The viewer seals every key, text and clipboard action to the sealing key of the host's screen in effect. It never sends them in the clear: to a host that states no sealing key it sends none.
  • A composed text longer than 64 bytes is sent as several text actions, split at character boundaries.
  • In a remote room the page follows every present principal's events. It shows that another controller "is typing…" or "is pasting…", never what.

The host agent (joule-moq-remote)

  • Sealing. It makes an X25519 sealing key for each session and states it. The gate decides each input with decide_opened and injects what the seal opens to.
  • The opening key stays in the session. --opening-key <file> writes it (mode 0600, never over an existing file) for a verifier the host chooses. Report::opening_key carries it too.
  • The headless viewer seals its key, text and clipboard actions to the host, keeps the records it receives, and follows another principal's events when told to (follow).
  • TLS. The room service is reached over https:// and the relay over wss://, with rustls (ring) and the system's roots plus any certificates in --ca <pem>. Every chain and every name is verified, and there is no option that turns verification off. tls::front is a TLS front for a plain backend, which the tests use.
  • --record-injected <file>, with --input record, writes counts and SHA-256 digests of what the recorder injected, so the browser e2e can check injection without the text being written anywhere.
  • The agent stops when the service's clock records the host leaving (Report::ended).
  • Lock files gain only tokio-rustls 0.26.6 and new dependency edges.

The room service (joule-moq-room): the record bounds, per principal first

Update (head e8c747ec): the bound falls on the participant. In the first push the bound was one total for the whole session, so a single participant could fill it and end the session for everyone. That includes a participant whose inputs the host refuses but whose records the clock still keeps.

  • [remote] max_records_per_principal: 100000 by default, allowed range 16 to 10000000.
    • What it holds: the clock holds each principal's records of seq 0 up to the bound. The host is the exception.
    • When a record past it arrives: the clock takes the principal's records up to the bound, cuts it at seq = the bound, and removes it (leave reason 2). The cut reason is max_records_per_principal, and it goes through the same path a record out of order takes.
    • The rest of the session is unaffected: everyone else continues, and the session verifies at both levels.
    • The host agent matches the clock: it reads the bound from GET /room (remote.max_records_per_principal), and its gate cuts the principal at the same record. So the host and the clock hold the same records.
    • The host has no bound of its own. Its outcomes answer everyone else's inputs, so its record count grows with the room, and cutting the host would end the session.
  • Why the default is 100000: the policy every grant carries allows 120 events a second. A controller sends at most one input statement per display refresh (60 a second) while it acts, and far fewer on average.
    • A two-hour session at an average of ten statements a second is 72000 records, which is within the bound.
    • A flood at the policy's full 120 a second reaches the bound in about 14 minutes, and only the flooder is removed.
  • [remote] max_records stays as the safety net, now 1000000 by default, allowed range 16 to 100000000. That is ten times the per-principal bound.
    • A host and four controllers each at their bound hold 400000 records of their own plus about as many host outcomes: 800000, still below the total.
    • So the per-principal bound is the one any single principal meets first. The total is reached only when five or more principals act near their bounds at once.
    • At the total, the clock holds about a gigabyte.
  • Where this is written down: remote-desktop §10 now says a room service SHOULD bound records per principal first, and says which bound bites first and why. The configuration docs (room.example.toml, the README, the module docs) say the same.
  • At the total, the clock ends the session. It holds no record past the bound. It signs every present principal's leave (reason 2), each after that principal's last record taken, with the host's last. GET /room's remote states ended: "max_records", the records held, and the records that arrived past the bound. The ended session is whole and verifies, and the next host's request starts a new room.
  • Why end it rather than refuse inputs past the bound:
    • a record the clock does not hold leaves a hole in its author's chain, and no later record of that author can follow it (live-events §6);
    • the host would decide inputs the clock never held, and its outcomes would name inputs missing from the clock's session (outcome_unknown_input);
    • an input's refusal is the host's to state (§5.3), not the clock's.

Design decisions, and why

  1. The sealing key is a field of the screen statement, not a grant attribute or a new statement. The host states its own key under its own signature and can make a new one for each session without involving the authority. A statement without it reads exactly as before, which is what lets the earlier corpus stay an unchanged prefix.
  2. One seal per statement, one commitment per action. One enc and one tag per statement keeps the overhead small. A commitment per action allows one action to be disclosed alone.
  3. Rule 9 comes last (after rule 7, the age rule). The rules every party can check decide first. Only a host's application or refusal of a seal it alone can open is outside the commitment level, and the opened level checks it.
  4. What stays in the clear is what rules 1 to 8 read and what the screen shows: that an input holds keys, text or clipboard actions and how many, their instants, and a clipboard text's length (rule 6 needs it). A text's length is padded away. §14 says that keystroke timing can still leak information, and that a composed text hides it.
  5. The page is stricter than the spec. The spec lets a host with no sealing key take keys in the clear; the page never sends them.

Checks

Check Host Command Result
wai-rs units: sealing, commitments, rules 8 and 9, HPKE (RFC 9180 A.1.1), the corpus against itself Linux build host cargo test --locked --no-default-features --features remote_desktop --lib -- remote_desktop:: hpke:: 11 passed
wai-rs corpus: regenerates byte for byte; every room at both levels; §12's four tables Linux build host … --features remote_desktop --test remote_desktop; wai_remote_desktop_vectors verify ../live-conformance/remote 3 passed; PASS, PASS
earlier corpus an unchanged prefix local the 42 vectors of #489's vectors.json compared as text and as JSON with the regenerated file identical; 10 vectors appended
telemetry_aggregate after the HPKE move Linux build host … --features telemetry_aggregate --lib -- telemetry_aggregate:: telemetry:: hpke::; --test telemetry_aggregate 23 passed; 6 passed
hpke builds alone; registry Linux build host cargo build --no-default-features --features hpke --lib; --lib -- codecs::registry::; wai registry export --check ok; 14 passed; ok
clippy, wai-rs Linux build host cargo clippy --no-default-features --features remote_desktop,telemetry_aggregate --all-targets no note in a touched file
second implementation Linux build host python3 -I live-conformance/remote_desktop_verify.py live-conformance/remote PASS: every vector at both levels, every reason of the four tables, 30 mutated copies caught, 39 dropped checks caught
verifiers it reads Linux build host telemetry_aggregate_verify.py live-conformance/aggregate; live_rooms_verify.py live-conformance/rooms PASS; PASS
host agent, every target, ×3 (and once at e8c747ec) Linux build host cd joule-moq-remote && cargo test --locked --all-targets -- --test-threads=1 15 unit + loopback + 3 TLS, 3 of 3 runs
host agent clippy Linux build host cargo clippy --locked --all-targets --no-deps -- -D warnings clean
room service, every target Linux build host cd joule-moq-room && cargo test --locked --all-targets 17 unit + 8 integration, remote_room's three among them (update: the flood test is the third)
the remote room, looped Linux build host cargo test --locked --test remote_room ×4, at the update 4 of 4 runs, 3 tests each
update: the gate's bound Linux build host cd joule-moq-remote && cargo test --locked --lib 16 passed (gate::tests::a_participant_is_cut_at_its_bound new); clippy -D warnings clean
room service clippy Linux build host cargo clippy --locked --all-targets --no-deps no new note (the one in epoch.rs is on main; the int_payload dead-code warning is wai-rs's, on main)
lock files Linux build host cargo metadata --locked --offline --format-version 1 in wai-rs, the agent and the room service ok; the two locks gain tokio-rustls and edges only
page modules local node --test web/room/remote.test.mjs (and the other page tests) 16 of 16 (63 of 63 in run.sh)
browser e2e, remote macOS build host BROWSER_BIN=… e2e/run.sh --only remote 34 of 34 at head e8c747ec (and at f96389a3 and 7ade5fc0): typed, composed and pasted text applied sealed (12 of 12 of her inputs applied, 11 sealed); bob's page shows "is typing…" and its 45 received records hold no trace of the three texts or of a key in the clear, while the search finds the text sent in the clear, which the host refuses input_unsealed; the injected keys, text and clipboard text match their digests; both sessions accept by remote_desktop_verify.py at the commitment level and, under the host's opening key, at the opened level, every seal opened to what alice typed; the platform hardware encoder; page latencies 180 ms to 1.1 s, source 1, with legs
repo checks local check-tests-in-ci.py, check-workflow-concurrency.sh, check-spec-refs.py, check-wai-section-refs.py, check-no-vanity-metrics.sh, test-public-text-checks.sh, check-comparative-prose.sh, check-orphan-sources.py; workflows parse (ruby -ryaml) all exit 0
public names local check-public-names.sh on the 39 changed files; commit messages and this body with grep -E clean

Falsifications

Everything was committed first. Each guard was broken alone and its test run, then the file was restored with git checkout -- <file> and touched, and the test run again.

  • The Rust mutations were applied with git apply inside a checkout of the committed branch, on the Linux build host.
  • The page's mutations were edits in the worktree, run with local node --test.
Guard broken Mutant Guard Mutant result Restored
send text unsealed (page) RemoteViewer.flush sends inputPayload(i) node --test remote.test.mjs exit 1: "seals keys, text and clipboard text to the host, and sends none in the clear" 0 (15/15)
send text unsealed (headless viewer) Viewer::input sends in the clear cargo test --test remote_room -- a_remote_room_the_service_keeps exit 101: "applied, the seal opened by the host" (refused input_unsealed) 0
seal to a viewer's key (page) the page seals to a key derived from its own principal node --test remote.test.mjs exit 1: "sealed to the host's sealing key, the screen statement's" 0
seal to a viewer's key (headless viewer) Viewer::input seals to a key of its own --test remote_room exit 101: "applied, the seal opened by the host" (refused input_bad_seal) 0
accept a commitment mismatch Input::open ignores a commitment that differs cargo test --features remote_desktop --test remote_desktop exit 101: the corpus does not regenerate (sealed_session opened: outcome_mismatch), stated outcomes differ 0
skip TLS certificate verification the agent's client config accepts every certificate cargo test --test tls exit 101: all three tests (untrusted chain, wrong name, wss) 0
remove the clock's bound on the session on_events holds every record --test remote_room -- a_session_ends_at_its_record_bound exit 101: no leave within the step 0
update: remove the bound per principal on_events holds bob's records past his bound --test remote_room -- a_flooding_principal_is_cut_at_its_own_bound exit 101: "bob alone is cut: []; the session ended: "max_records"". Bob's flood filled the total and ended the session for everyone 0
the page's sends out of order (found while running the browser scenario) flush not chained node --test remote.test.mjs exit 1: "the page's sends keep their order while a seal is made" 0
(Python) each of sealed_of, seal_length, rule_unsealed, seal_no_key, seal_open, seal_read, seal_commit dropped the verifier's own drop list remote_desktop_verify.py each caught by a vector —

What runs, and how

  • wai-rs. Run on the Linux build host:
    • cargo test --locked --no-default-features --features remote_desktop --lib -- remote_desktop:: hpke::
    • cargo test --locked --no-default-features --features remote_desktop --test remote_desktop
    • remote_desktop_verify.py live-conformance/remote
    • The corpus is regenerated with cargo run --no-default-features --features remote_desktop --bin wai_remote_desktop_vectors -- gen ../live-conformance/remote.
  • CI (wai-conformance.yml): the remote desktop step now also runs hpke::. Its comments and the paths comment say "four tables".
  • joule-moq-remote: cargo test --locked --all-targets -- --test-threads=1. This picks up the new tests/tls.rs on its own. CI runs the same on linux-arm64 and macos.
  • joule-moq-room: cargo test --locked --all-targets. This includes tests/remote_room.rs, now two tests.
  • The page's modules: node --test web/room/remote.test.mjs, which is 16 tests.
  • The browser scenario runs on the macOS build host: BROWSER_BIN=… e2e/run.sh --only remote.
  • Verifying a deployment's session at the opened level: run the host with --opening-key <file>, then python3 -I remote_desktop_verify.py --room <session.json> --opening-key <hex>.

Not done

  • Keystroke timing. The number of sealed actions and their instants stay in the clear, because rules 1 to 8 and the latency records need them. §14 states this. Padding the count, or sending dummy actions, is not done.
  • Forward secrecy relies on the host discarding the opening key. The agent makes a key for each session and keeps it only in memory, unless --opening-key writes it. It does not zeroize memory, and it does not rotate the key within a session. The spec allows rotation (a new screen statement with a new key), and both verifiers take several opening keys.
  • The session total can still end a session if five or more principals each act near their own bound at once. That is the safety net doing its job. The bounds are counts of records, not bytes.
  • TLS in the browser scenario. The host agent's TLS is covered by Rust tests (tests/tls.rs, and tests/remote_room.rs through TLS fronts). The browser scenario runs the agent over http:// and ws:// on loopback.
  • No deploy, and no Caddy or site change.
  • Everything #489 lists as not done, apart from its two items this slice closes:
    • its "viewers still receive every input record" item (their content is now sealed);
    • its "no TLS client" item.

Decisions for the owner

  1. Where the sealing key lives: as the screen statement's optional last field (described above). The alternative is a grant attribute that the authority signs. That would need the service to take the host's X25519 key when it issues the grant.
  2. What happens at a record bound. At a principal's own bound, that principal is cut and removed and the session goes on. At the session total, the session ends. Refusing new inputs was not chosen, for the three reasons given above.
  3. The page sends no key, text or clipboard action to a host that states no sealing key, although the spec allows a host to take them in the clear.
  4. HPKE is shared: it moved out of telemetry_aggregate into a crate module behind the feature hpke.
  5. The Python verifier imports telemetry_aggregate_verify.py for HPKE instead of carrying a second AES-GCM. Both are second implementations written from the RFCs, and neither shares code with wai-rs.
  6. --record-injected is a test hook in the binary. It works only with --input record and writes counts and digests, never text. It lets the browser scenario check what the host injected without the typed text being written to disk.

🤖 Generated with Claude Code


Closed: Landed on main through merge train E (#499, merge bf39ec9a); verified by content: this PR's head is in main

## What a controller types is sealed to the host (CX2c) **This branch contains #489 (`wai/room-remote`, head d1e8965f) and #481 (`wai/remote-desktop`), and lands after them.** The base is main. This slice's commits are the ones after d1e8965f. In a remote desktop room every input statement (`WDI1`) reached every principal of the room as a `meta` event, so a viewer received what a controller typed and pasted, in the clear. This slice seals key, text and clipboard actions to the host, keeps a commitment to each in the record, and verifies a session at two levels: what a viewer can check without the plaintext, and what a verifier given the host's opening key can check. It also gives the host agent TLS to the room service and the relay, and bounds the room service clock's per-session record store. ### The standard (`wai/extensions/remote-desktop.md`) - **The sealing key** (§3.3). A host states an X25519 public key (RFC 7748 §5) as the screen statement's last field, `seal`. A statement without it reads as before and means the host takes key, text and clipboard actions in the clear. - **The sealed action** (§5.2, type 8 `sealed`). Its class is the class of the type it seals. In the clear it keeps three things: `of` (4, 5 or 7), `length` (a clipboard text's byte length, 0 otherwise) and `commit` (32 bytes). Rules 1 to 8 read only those and the action's `dt_us`. - **The seal** (§5.5). One HPKE seal per statement, written after the last action as `enc` and then the ciphertext. It is SealBase (RFC 9180 §6.1), base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM: - `pkR`: the sealing key of the screen statement the input names; - `info`: `wai:remote-seal` 0x01, the room id and the host's key; - `aad`: the author's key, then the statement through its last action; - `pt`: the sealed actions' openings, in order. - **The opening** (§5.5) of a sealed action is a fresh 16-byte salt and then its fields: - a key: usage, down, modifiers; - a text: its length, padded with zero bytes to 64, so a text's length is not in the clear; - a clipboard text: its bytes. - **The commitment** is SHA-256 over `wai:remote-commit` 0x01, the room, the author, the host, the screen statement, the action's index and `dt_us`, the type it seals, and the opening. It binds the input, its order and its author. The salt keeps it from being guessed from the few thousand values a key can take. Because a commitment covers one action, a host can show a third party one action's opening without revealing the others. - **Pointer, wheel and touch actions stay in the clear.** The screen shows them anyway, and §5.5 says so. - **Two new rules** (§5.3). Rule 8, `input_unsealed`: a key, text or clipboard action sent in the clear to a screen that states a sealing key. Rule 9, `input_bad_seal`: a seal that cannot be accepted. Its four reasons are §12's new fourth table: - `seal_no_key`: the screen states no sealing key; - `seal_not_opened`: OpenBase fails (another key, another author, another room, or an altered seal); - `seal_unreadable`: what it opens to does not read; - `seal_commitment`: an opening does not match its commitment. - **Malformed seals** are refused `rd_statement_malformed`: a ciphertext that is not exactly 16 bytes longer than its openings, a sealed action whose `of` is not 4, 5 or 7, a non-zero `length` on a key or text, sealed actions with no seal, and a sealing key that is not 32 bytes. - **Two verification levels** (§11.4, §6.2 step 5). - The commitment level is what every party holding the room checks. It reads no plaintext. It checks every outcome against rules 1 to 8, and against rule 9 where the screen states no sealing key. For a sealed input to a screen that states one, it accepts either applied or refused by rule 9. - The opened level is given the host's opening key. It opens every seal, makes all nine rules, and states each sealed input `opened` (with the statement as it reads opened) or the reason it was not. - **§12's tables, SPEC §5 and the CHANGELOG** are updated. An outcome's `reason` now runs to 9. A host's totals state rules 8 and 9 only where they are not zero, so every earlier vector's statement is unchanged. Every other field of every record is as it was. ### The reference (`wai-rs`) and the corpus - `Input::seal`, `Input::open`, `remote_desktop::commitment`, `decide_opened` (all nine rules; returns the actions to inject), `verify_opened`, `SealFault` and `SealFact`. - HPKE moves from `telemetry_aggregate::hpke` to a crate module, `wai::hpke`, behind its own feature `hpke`. Both modules use it. Its RFC 9180 A.1.1 test moves with it. - **The corpus.** `live-conformance/remote/vectors.json` keeps its 42 vectors as #489 left them, an unchanged prefix byte for byte, and appends 10 sealed vectors. Each sealed vector states what the opened level gives under the corpus's `opening_key`. - `sealed_session`: keys, a text and a clipboard text applied sealed; a text in the clear refused 8; a seal to the viewer's key, a commitment that does not match, an opening that does not read, and another controller's copy each refused 9, which the opened level finds `seal_not_opened`, `seal_commitment`, `seal_unreadable` and `seal_not_opened`; on a screen with no sealing key, a sealed text refused 9 (`seal_no_key`) and keys in the clear applied. - Accepted at the commitment level and refused at the opened level: `sealed_applies_wrong_key`, `sealed_applies_commitment_mismatch`, `sealed_refuses_good_seal`. - Refused at both levels: `sealed_applies_unsealed`, `sealed_applies_no_key`, `sealed_refuses_clear_by_seal`, `sealed_malformed_length`, `sealed_malformed_type`, `screen_seal_malformed`. - For every vector that holds no seal, both verifiers require the opened level to give what the commitment level gives. - **`remote_desktop_verify.py`**, written from the spec text, checks both levels. It opens seals with the HPKE, X25519, HKDF and AES-128-GCM of `telemetry_aggregate_verify.py` (the aggregation corpus's second implementation, written from the RFCs and self-tested here against RFC 9180 A.1.1). It adds 11 mutations and 7 dropped checks: `sealed_of`, `seal_length`, `rule_unsealed`, `seal_no_key`, `seal_open`, `seal_read`, `seal_commit`. `--room <file> --opening-key <hex>` verifies a deployment's session at the opened level. ### The page (`joule-moq-web`) - `remote.js` seals with WebCrypto (X25519, HMAC, AES-GCM). Its HPKE is tested against RFC 9180 A.1.1. Its tests reseal every corpus input that opens, byte for byte, from the corpus's opened statement and seeds. - The viewer seals every key, text and clipboard action to the sealing key of the host's screen in effect. **It never sends them in the clear**: to a host that states no sealing key it sends none. - A composed text longer than 64 bytes is sent as several text actions, split at character boundaries. - In a remote room the page follows every present principal's events. It shows that another controller "is typing…" or "is pasting…", never what. ### The host agent (`joule-moq-remote`) - **Sealing.** It makes an X25519 sealing key for each session and states it. The gate decides each input with `decide_opened` and injects what the seal opens to. - **The opening key** stays in the session. `--opening-key <file>` writes it (mode 0600, never over an existing file) for a verifier the host chooses. `Report::opening_key` carries it too. - **The headless viewer** seals its key, text and clipboard actions to the host, keeps the records it receives, and follows another principal's events when told to (`follow`). - **TLS.** The room service is reached over `https://` and the relay over `wss://`, with rustls (ring) and the system's roots plus any certificates in `--ca <pem>`. Every chain and every name is verified, and there is no option that turns verification off. `tls::front` is a TLS front for a plain backend, which the tests use. - **`--record-injected <file>`**, with `--input record`, writes counts and SHA-256 digests of what the recorder injected, so the browser e2e can check injection without the text being written anywhere. - The agent stops when the service's clock records the host leaving (`Report::ended`). - Lock files gain only `tokio-rustls` 0.26.6 and new dependency edges. ### The room service (`joule-moq-room`): the record bounds, per principal first **Update (head e8c747ec): the bound falls on the participant.** In the first push the bound was one total for the whole session, so a single participant could fill it and end the session for everyone. That includes a participant whose inputs the host refuses but whose records the clock still keeps. - **`[remote] max_records_per_principal`**: 100000 by default, allowed range 16 to 10000000. - **What it holds:** the clock holds each principal's records of `seq` 0 up to the bound. The host is the exception. - **When a record past it arrives:** the clock takes the principal's records up to the bound, cuts it at `seq` = the bound, and removes it (`leave` reason 2). The cut reason is `max_records_per_principal`, and it goes through the same path a record out of order takes. - **The rest of the session is unaffected:** everyone else continues, and the session verifies at both levels. - **The host agent matches the clock:** it reads the bound from `GET /room` (`remote.max_records_per_principal`), and its gate cuts the principal at the same record. So the host and the clock hold the same records. - **The host has no bound of its own.** Its outcomes answer everyone else's inputs, so its record count grows with the room, and cutting the host would end the session. - **Why the default is 100000:** the policy every grant carries allows 120 events a second. A controller sends at most one input statement per display refresh (60 a second) while it acts, and far fewer on average. - A two-hour session at an average of ten statements a second is 72000 records, which is within the bound. - A flood at the policy's full 120 a second reaches the bound in about 14 minutes, and only the flooder is removed. - **`[remote] max_records` stays as the safety net**, now 1000000 by default, allowed range 16 to 100000000. That is ten times the per-principal bound. - A host and four controllers each at their bound hold 400000 records of their own plus about as many host outcomes: 800000, still below the total. - So the per-principal bound is the one any single principal meets first. The total is reached only when five or more principals act near their bounds at once. - At the total, the clock holds about a gigabyte. - **Where this is written down:** remote-desktop §10 now says a room service SHOULD bound records per principal first, and says which bound bites first and why. The configuration docs (`room.example.toml`, the README, the module docs) say the same. - **At the total, the clock ends the session.** It holds no record past the bound. It signs every present principal's `leave` (reason 2), each after that principal's last record taken, with the host's last. `GET /room`'s `remote` states `ended: "max_records"`, the records held, and the records that arrived past the bound. The ended session is whole and verifies, and the next host's request starts a new room. - **Why end it rather than refuse inputs past the bound:** - a record the clock does not hold leaves a hole in its author's chain, and no later record of that author can follow it (live-events §6); - the host would decide inputs the clock never held, and its outcomes would name inputs missing from the clock's session (`outcome_unknown_input`); - an input's refusal is the host's to state (§5.3), not the clock's. ### Design decisions, and why 1. **The sealing key is a field of the screen statement**, not a grant attribute or a new statement. The host states its own key under its own signature and can make a new one for each session without involving the authority. A statement without it reads exactly as before, which is what lets the earlier corpus stay an unchanged prefix. 2. **One seal per statement, one commitment per action.** One `enc` and one tag per statement keeps the overhead small. A commitment per action allows one action to be disclosed alone. 3. **Rule 9 comes last** (after rule 7, the age rule). The rules every party can check decide first. Only a host's application or refusal of a seal it alone can open is outside the commitment level, and the opened level checks it. 4. **What stays in the clear** is what rules 1 to 8 read and what the screen shows: that an input holds keys, text or clipboard actions and how many, their instants, and a clipboard text's length (rule 6 needs it). A text's length is padded away. §14 says that keystroke timing can still leak information, and that a composed text hides it. 5. **The page is stricter than the spec.** The spec lets a host with no sealing key take keys in the clear; the page never sends them. ### Checks | Check | Host | Command | Result | |---|---|---|---| | wai-rs units: sealing, commitments, rules 8 and 9, HPKE (RFC 9180 A.1.1), the corpus against itself | Linux build host | `cargo test --locked --no-default-features --features remote_desktop --lib -- remote_desktop:: hpke::` | 11 passed | | wai-rs corpus: regenerates byte for byte; every room at both levels; §12's four tables | Linux build host | `… --features remote_desktop --test remote_desktop`; `wai_remote_desktop_vectors verify ../live-conformance/remote` | 3 passed; PASS, PASS | | earlier corpus an unchanged prefix | local | the 42 vectors of #489's `vectors.json` compared as text and as JSON with the regenerated file | identical; 10 vectors appended | | telemetry_aggregate after the HPKE move | Linux build host | `… --features telemetry_aggregate --lib -- telemetry_aggregate:: telemetry:: hpke::`; `--test telemetry_aggregate` | 23 passed; 6 passed | | `hpke` builds alone; registry | Linux build host | `cargo build --no-default-features --features hpke --lib`; `--lib -- codecs::registry::`; `wai registry export --check` | ok; 14 passed; ok | | clippy, wai-rs | Linux build host | `cargo clippy --no-default-features --features remote_desktop,telemetry_aggregate --all-targets` | no note in a touched file | | second implementation | Linux build host | `python3 -I live-conformance/remote_desktop_verify.py live-conformance/remote` | PASS: every vector at both levels, every reason of the four tables, 30 mutated copies caught, 39 dropped checks caught | | verifiers it reads | Linux build host | `telemetry_aggregate_verify.py live-conformance/aggregate`; `live_rooms_verify.py live-conformance/rooms` | PASS; PASS | | host agent, every target, ×3 (and once at e8c747ec) | Linux build host | `cd joule-moq-remote && cargo test --locked --all-targets -- --test-threads=1` | 15 unit + loopback + 3 TLS, 3 of 3 runs | | host agent clippy | Linux build host | `cargo clippy --locked --all-targets --no-deps -- -D warnings` | clean | | room service, every target | Linux build host | `cd joule-moq-room && cargo test --locked --all-targets` | 17 unit + 8 integration, `remote_room`'s three among them (update: the flood test is the third) | | the remote room, looped | Linux build host | `cargo test --locked --test remote_room` ×4, at the update | 4 of 4 runs, 3 tests each | | update: the gate's bound | Linux build host | `cd joule-moq-remote && cargo test --locked --lib` | 16 passed (`gate::tests::a_participant_is_cut_at_its_bound` new); clippy `-D warnings` clean | | room service clippy | Linux build host | `cargo clippy --locked --all-targets --no-deps` | no new note (the one in `epoch.rs` is on main; the `int_payload` dead-code warning is wai-rs's, on main) | | lock files | Linux build host | `cargo metadata --locked --offline --format-version 1` in wai-rs, the agent and the room service | ok; the two locks gain `tokio-rustls` and edges only | | page modules | local | `node --test web/room/remote.test.mjs` (and the other page tests) | 16 of 16 (63 of 63 in run.sh) | | browser e2e, remote | macOS build host | `BROWSER_BIN=… e2e/run.sh --only remote` | 34 of 34 at head e8c747ec (and at f96389a3 and 7ade5fc0): typed, composed and pasted text applied sealed (12 of 12 of her inputs applied, 11 sealed); bob's page shows "is typing…" and its 45 received records hold no trace of the three texts or of a key in the clear, while the search finds the text sent in the clear, which the host refuses `input_unsealed`; the injected keys, text and clipboard text match their digests; both sessions accept by `remote_desktop_verify.py` at the commitment level and, under the host's opening key, at the opened level, every seal opened to what alice typed; the platform hardware encoder; page latencies 180 ms to 1.1 s, source 1, with legs | | repo checks | local | `check-tests-in-ci.py`, `check-workflow-concurrency.sh`, `check-spec-refs.py`, `check-wai-section-refs.py`, `check-no-vanity-metrics.sh`, `test-public-text-checks.sh`, `check-comparative-prose.sh`, `check-orphan-sources.py`; workflows parse (`ruby -ryaml`) | all exit 0 | | public names | local | `check-public-names.sh` on the 39 changed files; commit messages and this body with `grep -E` | clean | ### Falsifications Everything was committed first. Each guard was broken alone and its test run, then the file was restored with `git checkout -- <file>` and touched, and the test run again. - The Rust mutations were applied with `git apply` inside a checkout of the committed branch, on the Linux build host. - The page's mutations were edits in the worktree, run with local `node --test`. | Guard broken | Mutant | Guard | Mutant result | Restored | |---|---|---|---|---| | send text unsealed (page) | `RemoteViewer.flush` sends `inputPayload(i)` | `node --test remote.test.mjs` | exit 1: "seals keys, text and clipboard text to the host, and sends none in the clear" | 0 (15/15) | | send text unsealed (headless viewer) | `Viewer::input` sends in the clear | `cargo test --test remote_room -- a_remote_room_the_service_keeps` | exit 101: "applied, the seal opened by the host" (refused `input_unsealed`) | 0 | | seal to a viewer's key (page) | the page seals to a key derived from its own principal | `node --test remote.test.mjs` | exit 1: "sealed to the host's sealing key, the screen statement's" | 0 | | seal to a viewer's key (headless viewer) | `Viewer::input` seals to a key of its own | `--test remote_room` | exit 101: "applied, the seal opened by the host" (refused `input_bad_seal`) | 0 | | accept a commitment mismatch | `Input::open` ignores a commitment that differs | `cargo test --features remote_desktop --test remote_desktop` | exit 101: the corpus does not regenerate (`sealed_session` opened: `outcome_mismatch`), stated outcomes differ | 0 | | skip TLS certificate verification | the agent's client config accepts every certificate | `cargo test --test tls` | exit 101: all three tests (untrusted chain, wrong name, wss) | 0 | | remove the clock's bound on the session | `on_events` holds every record | `--test remote_room -- a_session_ends_at_its_record_bound` | exit 101: no `leave` within the step | 0 | | update: remove the bound per principal | `on_events` holds bob's records past his bound | `--test remote_room -- a_flooding_principal_is_cut_at_its_own_bound` | exit 101: "bob alone is cut: []; the session ended: \"max_records\"". Bob's flood filled the total and ended the session for everyone | 0 | | the page's sends out of order (found while running the browser scenario) | `flush` not chained | `node --test remote.test.mjs` | exit 1: "the page's sends keep their order while a seal is made" | 0 | | (Python) each of `sealed_of`, `seal_length`, `rule_unsealed`, `seal_no_key`, `seal_open`, `seal_read`, `seal_commit` dropped | the verifier's own drop list | `remote_desktop_verify.py` | each caught by a vector | — | ### What runs, and how - **wai-rs.** Run on the Linux build host: - `cargo test --locked --no-default-features --features remote_desktop --lib -- remote_desktop:: hpke::` - `cargo test --locked --no-default-features --features remote_desktop --test remote_desktop` - `remote_desktop_verify.py live-conformance/remote` - The corpus is regenerated with `cargo run --no-default-features --features remote_desktop --bin wai_remote_desktop_vectors -- gen ../live-conformance/remote`. - **CI** (`wai-conformance.yml`): the remote desktop step now also runs `hpke::`. Its comments and the paths comment say "four tables". - **joule-moq-remote**: `cargo test --locked --all-targets -- --test-threads=1`. This picks up the new `tests/tls.rs` on its own. CI runs the same on linux-arm64 and macos. - **joule-moq-room**: `cargo test --locked --all-targets`. This includes `tests/remote_room.rs`, now two tests. - **The page's modules**: `node --test web/room/remote.test.mjs`, which is 16 tests. - **The browser scenario** runs on the macOS build host: `BROWSER_BIN=… e2e/run.sh --only remote`. - **Verifying a deployment's session at the opened level**: run the host with `--opening-key <file>`, then `python3 -I remote_desktop_verify.py --room <session.json> --opening-key <hex>`. ### Not done - **Keystroke timing.** The number of sealed actions and their instants stay in the clear, because rules 1 to 8 and the latency records need them. §14 states this. Padding the count, or sending dummy actions, is not done. - **Forward secrecy relies on the host discarding the opening key.** The agent makes a key for each session and keeps it only in memory, unless `--opening-key` writes it. It does not zeroize memory, and it does not rotate the key within a session. The spec allows rotation (a new screen statement with a new key), and both verifiers take several opening keys. - **The session total can still end a session** if five or more principals each act near their own bound at once. That is the safety net doing its job. The bounds are counts of records, not bytes. - **TLS in the browser scenario.** The host agent's TLS is covered by Rust tests (`tests/tls.rs`, and `tests/remote_room.rs` through TLS fronts). The browser scenario runs the agent over `http://` and `ws://` on loopback. - **No deploy**, and no Caddy or site change. - Everything #489 lists as not done, apart from its two items this slice closes: - its "viewers still receive every input record" item (their content is now sealed); - its "no TLS client" item. ### Decisions for the owner 1. **Where the sealing key lives**: as the screen statement's optional last field (described above). The alternative is a grant attribute that the authority signs. That would need the service to take the host's X25519 key when it issues the grant. 2. **What happens at a record bound.** At a principal's own bound, that principal is cut and removed and the session goes on. At the session total, the session ends. Refusing new inputs was not chosen, for the three reasons given above. 3. **The page sends no key, text or clipboard action to a host that states no sealing key**, although the spec allows a host to take them in the clear. 4. **HPKE is shared**: it moved out of `telemetry_aggregate` into a crate module behind the feature `hpke`. 5. **The Python verifier imports `telemetry_aggregate_verify.py` for HPKE** instead of carrying a second AES-GCM. Both are second implementations written from the RFCs, and neither shares code with wai-rs. 6. **`--record-injected`** is a test hook in the binary. It works only with `--input record` and writes counts and digests, never text. It lets the browser scenario check what the host injected without the typed text being written to disk. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- **Closed:** Landed on main through merge train E (#499, merge bf39ec9a); verified by content: this PR's head is in main
The extension text (wai/extensions/remote-desktop.md); the remote_desktop module of wai-rs behind
its own feature: the statements WDS1, WDI1, WDO1, WDF1 and WDL1, the grant attributes
remote_host and remote_control, the host's rules of §5.3 (decide), the floor in effect, latency
legs, and the verifier over a room the live-rooms verifier replays. The corpus
live-conformance/remote/ written by wai_remote_desktop_vectors from fixed seeds: a floor session,
an open session whose control passes by grant exchange, a gap, and refused vectors giving every
reason of both tables of §12. Its second implementation, remote_desktop_verify.py, written from
the text and sharing no code with wai-rs, agrees on every vector, fails every mutated copy and
catches each of its 32 checks dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An agent that reads the screen, or a test harness, states the instant it held the frame verified
against its receipt; no photon is involved, and the record measures the path to the viewer alone.
wai-rs and remote_desktop_verify.py read source 1 to 3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host is its room's clock and its screen's publisher: it makes a room (descriptor and grants,
the host marked remote_host, each participant's control classes in remote_control) and the lines
a relay's configuration takes for it; signs each participant's join when its events track
appears; publishes the screen as the room's screen track, one frame a group with its receipts,
and a screen statement naming the encoder; decides every input by wai-rs's rules at its place in
room-clock order (gate.rs) and injects what they accept on a thread that captures after it
injects, so the outcome it signs names the first frame that can show the input; and passes the
floor on request or when told.

Behind small traits: capture (a pattern source; the macOS display stream; X11 on Linux), encode
(an H.264 encoder with every macroblock I_PCM, a pure function every decoder decodes; the macOS
media framework's hardware encoder, AVC or HEVC, read back as Annex B), and input (a recorder;
the macOS event services; the Linux kernel's input device). Platform API names are in the
adapter files, each line marked for the public-name check.

tests/loopback.rs: a session over a relay that requires grants with their proofs, three viewers,
floor handover, refusals for no control power, no floor and a paused screen, latency records,
and the session's chains verified whole by wai-rs; run with the pure-function encoder, and on
macOS again with the hardware encoder. A workflow runs it on linux-arm64 and macos.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
web/room/remote.js: input statements, floor statements and latency records written byte for byte,
every statement read, the host's rules and the floor replayed over a room's records, the table
from keyboard codes to HID usages; and RemoteViewer, which draws the host's screen (the page's
WebCodecs decoder, AVC or HEVC in Annex B, every frame verified against its receipt first), shows
what the host captures and who holds the floor, batches pointer, keys, composed text and pasted
text into input statements under the grant's classes, asks for and gives back the floor, and signs
a latency record of each applied input's frame at the display refresh that showed it.
remote.test.mjs checks it against wai/live-conformance/remote/, the host agent's key table among
it. The page draws a room with no composition descriptor, and names a screen track by its
namespace.

e2e: the remote scenario runs wai-remote-host with a pattern source (the platform's hardware
encoder on macOS), two pages, the floor asked for and given, a click and a key applied with their
latency records, an input with no control power refused, and the session the host records
verified by remote_desktop_verify.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A participant that joins late fetches what the clock and the host said before it came (live-rooms
§10.6), the host's screen statement among it. `room` writes participant-<name>.page.json, the room
page's test-room file; `run --record` writes the session as a verifier takes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai.live.remote: a corpus member's type named, for clippy's type_complexity
Some checks failed
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
5458920846
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The room service keeps every room as one host's screen shared into it (WAI
remote-desktop): it grants the host (remote_host), a controller (the control
power, by the allow rule) and a viewer, takes the host by its key or by an
invitation its authority signed, hands control over by the host's grant
exchange (its grant and a proof over the service's challenge), watches the
host's screen track and every principal's events, and takes each record in its
chain's order, removing a principal whose record is out of it.

The host agent runs in a room a service keeps (run --room): it asks for its
grant, follows the service's clock, and orders participants' records by the
same rules. The desk (wai-remote-host room) stays, without page files that
carry keys.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
tests/remote_room.rs: the host agent enters by an invitation (a second key
refused), two viewers make their keys, the host hands control over by a grant
exchange with its grant and proof (no proof, another key's proof, and a non-host
refused), a click under the floor applied, a viewer refused input_not_permitted,
the floor handed to a second controller, a record out of its chain's order cut
and its author removed by the clock and by the host, and the session verified by
wai-rs as the clock holds it and as the host holds it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
remote.js no longer reads a key file: RemoteViewer takes the grant the room
service issued for the key the page made, reads its control power from that
grant and from each exchange of it the clock signs, and sends nothing without a
grant, before the clock records it joining, or of a class its grant in effect
does not give. app.js enters a remote room through the service (#join asks to
control, #watch to view) and reads the room's grants again on an exchange. The
browser scenario runs against the room service: the host agent by invitation, a
controller's page and a viewer's, and the sessions the host and the clock hold
verified by remote_desktop_verify.py.

Also the room service's README, example and workflows for [remote].

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The viewer's control power is its grant in effect's while that grant is good,
from its nbf to before its exp (live-rooms §4.3): a page whose grant has ended
holds none and sends nothing. readGrant reads the grant's window.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-room: GET /remote/room bounded per client, as /remote/state is
Some checks failed
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
74cab2ccac
Co-Authored-By: Claude Opus 5.5 <[email protected]>
remote-desktop §7.2/§7.3: a floor statement its rules refuse is recorded, stated
with its reason and changes nothing; a verifier computes the floor without it and
a host gives it no effect, so a release crossing the host's take leaves a room
that verifies. §12 moves floor_not_permitted, floor_not_eligible and
floor_not_holder to a third table of stated reasons; §11.3 states each refused
floor statement.

wai-rs: FloorRefusal and the shared rule remote_desktop::floor_rule; the verifier
states floor_refused. The corpus regenerated: 33 of the 39 earlier vectors are
byte for byte as they were; the six refused-floor vectors keep their rooms and
are now accepted with floor_refused; three whole floor sessions are appended (a
give by a non-holder, a release by a non-holder, a late release).
remote_desktop_verify.py follows the text: states and does not apply a refused
floor statement, reads §12's three tables. remote.js's replay applies §7.2.

joule-moq-room: the clock computes the floor by floor_rule and cuts no one for a
floor statement; GET /remote/room and /remote/state answer only a grant this room
issued with its proof (401 otherwise, 403 for another room's grant). The host's
gate uses floor_rule. The browser scenario reads the session with the host's
grant and proof.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-room: name the floor replay's types; the reader check's refusal boxed
Some checks failed
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
d1e8965f70
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A host states a sealing key (an X25519 public key) as its screen statement's last field. A
controller seals every key, text and clipboard action to it: a sealed action (type 8) keeps in the
clear the type it seals, its dt_us, a clipboard text's length and a salted commitment binding its
fields, its order and its author; the statement ends with one HPKE seal (RFC 9180 base mode,
DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM) of the actions' openings. Pointer, wheel and
touch actions stay in the clear. §5.3 gains rule 8 (input_unsealed) and rule 9 (input_bad_seal),
§12 a fourth table (seal_no_key, seal_not_opened, seal_unreadable, seal_commitment), §11.4 the two
verification levels.

wai-rs: Input::seal, Input::open, commitment, decide_opened, verify_opened; HPKE moves to a crate
module behind its own feature (hpke), which telemetry_aggregate and remote_desktop both use. The
corpus keeps its 42 vectors byte for byte and appends ten sealed ones, each stating the opened
level; remote_desktop_verify.py checks both levels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
remote.js writes and reads sealed actions and seals (remote-desktop §5.5) with WebCrypto's X25519,
HMAC and AES-GCM: HPKE base mode (checked against RFC 9180 A.1.1), the openings and their
commitments. The viewer seals every key, text and clipboard action to the sealing key of the
host's screen in effect and sends none in the clear: to a host that states no sealing key it
sends no key, text or clipboard action. A composed text longer than a text action carries goes as
several. In a remote room the page follows every present principal's events, and shows that a
controller is typing or pasting, never what. Its tests seal every corpus input that opens again,
byte for byte, from what the corpus's opened level states.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host makes an X25519 sealing key for each session and states it in its screen statement; the
gate decides every input by all nine rules of remote-desktop §5.3 (wai-rs's decide_opened),
opening a sealed input with the session's opening key, and the host injects what it seals. The
opening key stays in the session unless --opening-key writes it (mode 0600, never over a file)
for a verifier. The headless viewer seals its key, text and clipboard actions to the screen's
sealing key, keeps the records it receives, and follows another principal's events when told.

The agent reaches the room service over https:// and the relay over wss://: rustls with ring,
the system's roots and any certificate a PEM file names (--ca), every chain and name verified.
tls::front is a TLS front for a plain backend. Lock files gain tokio-rustls and the new edges
alone.

tests/tls.rs: the service and the relay through a TLS front, trusted by a PEM file; refused under
the system's roots alone, and for a certificate that names another host. tests/loopback.rs:
alice's typing sealed, injected as typed, absent from what bob receives; her text in the clear
refused input_unsealed; the session verified at the opened level.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
[remote] max_records (100000 by default, 16 to 10000000): the clock holds at most that many of
a session's principals' records. When a record would pass the bound it holds none past it and
ends the session: every present principal's leave (reason 2) signed after its last record taken,
the host's last; GET /room's remote states ended "max_records", the records held and those over
the bound; the session it holds stays whole and verifies, and the next host starts a new room.
Ending rather than refusing inputs past the bound: a record the clock did not hold would leave a
hole in its author's chain, the host would decide inputs the clock never held, and an input's
refusal is the host's to state. The host agent stops at its own leave.

tests/remote_room.rs: the host reaches the service and the relay through TLS fronts; alice's
typing goes sealed, is injected as typed, and is absent from what bob receives and from the
session the service serves to a viewer's grant; the session verifies at the commitment and the
opened levels; and a session at a bound of 40 records ends, whole and verified.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Alice's page types a string, composes a text and pastes one; each goes sealed to the host's
sealing key. Bob's page, a viewer's that follows her events, shows that she is typing and holds
no trace of what in the records it received, which a positive control finds for a text alice's
page is made to send in the clear, which the host refuses input_unsealed. The host writes its
opening key (--opening-key) and, from its recorder, digests of what it injected
(--record-injected): the keys, the text and the clipboard text alice typed. The sessions the host
and the clock hold verify by remote_desktop_verify.py as a viewer verifies them and under the
opening key, every seal opened to what alice typed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A seal takes WebCrypto's time, so a send that seals could finish after a later one that does not:
a key's release could reach the host before its press. The viewer's sends now run one after
another; what arrives while one is sent goes at the next refresh with its actions' instants as
they were (the batch is no longer re-stamped). A test holds the order while a slow seal is made.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bob now enters before alice types, and what alice's page shows above the canvas grows when he
joins: the click is made where the canvas is on screen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-remote: the opening key in a type no Debug writes out
Some checks failed
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
f96389a387
Config and Report derive Debug; the session's opening key is held as OpeningKey, whose Debug is
OpeningKey(..).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
[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]>
joule-moq-room tests: the flood test states whether the session ended when bob is not cut
Some checks failed
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
e8c747ec83
Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot closed this pull request 2026-10-26 20:59:43 -04:00
Some checks failed
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 9s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 8s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 9s
WAI conformance / does this change touch it (pull_request) Successful in 9s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 9s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!490
No description provided.