@robota-sdk/agent-transport-webrtc 3.0.0-beta.82 → 3.0.0-beta.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +496 -0
  2. package/README.md +105 -0
  3. package/dist/node/index.cjs +1 -1
  4. package/dist/node/index.d.cts +402 -16
  5. package/dist/node/index.d.cts.map +1 -1
  6. package/dist/node/index.d.ts +402 -16
  7. package/dist/node/index.d.ts.map +1 -1
  8. package/dist/node/index.js +1 -1
  9. package/dist/node/index.js.map +1 -1
  10. package/package.json +13 -10
  11. package/src/__tests__/device-mesh-relay.test.ts +439 -0
  12. package/src/__tests__/device-mesh.test.ts +99 -0
  13. package/src/__tests__/discovering-mesh-relay.test.ts +234 -0
  14. package/src/__tests__/enrollment-link.test.ts +103 -0
  15. package/src/__tests__/fake-datachannel.ts +4 -1
  16. package/src/__tests__/mesh-discovery.test.ts +100 -6
  17. package/src/__tests__/mesh-internet.test.ts +160 -4
  18. package/src/__tests__/mesh-turn-relay.test.ts +159 -0
  19. package/src/__tests__/nostr-source-condition.test.ts +27 -0
  20. package/src/__tests__/rtc-channel-close.test.ts +52 -0
  21. package/src/__tests__/rtc-peer-candidates.test.ts +61 -0
  22. package/src/__tests__/turn-server-bind.test.ts +59 -0
  23. package/src/__tests__/turn-server.test.ts +335 -0
  24. package/src/__tests__/turn-test-client.ts +256 -0
  25. package/src/__tests__/webrtc-transport.test.ts +2 -1
  26. package/src/device-mesh-node.ts +294 -14
  27. package/src/discovering-mesh-relay.ts +96 -38
  28. package/src/enrollment-link.ts +465 -0
  29. package/src/index.ts +32 -0
  30. package/src/mesh-dht.ts +140 -23
  31. package/src/mesh-mdns.ts +24 -4
  32. package/src/mesh-peer-link.ts +19 -7
  33. package/src/mesh-records.ts +60 -13
  34. package/src/mesh-turn-relay.ts +238 -0
  35. package/src/rtc-peer.ts +45 -7
  36. package/src/stun-message.ts +341 -0
  37. package/src/turn-server.ts +1028 -0
  38. package/src/webrtc-transport-options.ts +7 -3
  39. package/src/webrtc-transport.ts +5 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,496 @@
1
+ # @robota-sdk/agent-transport-webrtc
2
+
3
+ ## 3.0.0-beta.83
4
+
5
+ ### Minor Changes
6
+
7
+ - 28fa8a7: `/devices add` and `/devices join` enrol a new device into your devices.
8
+
9
+ - On a device that holds the signing key, `/devices add` shows a one-time code on the terminal. It
10
+ works once, for five minutes.
11
+ - On the new device, `/devices join [name]` asks for the code on the terminal, creates the device's
12
+ keys, and meets the other device through the signaling relay in
13
+ `transports.webrtc.options.relayUrl`. Both devices need that setting.
14
+ - The new device proves the code over the WebRTC connection's DTLS fingerprints before anything else
15
+ crosses, so a relay in the middle cannot enrol anyone.
16
+ - Both devices then show the same six digits, and both operators compare them and confirm. Someone
17
+ who saw the code cannot choose the digits: the new device commits to its part before it sees the
18
+ existing device's.
19
+ - Only when both operators say yes does the existing device certify the new one and issue a new
20
+ roster. The new device keeps its identity only once the chain it receives verifies.
21
+ - A wrong, expired or already used code is refused. So is a code after a few failed attempts. When
22
+ either operator declines, nothing is issued or kept.
23
+ - The code appears only on the two terminals. It never reaches history, transcripts or the model. A
24
+ code typed as a command argument is refused. Both commands stay user-only and refuse remote
25
+ surfaces.
26
+ - `agent-remote-pairing`: enrollment codes, the enrollment proof, the signed request with its
27
+ commitment, the short authentication string and the frame decoder.
28
+ - `agent-transport-webrtc`: `dialEnrollment` and `listenForEnrollment` provide a data channel bound to
29
+ the negotiated fingerprints.
30
+
31
+ - 963a4e0: Where no direct path joins two of one user's devices, a TURN relay on one of the user's own devices carries the
32
+ connection.
33
+
34
+ - `agent-transport-webrtc` — `TurnServer`, a pure-JavaScript TURN server over UDP (Allocate, Refresh,
35
+ CreatePermission, ChannelBind, Send/Data indications, ChannelData, long-term credentials) with quotas for
36
+ allocations per owner and in all, relayed bytes per second per owner, and allocation lifetime, and an optional
37
+ relayed-port range for a relay behind a NAT. Only what MESSAGE-INTEGRITY covers is read. Requests nobody has
38
+ authenticated are answered at a limited rate (per source and in all) and never with more bytes than they
39
+ carried, and forwarding into private ranges can be turned off (`allowPrivatePeers`). `MeshTurnRelay` runs it
40
+ for the devices of the roster: each pair derives a short-lived credential of its own (`meshRelayCredential`), a
41
+ device the lists drop or revoke can no longer allocate and loses its allocations. `DeviceMeshNode` takes
42
+ `relays` (the relays paired devices advertise, then configured TURN servers, and `relayOnly`) and
43
+ `relayServer`; a connection that needs a relay and has none is refused with `MeshRelayNeededError`, which says
44
+ why a relay was needed and carries the direct attempt's failure. `MeshDht` publishes this device's relay
45
+ endpoints in the sealed hints records (`relayEndpoints`) and reads the peers' (`relayAdverts`). DTLS stays end
46
+ to end; the relay only forwards it.
47
+ - `agent-remote-pairing` — the pair rendezvous derives a `relay-user` tag and `relayPassword`, the relay
48
+ credential's password for one direction and username.
49
+ - `agent-cli` — `transports.mesh.options` takes `relay` (`serve`, `port`, `host`, `publicAddress`, `relayPorts`,
50
+ `allowPrivatePeers`), `turnServers` and `relayOnly`. When the mesh is on, the session's mesh runs the relay,
51
+ advertises it to paired devices only, and uses the fallback order; `/devices` names the relays, and a device
52
+ that needs a relay is reported once with why. Running a relay, or relay-only without TURN servers, needs the
53
+ DHT or pkarr relays, which carry a relay's address to the other devices; a setting that could not work is
54
+ refused, naming it.
55
+
56
+ ### Patch Changes
57
+
58
+ - 6e4c6ee: Device mesh discovery holds up better against endpoints and peers that misbehave.
59
+
60
+ - A direct path's admission deadline starts only from a `hello`, `offer` or `answer`, so ICE candidates
61
+ that trail an admitted connection no longer set a working path aside.
62
+ - An endpoint or signaling carrier that keeps failing is set aside for longer each time it fails
63
+ again, until a connection over it is admitted.
64
+ - An mDNS lookup keeps listening briefly after the first matching answer, so a faster answer from
65
+ another host cannot hide the peer's own.
66
+ - The device lists read from public records are bounded per paired device, each device's newest
67
+ before any device's next, and the chunks of one list carry a shared version, so a read that finds
68
+ chunks of two versions yields no list. Records written before this change are not read as lists.
69
+ - CLI processes that share `~/.robota/devices/address-cache.json` apply each change to the file as it
70
+ is on disk, so one process no longer overwrites what another learned.
71
+
72
+ - 4241fc5: Device mesh follow-ups.
73
+
74
+ - A device with no identity is pointed to `/devices add` on one of the user's devices and `/devices join`
75
+ here, as well as to `/devices init`. The message says that `init` is for the first device only,
76
+ because it creates a separate identity that can never link to the user's other devices. The `/devices`
77
+ description and the `init` subcommand say the same.
78
+ - An identity created mid-session (`/devices init`, or a successful `/devices join`) opens the mesh
79
+ without a restart when `transports.mesh.enabled` is on.
80
+ - If a session stalls for longer than the mesh lock's stale window (for example while the machine
81
+ sleeps), another session can take the mesh over. The stalled session now notices this on its next
82
+ lock refresh, closes its own mesh, and says why. Two sessions no longer run it together.
83
+ `holdExclusiveFileLock` has a new `onLost` option for this.
84
+ - `/peers` and `/handoff` still list and reach linked mesh devices when local same-host peer discovery
85
+ fails. `/peers` says why sessions on this host are not listed. `ICommandLocalPeersAdapter` has a new
86
+ optional `localDiscoveryOff` field for this.
87
+ - `agent-transport-webrtc`: when lists become newer on a node (reissued, revoked, or adopted from a
88
+ peer), the node sends them over every admitted connection instead of waiting for the next handshake.
89
+ Reissues, revocations and enrolments in the CLI take effect this way at once. A receiver adopts a
90
+ pushed list only if it is newer, issued by this user's signing key, and verifies. The push does not
91
+ depend on the peer's capabilities, and it never reaches the application's message handlers.
92
+
93
+ - 05d5391: The embedded TURN relay says why it cannot listen — the port is taken, the host is not an address of
94
+ this machine, or the port needs privileges — and keeps the bind error as `cause`; the CLI names the
95
+ relay setting that cause asks you to change. Mesh options are no longer checked while
96
+ `transports.mesh.enabled` is off, so a mistake in them does not report that the mesh could not start.
97
+ - 598b180: A WebRTC peer hands out its own ICE candidates only once it holds the remote description, so an answerer can no
98
+ longer reach the offerer while the answer is still being applied and have its certificate refused. A data channel
99
+ closed right after a send no longer loses that send: it reads closed at once and its stream is reset after a
100
+ grace. A LAN probe answered with anything but a valid proof is dropped at once.
101
+ - 57280bf: Every published package now declares `"engines": { "node": ">=22.12.0" }`. Before, 27 of the 38
102
+ packages declared no floor (`agent-core`, `agent-tools` and every provider among them),
103
+ `agent-session` and `agent-file-authority` declared `>=20.19.0`, and the other nine declared
104
+ `>=22.0.0`, so a consumer on Node 20 saw at most a warning from a transitive dependency.
105
+
106
+ Why 22.12: `agent-cli` and `agent-ui-terminal` need Node 22 through `ink` 7, and the CommonJS entries
107
+ of `agent-tools` and its dependents, `agent-transport`/`node` and its dependents, and
108
+ `agent-ui-terminal` `require()` ESM-only dependencies (`p-limit`, `jose`, `chalk`), which Node 22
109
+ supports unflagged only from 22.12. `engines` is advisory unless the consumer enables `engine-strict`.
110
+
111
+ No code changes: `tsdown` now reads `node22.12.0` as its build target from the field.
112
+
113
+ - 18c0d5c: Every published package now exports `./package.json`, so `require('<package>/package.json')` and
114
+ `import('<package>/package.json', { with: { type: 'json' } })` work instead of failing with
115
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`, and each tarball now ships the package's `CHANGELOG.md`.
116
+ - Updated dependencies [724fabb]
117
+ - Updated dependencies [d877de2]
118
+ - Updated dependencies [d61e159]
119
+ - Updated dependencies [bfe8ed5]
120
+ - Updated dependencies [28fa8a7]
121
+ - Updated dependencies [963a4e0]
122
+ - Updated dependencies [7b72344]
123
+ - Updated dependencies [6ae3f28]
124
+ - Updated dependencies [57f57f5]
125
+ - Updated dependencies [ba822c1]
126
+ - Updated dependencies [6e6b06b]
127
+ - Updated dependencies [57280bf]
128
+ - Updated dependencies [18c0d5c]
129
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.83
130
+ - @robota-sdk/agent-transport@3.0.0-beta.83
131
+ - @robota-sdk/agent-remote-pairing@3.0.0-beta.83
132
+ - @robota-sdk/agent-interface-session@3.0.0-beta.83
133
+ - @robota-sdk/agent-interface-session-mobility@3.0.0-beta.83
134
+
135
+ ## 3.0.0-beta.82
136
+
137
+ ### Minor Changes
138
+
139
+ - e15e22b: Two of one user's devices can connect to each other over WebRTC, admitted by the device handshake.
140
+
141
+ - `agent-transport-webrtc` — `DeviceMeshNode` keeps one connection per device pair, in either role: the
142
+ device with the lower id offers and the other answers, so concurrent attempts resolve by rule. Both
143
+ roles bind the handshake to the certificate the DTLS layer verified and take one remote description
144
+ with exactly one fingerprint; before admission only handshake frames cross, and a connection counts
145
+ as admitted only once both sides admitted each other. Each connection gets its own DTLS certificate
146
+ (`createDtlsKeys`). A new attempt never displaces an admitted connection until it is admitted
147
+ itself, and attempts per pair are paced. Lists adopted in a handshake, or handed over through
148
+ `refresh`, apply at once, and a device they revoke loses its connection. Signaling runs through
149
+ `IMeshRelay`: `WsMeshRelayClient` for the self-hosted relay's `presence` / `message` frames
150
+ (topics capped per source and relay-wide), `createInMemoryMeshRelayHub` for tests.
151
+ - `agent-remote-pairing` — the pair's two relay inbox topics, one per direction, come from their
152
+ pairwise secret (`derivePairRendezvous(...).relayInbox()`).
153
+ - `agent-cli` — a device holding the signing key reissues its roster and revocation list before they
154
+ expire while an interactive session runs. The host can open this device's mesh endpoint from the
155
+ identity under `~/.robota/devices`, saving newer lists a peer hands over; no command starts it yet.
156
+ - `agent-command` — `/devices add|join` still reports that enrolment is not available yet.
157
+
158
+ - e15e22b: The Node WebRTC transport runs on `node-datachannel` (libdatachannel, DTLS by OpenSSL). The DTLS stack
159
+ verifies handshake signatures, so a channel binding names the party that holds the certificate's key.
160
+ It is an optional dependency with a prebuilt binary per platform; where it cannot load, the WebRTC
161
+ transport reports itself unavailable instead of falling back to another implementation.
162
+
163
+ - `agent-transport-webrtc` — `RtcPeer` / `RtcChannel` wrap one connection; `loadDataChannel` replaces
164
+ `loadWerift` (and the `loadWerift` option becomes `loadDataChannel`). Every connection has its own DTLS
165
+ certificate, no ICE server is contacted unless configured, and `werift` is no longer a peer dependency.
166
+ A pairing peer that finishes the handshake first and speaks at once no longer has that frame dropped: it
167
+ is held until this side accepts, and discarded if it does not.
168
+ - `agent-cli` — depends on `node-datachannel` (optional) instead of `werift`.
169
+
170
+ - c7f9203: Connected sessions can send each other files.
171
+
172
+ - `/peers send-file <session-id> <path>` sends a copy of any file the operator can read to another
173
+ live session on this host.
174
+ - The model sends a file only through the `peer_send_file` tool. Every call asks the user, showing
175
+ the path, size, hash and destination; no permission mode, rule or remembered consent answers it.
176
+ The tool reaches only files inside the workspace whose path does not look like it holds secrets
177
+ (`.env*`, `~/.ssh`, keys and credentials), and it does not exist in a turn a peer's message started.
178
+ - The receiving operator approves every file. A received file is kept as an inert copy (mode 0600)
179
+ under `~/.robota/peer-files/<sender>/`. It is never run and never placed in the model's context.
180
+ The conversation is told only its name, size and sha256. A name that leaves that directory is
181
+ refused, a symbolic link is never written through, and nothing is overwritten.
182
+ - Transfers travel on a channel of their own (a separate connection on this host, a separate data
183
+ channel between devices), in chunks the receiver paces, up to 32 MiB, and are kept only when the
184
+ whole content matches the offered sha256. A transfer that ends early is discarded; there is no
185
+ resume.
186
+
187
+ **API**
188
+
189
+ - `agent-interface-session-mobility`: the `file` capability, which asks the operator for every
190
+ request; `ConnectionAuthority.authorizeFile`; `IFileOffer` and `IFileFrameChannel`.
191
+ - `agent-transport/node`: `sendFileOverChannel` and `receiveFileOverChannel`, the carrier over any
192
+ `IFileFrameChannel`; `DEFAULT_MAX_FILE_BYTES`.
193
+ - `agent-transport-webrtc`: `IDeviceMeshLink.openFileChannel` and `onFileChannel`.
194
+ - `agent-remote-pairing`: `file` joins `DEVICE_CAPABILITIES`. A device certificate that names it is
195
+ refused as malformed by an earlier version.
196
+ - `agent-core`: `IToolPermissionProfile.notInPeerTurn` withholds a tool from a turn a peer's message
197
+ started.
198
+ - `agent-framework`: `ICommandLocalPeersAdapter.prepareFile`.
199
+
200
+ - 004fe7f: `/handoff` moves a session over a real connection.
201
+
202
+ - `/handoff <session-id>` pushes this conversation to another Robota session on this machine. The
203
+ same carrier also runs between two of the user's devices over their mesh connection; no command
204
+ opens that connection yet.
205
+ - A hand-off is push-only: only the operator of the session that holds it starts one. A session or
206
+ device that asks another for its session is refused.
207
+ - The source signs a grant for that one transfer over that one channel with its device key, so a
208
+ hand-off needs the device identity from `/devices init`. The receiving side checks it against the
209
+ sender's certificate, then asks its own operator; without a yes nothing is sent.
210
+ - The session travels on the file-transfer carrier, is kept aside until it matches the manifest, and
211
+ is saved without being started. The operator there resumes it with `robota --resume <id>`.
212
+ - The source gives the session up, and ends, only once the receiving side confirms it saved it.
213
+ Every other outcome leaves the session where it was; if the confirmation is lost, `/handoff` to the
214
+ same session again resends the same transfer, which the receiver settles without saving it twice.
215
+ Peer attribution (`driverId`, `turnSource`)
216
+ travels with it.
217
+ - `/handoff` stays user-only. Its description tells the model to suggest the command to the user.
218
+ - `agent-transport-webrtc`: an admitted mesh link exposes the DTLS fingerprints it is bound to, and
219
+ `judgeHandoffGrant` is exported.
220
+ - `agent-interface-session-mobility`: a hand-off carrier may move the sealed payload whole
221
+ (`sendPayload`), the destination verifies it with `receivePayload`, and the source can report its
222
+ open transfer (`status`) and abandon it for any refusal the destination names.
223
+
224
+ - 189c21e: The device mesh can find and signal a peer device beyond the local network, through public
225
+ infrastructure that sees only signed ciphertext.
226
+
227
+ - **Rendezvous records (BEP 44):** each device publishes, per peer, its connection hints and the newest
228
+ device revocation lists it holds as mutable items on the BitTorrent Mainline DHT (`bittorrent-dht`,
229
+ directly over UDP), or through pkarr relays (`createPkarrRelayStore`, for clients that cannot reach
230
+ the DHT). Every record is signed by a one-time key of the pair, direction, epoch and purpose, stored
231
+ under a rotating salt, AEAD-sealed and padded to a fixed size; publish times are jittered per pair.
232
+ Lookups read the peer's records at the current and adjacent epochs and reject anything that does not
233
+ verify or open for the pair. Lists too large for one record are split into fixed-size chunks.
234
+ `MeshDht` is the candidate source and publisher, and its `latestLists` feeds the freshness lookup
235
+ before a remote admission with every list candidate found, newest first.
236
+ - **Nostr signaling:** `NostrMeshRelay` carries live SDP/ICE as ephemeral events on several relays,
237
+ under per-epoch, per-direction keys and kinds, with our own AEAD over the payload and no tags.
238
+ - **Order:** `DiscoveringMeshRelay` tries the address cache, mDNS, then DHT records for candidates,
239
+ and when none answers the signaling carriers in order — Nostr, then the self-hosted relay. A
240
+ carrier that carries no admission in time is set aside for the next one.
241
+ - **Defaults:** `DEFAULT_NOSTR_RELAYS` and `DEFAULT_PKARR_RELAYS` list well-known relays of several
242
+ operators; `transports.mesh.options` (`dht`, `pkarrRelays`, `nostrRelays`) replaces them, parsed by
243
+ the CLI's `parseMeshInternetSettings`, and `openDeviceMesh` takes them as `internet`. No command
244
+ starts the mesh yet.
245
+ - `agent-remote-pairing`: `signingSeed`, `sealRecord` and `openRecord` take a record purpose
246
+ (`hints`, the default and unchanged; `revocation`; `signal`), and two tag purposes are added. The
247
+ device handshake's `fetchLatestLists` may return several candidates per list kind; each is verified
248
+ and the newest that verifies counts, so a forged "newer" list cannot hide a real one.
249
+ - `agent-cli` declares the mesh's runtime dependencies (`bittorrent-dht`, `nostr-tools`,
250
+ `multicast-dns`), which its bundle leaves external.
251
+
252
+ **Breaking (pre-release, hence minor):** `IDiscoveringMeshRelayOptions.advertiser` becomes
253
+ `advertisers` (a list).
254
+
255
+ - 5e924ee: Two of one user's devices can find each other on the local network before the relay.
256
+
257
+ - `agent-remote-pairing` — `derivePairRendezvous` derives every place a device pair meets from their
258
+ pairwise secret, separated by direction: rotating tags (hourly epochs, looked up one epoch either
259
+ way), the seed of each epoch's one-time signing key, sealed connection-hint records, and the relay
260
+ inbox topics. It takes the lists in force and refuses a device they do not name with the same
261
+ key-agreement key, or revoke, so a rotated or revoked key stops deriving.
262
+ - `agent-transport-webrtc` — `startLanMeshRelay` / `DiscoveringMeshRelay` look for a peer in the
263
+ address cache, then with mDNS (`MeshMdns`, over `multicast-dns`), then on the self-hosted relay, and
264
+ carry signals to the peer's direct endpoint (`startMeshLanListener`) on rotating pairwise topics,
265
+ sent only as hashes. An endpoint must prove it holds the pair's topic before it carries signals,
266
+ and is set aside when no admission follows. The mDNS announcement names no product, device or
267
+ host, pads its instance count with names that hold for the epoch, and answers queries at a bounded
268
+ rate. Discovery yields candidates only: admission is still the device handshake, and an address is
269
+ remembered only after an admission it carried.
270
+ - `agent-cli` — the device mesh endpoint can look on the local network (`lan` option), remembering
271
+ the addresses that worked in an owner-only `~/.robota/devices/address-cache.json`; no command starts
272
+ it yet.
273
+
274
+ ### Patch Changes
275
+
276
+ - Updated dependencies [e15e22b]
277
+ - Updated dependencies [e15e22b]
278
+ - Updated dependencies [c7f9203]
279
+ - Updated dependencies [004fe7f]
280
+ - Updated dependencies [189c21e]
281
+ - Updated dependencies [5e924ee]
282
+ - @robota-sdk/agent-remote-pairing@3.0.0-beta.82
283
+ - @robota-sdk/agent-interface-session-mobility@3.0.0-beta.82
284
+ - @robota-sdk/agent-transport@3.0.0-beta.82
285
+ - @robota-sdk/agent-interface-session@3.0.0-beta.82
286
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.82
287
+
288
+ ## 3.0.0-beta.81
289
+
290
+ ### Minor Changes
291
+
292
+ - 007fd90: Authority per connection: pairing stays with the local operator, and driving needs the operator's yes.
293
+
294
+ - `agent-interface-session-mobility` — `ConnectionAuthority` decides what one connection may do.
295
+ `presence` and `message` are allowed; `delegate` and `handoff` ask the receiving operator for every
296
+ request; `observe` and `drive` ask once per connection. With no `IOperatorApprover` the answer is
297
+ no. `authorizeDelegation` turns an approved task into a peer turn from where admission placed the
298
+ peer, ignoring anything else on the request, so the receiver's policy decides what it may do.
299
+ - `agent-transport-webrtc` — `connectionApproval` asks the operator before a connection reaches the
300
+ session, after every proof has run. Frames the peer sends meanwhile are held (bounded) and delivered
301
+ only after a yes. A channel that closes first withdraws the question (the approval context carries
302
+ an `AbortSignal`), and a later answer admits nothing. A first-pairing device is pinned for
303
+ reconnect only once it is admitted.
304
+ - `agent-command` — `/remote-control enable` and `revoke` from a connected surface are refused, and
305
+ `status` never shows a connected surface the pairing link.
306
+ - `agent-framework` — the `remote-control-enable` host action runs only for the operator's own
307
+ command, whichever command asked for it.
308
+ - `agent-cli` — every remote-control connection, a returning trusted device included, is put to the
309
+ operator on the host terminal; without an interactive terminal it is refused.
310
+
311
+ ### Patch Changes
312
+
313
+ - Updated dependencies [3038eb7]
314
+ - Updated dependencies [b2e0afe]
315
+ - Updated dependencies [44fc732]
316
+ - Updated dependencies [007fd90]
317
+ - @robota-sdk/agent-interface-session@3.0.0-beta.81
318
+ - @robota-sdk/agent-interface-session-mobility@3.0.0-beta.81
319
+ - @robota-sdk/agent-remote-pairing@3.0.0-beta.81
320
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.81
321
+ - @robota-sdk/agent-transport@3.0.0-beta.81
322
+
323
+ ## 3.0.0-beta.80
324
+
325
+ ### Major Changes
326
+
327
+ - e82215f: **ARCH-011: replace the ambiguous transport lifecycle stub with executable conformance.**
328
+
329
+ `ITransportAdapter` now requires a frozen `service | runner` lifecycle descriptor. `start()` resolves
330
+ at the concrete transport's documented readiness boundary; start before attach and repeated active
331
+ start reject a stable lifecycle error, repeated stop is safe, and stopped adapters can reattach and
332
+ restart.
333
+
334
+ Runner adapters launch separately and expose a typed terminal outcome through
335
+ `waitForCompletion()`. The registry accepts base adapters, rejects duplicate names, keeps
336
+ configuration as an optional capability, returns complete ordered records whose pending slots become
337
+ registry-owned `abandoned` outcomes on stop/rollback, and exposes a real-runner-only first-failure
338
+ wait. It serializes startup/stop, rejects active restart before mutation, and reverses partial startup
339
+ from the currently failing adapter with typed safe rollback details. Runtime host and serve mode
340
+ propagate real nonzero runner results without treating normal shutdown abandonment as failure.
341
+
342
+ HTTP, MCP, both WebSocket adapters, WebRTC, and headless invoke one shared public conformance kit.
343
+ The former `TuiTransport` export is removed because it ignored the attached session; use `renderApp`
344
+ or `TuiInteractionChannel`, which honestly own their session lifecycle.
345
+
346
+ ### Minor Changes
347
+
348
+ - 9db63ee: Add named session capability roles and explicit capability-host queries while preserving the legacy
349
+ `IInteractiveSession` interface shape. HTTP, MCP, protocol, WS, WebRTC, and headless transports now
350
+ declare only the session roles they consume, and the direct aggregate-cast floor is zero.
351
+ - 2ebff01: Emit the complete persisted checkpoint and branch lifecycle, forward plan, context-refresh, and
352
+ branch events through protocol transports, and render deterministic bounded notices in the TUI.
353
+ Transport-owned delivery failures now enter the owning carrier cleanup lifecycle without reversing
354
+ an already-committed session operation.
355
+ - 94cc355: REMOTE-002 Stage A: extract the transport-neutral session bridge + wire protocol into a new
356
+ `@robota-sdk/agent-transport-protocol` package (`createWsHandler`, `TClientMessage`/`TServerMessage`); repoint
357
+ `agent-transport-ws` and `agent-web-ui` at it (no pass-through re-export). Add a new
358
+ `@robota-sdk/agent-transport-webrtc` package: a `WebRtcTransport` (`IConfigurableTransport`, `defaultEnabled:false`)
359
+ that carries an `IInteractiveSession` over an `RTCDataChannel` reusing the shared handler, with a lazily-loaded
360
+ optional `werift` peer dependency that throws an explicit "WebRTC transport unavailable" error on absence (never a
361
+ silent no-op). No user-facing enable path and no auth in Stage A (that lands in Stage B); the transport is proven
362
+ by an in-process loopback data-channel round-trip only.
363
+
364
+ (Bump target corrected during REL-023 triage: `@robota-sdk/agent-web-ui` was dissolved by GUI-006 (#1141, 2026-07-12) before this work was ever published; its protocol-consumer role now lives in `@robota-sdk/agent-transport-webrtc-web`.)
365
+
366
+ - 2c69a3f: REMOTE-004 Stage B2: production WebRTC signaling + relay abuse-hardening (still no user-facing enable path).
367
+
368
+ - Add `WsSignalingClient` — a production `ISignalingClient` over a `ws` socket to the `@robota-sdk/remote-signaling`
369
+ relay (Node host-side): joins a rendezvous, buffers signals produced before the socket opens and flushes them,
370
+ and surfaces relay/socket errors through an explicit `onError` (no silent degrade). An `onReady` callback fires
371
+ once the rendezvous join is confirmed.
372
+ - Expose opt-in `forceTurn` on `IWebRtcTransportOptions` (relay-only ICE) as defense-in-depth.
373
+ - The private `@robota-sdk/remote-signaling` relay is hardened in-layer (safe by default): a per-source
374
+ token-bucket bounds join floods, rendezvous ids are single-use (a distinct third peer is refused for the id's
375
+ lifetime, even after one of the pair leaves), a half-open rendezvous expires after a TTL, and concurrent
376
+ rendezvous are capped — all with injected clock/scheduler for deterministic tests.
377
+ - `CVE-2024-29415` (werift-transitive `ip` SSRF) is discharged as a reviewed re-accept: werift never calls the
378
+ vulnerable `ip.isPublic`/`isPrivate`/`address` (verified + guarded by a regression test), so the
379
+ `ignoreCves` entry is retained with a documented non-reachability rationale.
380
+
381
+ - 92bf33e: Add the pairing gate to the WebRTC transport (REMOTE-008 Step 1, security milestone). When a pairing
382
+ `secret` is configured, the data channel is phase-separated: pre-accept it carries only pairing frames
383
+ (routed to the directional-HMAC handshake bound to the DTLS fingerprints; any non-pairing frame is
384
+ dropped), and only after the handshake accepts is the session bridge built — fail closed on
385
+ mismatch/timeout (channel closed, session never exposed). Without a `secret` the channel is exposed
386
+ immediately, unchanged. Introduces a dependency on the zero-dep `@robota-sdk/agent-remote-pairing` leaf
387
+ (the gate must live where the SDP fingerprints and channel frames are visible).
388
+ - 9f602df: Add user-supplied TURN fallback for remote control (REMOTE-010 / Stage E1) so P2P works behind symmetric
389
+ NAT / restrictive firewalls. The host reads + validates `transports.webrtc.options.iceServers`/`forceTurn`
390
+ at the agent-cli composition root (a fail-closed validator narrowing the untyped value → `IIceServer[]`;
391
+ `IWebRtcTransportOptions.iceServers` widened to carry TURN `username`/`credential`), and the browser reads a
392
+ validated `ice`/`forceTurn` pairing-URL query param (fail-closed decoder for the attacker-influenceable value;
393
+ `forceTurn` → `iceTransportPolicy: 'relay'`) — both threaded into their `RTCPeerConnection`. `forceTurn` without
394
+ a TURN server fails closed (else ICE gathers no candidates and silently never connects). Absent ICE config ⇒
395
+ host-candidate-only, unchanged.
396
+
397
+ (Bump target corrected during REL-023 triage: `@robota-sdk/agent-web-ui` was dissolved by GUI-006 (#1141, 2026-07-12) before this work was ever published; the browser-side ICE/`forceTurn` handling described here now lives in `@robota-sdk/agent-transport-webrtc-web`.)
398
+
399
+ ### Patch Changes
400
+
401
+ - 235da81: **BREAKING — ARCH-030: `createWsHandler` takes the carrier's delivery boundary, not a raw `send`.**
402
+
403
+ `createWsHandler` had two outbound semantics on one connection. The session-event fan-out went through
404
+ a guard that reported carrier failures through `onDeliveryError`; every reply to an inbound frame got
405
+ the raw `send`. Eleven reply families were unguarded — five resolving from a Promise continuation, so a
406
+ reply landing after a disconnect escaped as an **unhandled rejection** while the carrier's cleanup was
407
+ never notified, and six synchronous ones that threw into the carrier's inbound listener instead.
408
+
409
+ `IWsHandlerOptions` now takes a single `deliver: TOutboundDeliver` in place of `send` and
410
+ `onDeliveryError`. **The carrier builds the boundary** from its own sink and its own failure policy and
411
+ passes it down — not the reverse, because a protocol layer handed a raw sink so it can hand a wrapper
412
+ back leaves the raw sink reachable, which is how the twelfth reply family gets added unguarded.
413
+
414
+ ```ts
415
+ // before
416
+ const { onMessage, cleanup } = createWsHandler({
417
+ session,
418
+ send: (msg) => ws.send(JSON.stringify(msg)),
419
+ onDeliveryError: (error) => ws.close(1011, error.message),
420
+ });
421
+
422
+ // after
423
+ const deliver = createOutboundDelivery(
424
+ (msg) => ws.send(JSON.stringify(msg)),
425
+ (error) => ws.close(1011, error.message),
426
+ );
427
+ const { onMessage, cleanup } = createWsHandler({ session, deliver });
428
+ ```
429
+
430
+ `TOutboundDeliver` is branded and `createOutboundDelivery` is its only producer, so a plain
431
+ `(message: TServerMessage) => void` is refused by the compiler wherever a boundary is required.
432
+
433
+ **The boundary latches:** it reports at most one delivery failure per connection, after which frames are
434
+ dropped without a further report. All three carriers already treated a delivery failure as terminal and
435
+ each had grown its own latch; it belongs upstream of all three. `SessionResumeBridge` builds a fresh
436
+ boundary per `attach`, which is what un-latches the session after a reconnect, and buffers a frame
437
+ before the boundary so a dropped one still replays.
438
+
439
+ **`ISubscribeSessionEventsOptions` is no longer exported** from the package barrel. It is the options bag
440
+ of `subscribeSessionEvents`, which is package-internal, and it was already absent from the SPEC's public
441
+ API table. Its `onDeliveryError` member is gone regardless — carrier-failure containment is the
442
+ boundary's job now.
443
+
444
+ `agent-transport-ws` and `agent-transport-webrtc` are `patch`: `WsSessionDelivery` (whose raw `send` is
445
+ now private, with `deliver` the only public sink) and `PairingGate` are not on their packages' barrels,
446
+ and every barrel export of both packages keeps its signature.
447
+
448
+ - 4f3c075: Assemble complete, verified package generations before switching build output; preserve the previous generation on build failure and pack only verified regular-file images. Include copied CLI web assets in affected-build ordering and artifact transfer. Preserve the CLI version in managed build paths. Public runtime contracts remain compatible (patch).
449
+ - 2db1b97: Remote-control pairing binds to the negotiated DTLS certificate.
450
+
451
+ - The Node host reads the remote fingerprint from the certificate the DTLS layer verified, not from the answer's
452
+ SDP text, and builds the pairing gate once the DTLS handshake completes.
453
+ - An SDP must advertise exactly one DTLS fingerprint. `extractDtlsFingerprint` now throws when two different
454
+ fingerprints are present, and `extractDtlsFingerprintAttribute` returns the algorithm with the value.
455
+ - A start takes one answer (host) and a connection takes one offer (browser client); a later description is
456
+ ignored.
457
+
458
+ - Updated dependencies [37b4bd7]
459
+ - Updated dependencies [50d2c9f]
460
+ - Updated dependencies [a5961c9]
461
+ - Updated dependencies [4dd45cc]
462
+ - Updated dependencies [040f31f]
463
+ - Updated dependencies [4c5148e]
464
+ - Updated dependencies [0116a29]
465
+ - Updated dependencies [e82215f]
466
+ - Updated dependencies [52b7346]
467
+ - Updated dependencies [9db63ee]
468
+ - Updated dependencies [b078afa]
469
+ - Updated dependencies [2ebff01]
470
+ - Updated dependencies [9db63ee]
471
+ - Updated dependencies [2ebff01]
472
+ - Updated dependencies [4772067]
473
+ - Updated dependencies [0f98419]
474
+ - Updated dependencies [64ba748]
475
+ - Updated dependencies [d312755]
476
+ - Updated dependencies [3244fb8]
477
+ - Updated dependencies [1e3f91a]
478
+ - Updated dependencies [4f3c075]
479
+ - Updated dependencies [1e40b5b]
480
+ - Updated dependencies [7669851]
481
+ - Updated dependencies [4b76cfa]
482
+ - Updated dependencies [2db1b97]
483
+ - Updated dependencies [07b627f]
484
+ - Updated dependencies [1f45110]
485
+ - Updated dependencies [9665c6e]
486
+ - Updated dependencies [44393be]
487
+ - Updated dependencies [c7fa299]
488
+ - Updated dependencies [5134b3b]
489
+ - Updated dependencies [833afe1]
490
+ - Updated dependencies [5c5ff23]
491
+ - Updated dependencies [9814afc]
492
+ - @robota-sdk/agent-interface-session@3.0.0-beta.80
493
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.80
494
+ - @robota-sdk/agent-transport@3.0.0-beta.80
495
+ - @robota-sdk/agent-interface-session-mobility@3.0.0-beta.80
496
+ - @robota-sdk/agent-remote-pairing@3.0.0-beta.80
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @robota-sdk/agent-transport-webrtc
2
+
3
+ WebRTC peer-to-peer transport for the Robota SDK (Node.js). `WebRtcTransport` carries a Robota
4
+ session over an `RTCDataChannel`, so a remote client can drive a live session directly, peer to
5
+ peer, without session content passing through a server. It reuses the transport-neutral session
6
+ bridge and wire protocol from `@robota-sdk/agent-transport`, the same ones the WebSocket transport
7
+ uses. The package also contains the building blocks for connecting one user's devices to each other
8
+ (the device mesh): peer discovery and signaling, device enrollment and a TURN relay.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @robota-sdk/agent-transport-webrtc @robota-sdk/agent-transport node-datachannel
14
+ ```
15
+
16
+ Requires Node.js 22 or later. The WebRTC implementation, `node-datachannel` (libdatachannel, a
17
+ native module), is an optional peer dependency loaded lazily. If it is not installed or has no
18
+ prebuilt binary for the platform, `start()` fails with an explicit `WebRTC transport unavailable`
19
+ error; there is no fallback implementation. `@robota-sdk/agent-transport` provides the
20
+ `IProtocolSession` type used below.
21
+
22
+ ## Usage
23
+
24
+ ```typescript
25
+ import { WebRtcTransport, WsSignalingClient } from '@robota-sdk/agent-transport-webrtc';
26
+ import type { IProtocolSession } from '@robota-sdk/agent-transport';
27
+
28
+ declare const session: IProtocolSession; // e.g. a live interactive session
29
+ declare const pairingSecret: string; // shared with the remote client out of band
30
+
31
+ const signaling = new WsSignalingClient({
32
+ url: 'wss://signaling.example.com',
33
+ rendezvous: 'my-rendezvous-id',
34
+ onError: (error) => console.error(error),
35
+ });
36
+
37
+ const transport = new WebRtcTransport({
38
+ signaling,
39
+ secret: pairingSecret,
40
+ onPaired: () => console.log('remote client paired'),
41
+ onPairingFailed: () => console.log('pairing failed; channel closed'),
42
+ });
43
+
44
+ transport.attach(session);
45
+ await transport.start(); // the host is the offerer: creates the peer and data channel, sends the offer
46
+ // ...
47
+ await transport.stop();
48
+ ```
49
+
50
+ Admission is decided at construction. With `secret`, the data channel carries only pairing frames
51
+ until the pairing handshake from `@robota-sdk/agent-remote-pairing` accepts, bound to the DTLS
52
+ fingerprints of the connection; only then is the session exposed, and a mismatch or timeout closes
53
+ the channel. To run without pairing (for example over loopback in tests), pass
54
+ `{ open: true, openReason: '<why this is safe>' }` instead. The constructor throws when neither is
55
+ given, or when both are.
56
+
57
+ Signaling is an injected port (`ISignalingClient`) that only carries SDP offers/answers and ICE
58
+ candidates, never session content. `createInMemorySignalingPair()` wires two in-process clients
59
+ together for tests; `WsSignalingClient` joins a rendezvous on a WebSocket signaling relay.
60
+
61
+ ## `WebRtcTransport` options
62
+
63
+ | Option | Type | Description |
64
+ | ------------------------------------------------------------------------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
65
+ | `signaling` | `ISignalingClient` | Required. Exchanges SDP/ICE with the remote peer. |
66
+ | `secret` | `string` | Pairing secret; gates the data channel until the pairing handshake accepts. |
67
+ | `open` / `openReason` | `boolean` / `string` | Runs without a pairing gate; `openReason` must say why. |
68
+ | `iceServers` | `readonly IIceServer[]` | STUN/TURN servers. Omitted: host candidates only. |
69
+ | `forceTurn` | `boolean` | Restricts ICE to relay (TURN) candidates; needs a TURN server in `iceServers`. |
70
+ | `onPaired` / `onPairingFailed` | callbacks | Called when pairing accepts (session exposed) or rejects/times out (channel closed). |
71
+ | `reconnect` | `IHostReconnectConfig` | With `secret`: admits either a first pairing (with device enrollment) or a pinned-device reconnect. |
72
+ | `connectionApproval` | `IConnectionApproval` | With `secret`: asks an operator to approve each connection before it reaches the session. |
73
+ | `localPeer` | `ILocalPeerProof` | With `secret`: also requires a nonce issued at a guarded local rendezvous. |
74
+ | `resumeBridge` / `onDropped` | `SessionResumeBridge` / callback | With `secret`: `resumeBridge` carries the paired session across channel drops; `onDropped` is called when a paired channel drops. |
75
+ | `onDeliveryError` | callback | Observes an outbound delivery failure before the channel is dropped. |
76
+ | `personalUsageReporter` / `usageReporter` / `storedSessionUsageReporter` | reporters | Host-owned usage read models, available to the peer only after admission. |
77
+
78
+ `start()` before `attach()` throws; `stop()` is safe to call more than once, and starting again
79
+ requires attaching again.
80
+
81
+ ## Other exports
82
+
83
+ - Peers: `RtcPeer`, `RtcChannel` and `loadDataChannel`, one peer connection over `node-datachannel`.
84
+ - Device mesh: `DeviceMeshNode` connects one user's devices, admitted by the device handshake.
85
+ Discovery and signaling for it: `DiscoveringMeshRelay`, `MeshMdns`, `MeshDht`, `NostrMeshRelay`,
86
+ `WsMeshRelayClient`, `startLanMeshRelay`.
87
+ - Relay: `TurnServer` and `MeshTurnRelay`, a TURN relay one of the user's devices can run for its
88
+ paired devices.
89
+ - Enrollment: `dialEnrollment` and `listenForEnrollment` open the channel used to enroll a new
90
+ device.
91
+
92
+ ## Related packages
93
+
94
+ - [`@robota-sdk/agent-transport`](../agent-transport/README.md): the transport-neutral session
95
+ bridge and wire protocol this transport carries.
96
+ - [`@robota-sdk/agent-remote-pairing`](../agent-remote-pairing/README.md): the pairing handshake and
97
+ DTLS-fingerprint channel binding the pairing gate uses.
98
+ - [`@robota-sdk/agent-transport-ws`](../agent-transport-ws/README.md): the WebSocket transport for
99
+ the same protocol.
100
+
101
+ See [docs/SPEC.md](docs/SPEC.md) for the package contract.
102
+
103
+ ## License
104
+
105
+ Robota is dual-licensed under the [GNU AGPL-3.0](../../LICENSE) or a [commercial license](../../COMMERCIAL.md). See [LICENSING.md](../../LICENSING.md).