Import protocol crate from `session-sharing-server` repo. (#1)

This yanks the `session-sharing-protocol` crate from `https://github.com/warpdotdev/session-sharing-server` into this dedicated repository. The client compiles against this protocol, so when we open-source the client, we'll need to make the protocol code public too. We don't want to make the server code public, though, so they need to be in separate repos (hence this PR). Once this merges, I'll send out a pair of follow-up PRs to update the client repo to point at the crate here, and the server repo to remove the crate from there and instead point at it here.

David Stern committed Apr 19, 2026 at 12:49 UTC 3d803708cdd249386d585ec4b9064e86dc179165
28 files changed +3494
.github/dependabot.yml new
+40
@@ -0,0 +1,40 @@
1 +# Please see the documentation for all configuration options:
2 +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
3 +
4 +version: 2
5 +updates:
6 + - package-ecosystem: "cargo"
7 + directory: "/"
8 + schedule:
9 + interval: "daily"
10 + reviewers:
11 + - "warpdotdev/tech-leads"
12 + # Only send security updates, not general version updates.
13 + open-pull-requests-limit: 0
14 + - package-ecosystem: "github-actions"
15 + directory: "/"
16 + schedule:
17 + interval: "daily"
18 + reviewers:
19 + - "warpdotdev/tech-leads"
20 + cooldown:
21 + # Don't update to any action release that is less than two weeks old.
22 + default-days: 14
23 + groups:
24 + # Group all non-major updates of official actions together - they're lower-risk.
25 + official-actions:
26 + applies-to: version-updates
27 + patterns:
28 + - "actions/*"
29 + update-types:
30 + - "minor"
31 + - "patch"
32 + # Group all non-major updates of Namespace actions together - they're lower-risk.
33 + namespace-actions:
34 + applies-to: version-updates
35 + patterns:
36 + - "namespacelabs/*"
37 + - "namespace-actions/*"
38 + update-types:
39 + - "minor"
40 + - "patch"
.github/workflows/ci.yml new
+36
@@ -0,0 +1,36 @@
1 +name: CI
2 +
3 +on:
4 + push:
5 + branches: [ "main" ]
6 + pull_request:
7 + branches: [ "main" ]
8 +
9 +env:
10 + CARGO_TERM_COLOR: always
11 +
12 +jobs:
13 + ci:
14 + name: CI
15 + runs-on: ubuntu-latest
16 + steps:
17 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
18 +
19 + - name: Setup SSH keys for private crates
20 + uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
21 + with:
22 + ssh-private-key: ${{ secrets.WARP_MACHINE_USER_SSH_PRIVATE_KEY }}
23 +
24 + - name: Run cargo fmt
25 + run: cargo fmt --check
26 +
27 + - name: Run cargo clippy
28 + run: cargo clippy --all-targets --all-features -- -D warnings
29 +
30 + - name: Install cargo nextest
31 + uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18
32 + with:
33 + tool: nextest
34 +
35 + - name: Run tests
36 + run: cargo nextest run --workspace --no-tests=warn
.gitignore new
+7
@@ -0,0 +1,7 @@
1 +/target
2 +
3 +# IDE things
4 +.idea/*
5 +
6 +# DS_STORE
7 +.DS_STORE
Cargo.lock new
+905
@@ -0,0 +1,905 @@
1 +# This file is automatically @generated by Cargo.
2 +# It is not intended for manual editing.
3 +version = 4
4 +
5 +[[package]]
6 +name = "ahash"
7 +version = "0.7.8"
8 +source = "registry+https://github.com/rust-lang/crates.io-index"
9 +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
10 +dependencies = [
11 + "getrandom 0.2.17",
12 + "once_cell",
13 + "version_check",
14 +]
15 +
16 +[[package]]
17 +name = "anyhow"
18 +version = "1.0.102"
19 +source = "registry+https://github.com/rust-lang/crates.io-index"
20 +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
21 +
22 +[[package]]
23 +name = "arrayvec"
24 +version = "0.7.6"
25 +source = "registry+https://github.com/rust-lang/crates.io-index"
26 +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
27 +
28 +[[package]]
29 +name = "autocfg"
30 +version = "1.5.0"
31 +source = "registry+https://github.com/rust-lang/crates.io-index"
32 +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
33 +
34 +[[package]]
35 +name = "bitflags"
36 +version = "2.11.1"
37 +source = "registry+https://github.com/rust-lang/crates.io-index"
38 +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
39 +
40 +[[package]]
41 +name = "bitvec"
42 +version = "1.0.1"
43 +source = "registry+https://github.com/rust-lang/crates.io-index"
44 +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
45 +dependencies = [
46 + "funty",
47 + "radium",
48 + "tap",
49 + "wyz",
50 +]
51 +
52 +[[package]]
53 +name = "borsh"
54 +version = "1.6.1"
55 +source = "registry+https://github.com/rust-lang/crates.io-index"
56 +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
57 +dependencies = [
58 + "borsh-derive",
59 + "bytes",
60 + "cfg_aliases",
61 +]
62 +
63 +[[package]]
64 +name = "borsh-derive"
65 +version = "1.6.1"
66 +source = "registry+https://github.com/rust-lang/crates.io-index"
67 +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
68 +dependencies = [
69 + "once_cell",
70 + "proc-macro-crate",
71 + "proc-macro2",
72 + "quote",
73 + "syn 2.0.117",
74 +]
75 +
76 +[[package]]
77 +name = "bumpalo"
78 +version = "3.20.2"
79 +source = "registry+https://github.com/rust-lang/crates.io-index"
80 +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
81 +
82 +[[package]]
83 +name = "byte-unit"
84 +version = "5.2.0"
85 +source = "registry+https://github.com/rust-lang/crates.io-index"
86 +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d"
87 +dependencies = [
88 + "rust_decimal",
89 + "schemars",
90 + "serde",
91 + "utf8-width",
92 +]
93 +
94 +[[package]]
95 +name = "bytecheck"
96 +version = "0.6.12"
97 +source = "registry+https://github.com/rust-lang/crates.io-index"
98 +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
99 +dependencies = [
100 + "bytecheck_derive",
101 + "ptr_meta",
102 + "simdutf8",
103 +]
104 +
105 +[[package]]
106 +name = "bytecheck_derive"
107 +version = "0.6.12"
108 +source = "registry+https://github.com/rust-lang/crates.io-index"
109 +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
110 +dependencies = [
111 + "proc-macro2",
112 + "quote",
113 + "syn 1.0.109",
114 +]
115 +
116 +[[package]]
117 +name = "bytes"
118 +version = "1.11.1"
119 +source = "registry+https://github.com/rust-lang/crates.io-index"
120 +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
121 +
122 +[[package]]
123 +name = "cfg-if"
124 +version = "1.0.4"
125 +source = "registry+https://github.com/rust-lang/crates.io-index"
126 +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
127 +
128 +[[package]]
129 +name = "cfg_aliases"
130 +version = "0.2.1"
131 +source = "registry+https://github.com/rust-lang/crates.io-index"
132 +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
133 +
134 +[[package]]
135 +name = "dyn-clone"
136 +version = "1.0.20"
137 +source = "registry+https://github.com/rust-lang/crates.io-index"
138 +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
139 +
140 +[[package]]
141 +name = "equivalent"
142 +version = "1.0.2"
143 +source = "registry+https://github.com/rust-lang/crates.io-index"
144 +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
145 +
146 +[[package]]
147 +name = "foldhash"
148 +version = "0.1.5"
149 +source = "registry+https://github.com/rust-lang/crates.io-index"
150 +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
151 +
152 +[[package]]
153 +name = "funty"
154 +version = "2.0.0"
155 +source = "registry+https://github.com/rust-lang/crates.io-index"
156 +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
157 +
158 +[[package]]
159 +name = "getrandom"
160 +version = "0.2.17"
161 +source = "registry+https://github.com/rust-lang/crates.io-index"
162 +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
163 +dependencies = [
164 + "cfg-if",
165 + "libc",
166 + "wasi",
167 +]
168 +
169 +[[package]]
170 +name = "getrandom"
171 +version = "0.4.2"
172 +source = "registry+https://github.com/rust-lang/crates.io-index"
173 +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
174 +dependencies = [
175 + "cfg-if",
176 + "libc",
177 + "r-efi",
178 + "wasip2",
179 + "wasip3",
180 +]
181 +
182 +[[package]]
183 +name = "hashbrown"
184 +version = "0.12.3"
185 +source = "registry+https://github.com/rust-lang/crates.io-index"
186 +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
187 +dependencies = [
188 + "ahash",
189 +]
190 +
191 +[[package]]
192 +name = "hashbrown"
193 +version = "0.15.5"
194 +source = "registry+https://github.com/rust-lang/crates.io-index"
195 +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
196 +dependencies = [
197 + "foldhash",
198 +]
199 +
200 +[[package]]
201 +name = "hashbrown"
202 +version = "0.17.0"
203 +source = "registry+https://github.com/rust-lang/crates.io-index"
204 +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
205 +
206 +[[package]]
207 +name = "heck"
208 +version = "0.5.0"
209 +source = "registry+https://github.com/rust-lang/crates.io-index"
210 +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
211 +
212 +[[package]]
213 +name = "id-arena"
214 +version = "2.3.0"
215 +source = "registry+https://github.com/rust-lang/crates.io-index"
216 +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
217 +
218 +[[package]]
219 +name = "indexmap"
220 +version = "2.14.0"
221 +source = "registry+https://github.com/rust-lang/crates.io-index"
222 +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
223 +dependencies = [
224 + "equivalent",
225 + "hashbrown 0.17.0",
226 + "serde",
227 + "serde_core",
228 +]
229 +
230 +[[package]]
231 +name = "itoa"
232 +version = "1.0.18"
233 +source = "registry+https://github.com/rust-lang/crates.io-index"
234 +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
235 +
236 +[[package]]
237 +name = "js-sys"
238 +version = "0.3.95"
239 +source = "registry+https://github.com/rust-lang/crates.io-index"
240 +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
241 +dependencies = [
242 + "once_cell",
243 + "wasm-bindgen",
244 +]
245 +
246 +[[package]]
247 +name = "leb128fmt"
248 +version = "0.1.0"
249 +source = "registry+https://github.com/rust-lang/crates.io-index"
250 +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
251 +
252 +[[package]]
253 +name = "libc"
254 +version = "0.2.185"
255 +source = "registry+https://github.com/rust-lang/crates.io-index"
256 +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
257 +
258 +[[package]]
259 +name = "log"
260 +version = "0.4.29"
261 +source = "registry+https://github.com/rust-lang/crates.io-index"
262 +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
263 +
264 +[[package]]
265 +name = "memchr"
266 +version = "2.8.0"
267 +source = "registry+https://github.com/rust-lang/crates.io-index"
268 +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
269 +
270 +[[package]]
271 +name = "num-traits"
272 +version = "0.2.19"
273 +source = "registry+https://github.com/rust-lang/crates.io-index"
274 +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
275 +dependencies = [
276 + "autocfg",
277 +]
278 +
279 +[[package]]
280 +name = "once_cell"
281 +version = "1.21.4"
282 +source = "registry+https://github.com/rust-lang/crates.io-index"
283 +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
284 +
285 +[[package]]
286 +name = "ppv-lite86"
287 +version = "0.2.21"
288 +source = "registry+https://github.com/rust-lang/crates.io-index"
289 +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
290 +dependencies = [
291 + "zerocopy",
292 +]
293 +
294 +[[package]]
295 +name = "prettyplease"
296 +version = "0.2.37"
297 +source = "registry+https://github.com/rust-lang/crates.io-index"
298 +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
299 +dependencies = [
300 + "proc-macro2",
301 + "syn 2.0.117",
302 +]
303 +
304 +[[package]]
305 +name = "proc-macro-crate"
306 +version = "3.5.0"
307 +source = "registry+https://github.com/rust-lang/crates.io-index"
308 +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
309 +dependencies = [
310 + "toml_edit",
311 +]
312 +
313 +[[package]]
314 +name = "proc-macro2"
315 +version = "1.0.106"
316 +source = "registry+https://github.com/rust-lang/crates.io-index"
317 +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
318 +dependencies = [
319 + "unicode-ident",
320 +]
321 +
322 +[[package]]
323 +name = "ptr_meta"
324 +version = "0.1.4"
325 +source = "registry+https://github.com/rust-lang/crates.io-index"
326 +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
327 +dependencies = [
328 + "ptr_meta_derive",
329 +]
330 +
331 +[[package]]
332 +name = "ptr_meta_derive"
333 +version = "0.1.4"
334 +source = "registry+https://github.com/rust-lang/crates.io-index"
335 +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
336 +dependencies = [
337 + "proc-macro2",
338 + "quote",
339 + "syn 1.0.109",
340 +]
341 +
342 +[[package]]
343 +name = "quote"
344 +version = "1.0.45"
345 +source = "registry+https://github.com/rust-lang/crates.io-index"
346 +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
347 +dependencies = [
348 + "proc-macro2",
349 +]
350 +
351 +[[package]]
352 +name = "r-efi"
353 +version = "6.0.0"
354 +source = "registry+https://github.com/rust-lang/crates.io-index"
355 +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
356 +
357 +[[package]]
358 +name = "radium"
359 +version = "0.7.0"
360 +source = "registry+https://github.com/rust-lang/crates.io-index"
361 +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
362 +
363 +[[package]]
364 +name = "rand"
365 +version = "0.8.6"
366 +source = "registry+https://github.com/rust-lang/crates.io-index"
367 +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
368 +dependencies = [
369 + "libc",
370 + "rand_chacha",
371 + "rand_core",
372 +]
373 +
374 +[[package]]
375 +name = "rand_chacha"
376 +version = "0.3.1"
377 +source = "registry+https://github.com/rust-lang/crates.io-index"
378 +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
379 +dependencies = [
380 + "ppv-lite86",
381 + "rand_core",
382 +]
383 +
384 +[[package]]
385 +name = "rand_core"
386 +version = "0.6.4"
387 +source = "registry+https://github.com/rust-lang/crates.io-index"
388 +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
389 +dependencies = [
390 + "getrandom 0.2.17",
391 +]
392 +
393 +[[package]]
394 +name = "ref-cast"
395 +version = "1.0.25"
396 +source = "registry+https://github.com/rust-lang/crates.io-index"
397 +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
398 +dependencies = [
399 + "ref-cast-impl",
400 +]
401 +
402 +[[package]]
403 +name = "ref-cast-impl"
404 +version = "1.0.25"
405 +source = "registry+https://github.com/rust-lang/crates.io-index"
406 +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
407 +dependencies = [
408 + "proc-macro2",
409 + "quote",
410 + "syn 2.0.117",
411 +]
412 +
413 +[[package]]
414 +name = "rend"
415 +version = "0.4.2"
416 +source = "registry+https://github.com/rust-lang/crates.io-index"
417 +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
418 +dependencies = [
419 + "bytecheck",
420 +]
421 +
422 +[[package]]
423 +name = "rkyv"
424 +version = "0.7.46"
425 +source = "registry+https://github.com/rust-lang/crates.io-index"
426 +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1"
427 +dependencies = [
428 + "bitvec",
429 + "bytecheck",
430 + "bytes",
431 + "hashbrown 0.12.3",
432 + "ptr_meta",
433 + "rend",
434 + "rkyv_derive",
435 + "seahash",
436 + "tinyvec",
437 + "uuid",
438 +]
439 +
440 +[[package]]
441 +name = "rkyv_derive"
442 +version = "0.7.46"
443 +source = "registry+https://github.com/rust-lang/crates.io-index"
444 +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5"
445 +dependencies = [
446 + "proc-macro2",
447 + "quote",
448 + "syn 1.0.109",
449 +]
450 +
451 +[[package]]
452 +name = "rust_decimal"
453 +version = "1.41.0"
454 +source = "registry+https://github.com/rust-lang/crates.io-index"
455 +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a"
456 +dependencies = [
457 + "arrayvec",
458 + "borsh",
459 + "bytes",
460 + "num-traits",
461 + "rand",
462 + "rkyv",
463 + "serde",
464 + "serde_json",
465 + "wasm-bindgen",
466 +]
467 +
468 +[[package]]
469 +name = "rustversion"
470 +version = "1.0.22"
471 +source = "registry+https://github.com/rust-lang/crates.io-index"
472 +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
473 +
474 +[[package]]
475 +name = "schemars"
476 +version = "1.2.1"
477 +source = "registry+https://github.com/rust-lang/crates.io-index"
478 +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
479 +dependencies = [
480 + "dyn-clone",
481 + "ref-cast",
482 + "serde",
483 + "serde_json",
484 +]
485 +
486 +[[package]]
487 +name = "seahash"
488 +version = "4.1.0"
489 +source = "registry+https://github.com/rust-lang/crates.io-index"
490 +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
491 +
492 +[[package]]
493 +name = "semver"
494 +version = "1.0.28"
495 +source = "registry+https://github.com/rust-lang/crates.io-index"
496 +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
497 +
498 +[[package]]
499 +name = "serde"
500 +version = "1.0.228"
501 +source = "registry+https://github.com/rust-lang/crates.io-index"
502 +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
503 +dependencies = [
504 + "serde_core",
505 + "serde_derive",
506 +]
507 +
508 +[[package]]
509 +name = "serde_core"
510 +version = "1.0.228"
511 +source = "registry+https://github.com/rust-lang/crates.io-index"
512 +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
513 +dependencies = [
514 + "serde_derive",
515 +]
516 +
517 +[[package]]
518 +name = "serde_derive"
519 +version = "1.0.228"
520 +source = "registry+https://github.com/rust-lang/crates.io-index"
521 +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
522 +dependencies = [
523 + "proc-macro2",
524 + "quote",
525 + "syn 2.0.117",
526 +]
527 +
528 +[[package]]
529 +name = "serde_json"
530 +version = "1.0.149"
531 +source = "registry+https://github.com/rust-lang/crates.io-index"
532 +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
533 +dependencies = [
534 + "itoa",
535 + "memchr",
536 + "serde",
537 + "serde_core",
538 + "zmij",
539 +]
540 +
541 +[[package]]
542 +name = "session-sharing-protocol"
543 +version = "0.0.0"
544 +dependencies = [
545 + "byte-unit",
546 + "serde",
547 + "serde_json",
548 + "uuid",
549 +]
550 +
551 +[[package]]
552 +name = "simdutf8"
553 +version = "0.1.5"
554 +source = "registry+https://github.com/rust-lang/crates.io-index"
555 +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
556 +
557 +[[package]]
558 +name = "syn"
559 +version = "1.0.109"
560 +source = "registry+https://github.com/rust-lang/crates.io-index"
561 +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
562 +dependencies = [
563 + "proc-macro2",
564 + "quote",
565 + "unicode-ident",
566 +]
567 +
568 +[[package]]
569 +name = "syn"
570 +version = "2.0.117"
571 +source = "registry+https://github.com/rust-lang/crates.io-index"
572 +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
573 +dependencies = [
574 + "proc-macro2",
575 + "quote",
576 + "unicode-ident",
577 +]
578 +
579 +[[package]]
580 +name = "tap"
581 +version = "1.0.1"
582 +source = "registry+https://github.com/rust-lang/crates.io-index"
583 +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
584 +
585 +[[package]]
586 +name = "tinyvec"
587 +version = "1.11.0"
588 +source = "registry+https://github.com/rust-lang/crates.io-index"
589 +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
590 +dependencies = [
591 + "tinyvec_macros",
592 +]
593 +
594 +[[package]]
595 +name = "tinyvec_macros"
596 +version = "0.1.1"
597 +source = "registry+https://github.com/rust-lang/crates.io-index"
598 +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
599 +
600 +[[package]]
601 +name = "toml_datetime"
602 +version = "1.1.1+spec-1.1.0"
603 +source = "registry+https://github.com/rust-lang/crates.io-index"
604 +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
605 +dependencies = [
606 + "serde_core",
607 +]
608 +
609 +[[package]]
610 +name = "toml_edit"
611 +version = "0.25.11+spec-1.1.0"
612 +source = "registry+https://github.com/rust-lang/crates.io-index"
613 +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
614 +dependencies = [
615 + "indexmap",
616 + "toml_datetime",
617 + "toml_parser",
618 + "winnow",
619 +]
620 +
621 +[[package]]
622 +name = "toml_parser"
623 +version = "1.1.2+spec-1.1.0"
624 +source = "registry+https://github.com/rust-lang/crates.io-index"
625 +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
626 +dependencies = [
627 + "winnow",
628 +]
629 +
630 +[[package]]
631 +name = "unicode-ident"
632 +version = "1.0.24"
633 +source = "registry+https://github.com/rust-lang/crates.io-index"
634 +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
635 +
636 +[[package]]
637 +name = "unicode-xid"
638 +version = "0.2.6"
639 +source = "registry+https://github.com/rust-lang/crates.io-index"
640 +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
641 +
642 +[[package]]
643 +name = "utf8-width"
644 +version = "0.1.8"
645 +source = "registry+https://github.com/rust-lang/crates.io-index"
646 +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
647 +
648 +[[package]]
649 +name = "uuid"
650 +version = "1.23.1"
651 +source = "registry+https://github.com/rust-lang/crates.io-index"
652 +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
653 +dependencies = [
654 + "getrandom 0.4.2",
655 + "js-sys",
656 + "serde_core",
657 + "wasm-bindgen",
658 +]
659 +
660 +[[package]]
661 +name = "version_check"
662 +version = "0.9.5"
663 +source = "registry+https://github.com/rust-lang/crates.io-index"
664 +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
665 +
666 +[[package]]
667 +name = "wasi"
668 +version = "0.11.1+wasi-snapshot-preview1"
669 +source = "registry+https://github.com/rust-lang/crates.io-index"
670 +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
671 +
672 +[[package]]
673 +name = "wasip2"
674 +version = "1.0.3+wasi-0.2.9"
675 +source = "registry+https://github.com/rust-lang/crates.io-index"
676 +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
677 +dependencies = [
678 + "wit-bindgen 0.57.1",
679 +]
680 +
681 +[[package]]
682 +name = "wasip3"
683 +version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
684 +source = "registry+https://github.com/rust-lang/crates.io-index"
685 +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
686 +dependencies = [
687 + "wit-bindgen 0.51.0",
688 +]
689 +
690 +[[package]]
691 +name = "wasm-bindgen"
692 +version = "0.2.118"
693 +source = "registry+https://github.com/rust-lang/crates.io-index"
694 +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
695 +dependencies = [
696 + "cfg-if",
697 + "once_cell",
698 + "rustversion",
699 + "wasm-bindgen-macro",
700 + "wasm-bindgen-shared",
701 +]
702 +
703 +[[package]]
704 +name = "wasm-bindgen-macro"
705 +version = "0.2.118"
706 +source = "registry+https://github.com/rust-lang/crates.io-index"
707 +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
708 +dependencies = [
709 + "quote",
710 + "wasm-bindgen-macro-support",
711 +]
712 +
713 +[[package]]
714 +name = "wasm-bindgen-macro-support"
715 +version = "0.2.118"
716 +source = "registry+https://github.com/rust-lang/crates.io-index"
717 +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
718 +dependencies = [
719 + "bumpalo",
720 + "proc-macro2",
721 + "quote",
722 + "syn 2.0.117",
723 + "wasm-bindgen-shared",
724 +]
725 +
726 +[[package]]
727 +name = "wasm-bindgen-shared"
728 +version = "0.2.118"
729 +source = "registry+https://github.com/rust-lang/crates.io-index"
730 +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
731 +dependencies = [
732 + "unicode-ident",
733 +]
734 +
735 +[[package]]
736 +name = "wasm-encoder"
737 +version = "0.244.0"
738 +source = "registry+https://github.com/rust-lang/crates.io-index"
739 +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
740 +dependencies = [
741 + "leb128fmt",
742 + "wasmparser",
743 +]
744 +
745 +[[package]]
746 +name = "wasm-metadata"
747 +version = "0.244.0"
748 +source = "registry+https://github.com/rust-lang/crates.io-index"
749 +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
750 +dependencies = [
751 + "anyhow",
752 + "indexmap",
753 + "wasm-encoder",
754 + "wasmparser",
755 +]
756 +
757 +[[package]]
758 +name = "wasmparser"
759 +version = "0.244.0"
760 +source = "registry+https://github.com/rust-lang/crates.io-index"
761 +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
762 +dependencies = [
763 + "bitflags",
764 + "hashbrown 0.15.5",
765 + "indexmap",
766 + "semver",
767 +]
768 +
769 +[[package]]
770 +name = "winnow"
771 +version = "1.0.1"
772 +source = "registry+https://github.com/rust-lang/crates.io-index"
773 +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
774 +dependencies = [
775 + "memchr",
776 +]
777 +
778 +[[package]]
779 +name = "wit-bindgen"
780 +version = "0.51.0"
781 +source = "registry+https://github.com/rust-lang/crates.io-index"
782 +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
783 +dependencies = [
784 + "wit-bindgen-rust-macro",
785 +]
786 +
787 +[[package]]
788 +name = "wit-bindgen"
789 +version = "0.57.1"
790 +source = "registry+https://github.com/rust-lang/crates.io-index"
791 +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
792 +
793 +[[package]]
794 +name = "wit-bindgen-core"
795 +version = "0.51.0"
796 +source = "registry+https://github.com/rust-lang/crates.io-index"
797 +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
798 +dependencies = [
799 + "anyhow",
800 + "heck",
801 + "wit-parser",
802 +]
803 +
804 +[[package]]
805 +name = "wit-bindgen-rust"
806 +version = "0.51.0"
807 +source = "registry+https://github.com/rust-lang/crates.io-index"
808 +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
809 +dependencies = [
810 + "anyhow",
811 + "heck",
812 + "indexmap",
813 + "prettyplease",
814 + "syn 2.0.117",
815 + "wasm-metadata",
816 + "wit-bindgen-core",
817 + "wit-component",
818 +]
819 +
820 +[[package]]
821 +name = "wit-bindgen-rust-macro"
822 +version = "0.51.0"
823 +source = "registry+https://github.com/rust-lang/crates.io-index"
824 +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
825 +dependencies = [
826 + "anyhow",
827 + "prettyplease",
828 + "proc-macro2",
829 + "quote",
830 + "syn 2.0.117",
831 + "wit-bindgen-core",
832 + "wit-bindgen-rust",
833 +]
834 +
835 +[[package]]
836 +name = "wit-component"
837 +version = "0.244.0"
838 +source = "registry+https://github.com/rust-lang/crates.io-index"
839 +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
840 +dependencies = [
841 + "anyhow",
842 + "bitflags",
843 + "indexmap",
844 + "log",
845 + "serde",
846 + "serde_derive",
847 + "serde_json",
848 + "wasm-encoder",
849 + "wasm-metadata",
850 + "wasmparser",
851 + "wit-parser",
852 +]
853 +
854 +[[package]]
855 +name = "wit-parser"
856 +version = "0.244.0"
857 +source = "registry+https://github.com/rust-lang/crates.io-index"
858 +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
859 +dependencies = [
860 + "anyhow",
861 + "id-arena",
862 + "indexmap",
863 + "log",
864 + "semver",
865 + "serde",
866 + "serde_derive",
867 + "serde_json",
868 + "unicode-xid",
869 + "wasmparser",
870 +]
871 +
872 +[[package]]
873 +name = "wyz"
874 +version = "0.5.1"
875 +source = "registry+https://github.com/rust-lang/crates.io-index"
876 +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
877 +dependencies = [
878 + "tap",
879 +]
880 +
881 +[[package]]
882 +name = "zerocopy"
883 +version = "0.8.48"
884 +source = "registry+https://github.com/rust-lang/crates.io-index"
885 +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
886 +dependencies = [
887 + "zerocopy-derive",
888 +]
889 +
890 +[[package]]
891 +name = "zerocopy-derive"
892 +version = "0.8.48"
893 +source = "registry+https://github.com/rust-lang/crates.io-index"
894 +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
895 +dependencies = [
896 + "proc-macro2",
897 + "quote",
898 + "syn 2.0.117",
899 +]
900 +
901 +[[package]]
902 +name = "zmij"
903 +version = "1.0.21"
904 +source = "registry+https://github.com/rust-lang/crates.io-index"
905 +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
Cargo.toml new
+10
@@ -0,0 +1,10 @@
1 +[package]
2 +name = "session-sharing-protocol"
3 +edition = "2024"
4 +publish = false
5 +
6 +[dependencies]
7 +byte-unit = { version = "5.1.7", features = ["serde"] }
8 +serde = { version = "1.0", features = ["derive"] }
9 +serde_json = "1.0"
10 +uuid = { version = "1.4", features = ["serde", "v4"] }
rust-toolchain.toml new
+4
@@ -0,0 +1,4 @@
1 +[toolchain]
2 +channel = "1.88.0"
3 +components = ["rustfmt", "clippy"]
4 +profile = "minimal"
src/common/agent_prompt.rs new
+116
@@ -0,0 +1,116 @@
1 +use serde::{Deserialize, Serialize};
2 +use uuid::Uuid;
3 +
4 +use super::BlockId;
5 +
6 +/// An opaque id to track agent prompt requests from viewers.
7 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
8 +pub struct AgentPromptRequestId(String);
9 +
10 +impl AgentPromptRequestId {
11 + pub fn new() -> Self {
12 + Self(Uuid::new_v4().to_string())
13 + }
14 +}
15 +
16 +impl Default for AgentPromptRequestId {
17 + fn default() -> Self {
18 + Self::new()
19 + }
20 +}
21 +
22 +/// A set of reasons for which an agent prompt request might fail.
23 +#[derive(Clone, Debug, Serialize, Deserialize)]
24 +pub enum AgentPromptFailureReason {
25 + /// The viewer does not have sufficient permissions to send agent prompts.
26 + InsufficientPermissions,
27 +
28 + /// The conversation ID provided is invalid or doesn't exist.
29 + InvalidConversation,
30 +
31 + // There is a long running command that is already in progress.
32 + CommandInProgress,
33 +}
34 +
35 +/// Represents an AI agent attachment that can be sent with a prompt.
36 +/// This is a simplified version for the protocol - the sharer will reconstruct
37 +/// the full attachment from the block ID.
38 +///
39 +/// TODO: Add support for image attachments. Images are currently handled as
40 +/// AIAgentContext::Image (contextual info) rather than AIAgentAttachment in the client,
41 +/// so we need to decide whether to treat viewer-attached images as context or attachments
42 +/// before adding them to the protocol.
43 +#[derive(Clone, Debug, Serialize, Deserialize)]
44 +pub enum AgentAttachment {
45 + /// A reference to a terminal block by ID.
46 + /// The sharer will resolve this to the actual block content.
47 + BlockReference { block_id: BlockId },
48 +
49 + /// Plain text attachment (e.g., clipboard content).
50 + PlainText { content: String },
51 +
52 + /// A reference to a file that has been uploaded to GCS.
53 + /// The host fetches download URLs directly from warp-server using the attachment ID.
54 + FileReference {
55 + attachment_id: String,
56 + file_name: String,
57 + },
58 +}
59 +
60 +/// An optional server conversation token for continuing an existing agent conversation.
61 +/// The id provided here is 1:1 with the ServerConversationToken used on the client/server (not AIAgentConversationId).
62 +/// If no token is provided, a new conversation will be started.
63 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Copy)]
64 +pub struct ServerConversationToken(Uuid);
65 +
66 +impl ServerConversationToken {
67 + pub fn new() -> Self {
68 + Self(Uuid::new_v4())
69 + }
70 +
71 + pub fn from_uuid(uuid: Uuid) -> Self {
72 + Self(uuid)
73 + }
74 +
75 + pub fn as_uuid(&self) -> Uuid {
76 + self.0
77 + }
78 +}
79 +
80 +impl Default for ServerConversationToken {
81 + fn default() -> Self {
82 + Self::new()
83 + }
84 +}
85 +
86 +impl std::fmt::Display for ServerConversationToken {
87 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 + write!(f, "{}", self.0)
89 + }
90 +}
91 +
92 +impl std::str::FromStr for ServerConversationToken {
93 + type Err = uuid::Error;
94 +
95 + fn from_str(s: &str) -> Result<Self, Self::Err> {
96 + Ok(Self(Uuid::parse_str(s)?))
97 + }
98 +}
99 +
100 +/// The data for an agent prompt request from a viewer.
101 +#[derive(Clone, Debug, Serialize, Deserialize)]
102 +pub struct AgentPromptRequest {
103 + /// Unique identifier for this request.
104 + pub id: AgentPromptRequestId,
105 +
106 + /// The server conversation token to continue. If None, start a new conversation.
107 + /// This is the server_conversation_token that links viewer and sharer conversations.
108 + pub server_conversation_token: Option<ServerConversationToken>,
109 +
110 + /// The user's prompt/query.
111 + pub prompt: String,
112 +
113 + /// Optional attachments (blocks, files, etc.) referenced in the prompt.
114 + #[serde(default)]
115 + pub attachments: Vec<AgentAttachment>,
116 +}
src/common/command_execution.rs new
+24
@@ -0,0 +1,24 @@
1 +use serde::{Deserialize, Serialize};
2 +use uuid::Uuid;
3 +
4 +/// An opaque id to track command execution requests.
5 +#[derive(Clone, Debug, Serialize, Deserialize)]
6 +pub struct CommandExecutionRequestId(String);
7 +impl CommandExecutionRequestId {
8 + #[allow(clippy::new_without_default)]
9 + pub fn new() -> Self {
10 + Self(Uuid::new_v4().to_string())
11 + }
12 +}
13 +
14 +/// A set of reasons for which a command execution request might fail.
15 +#[derive(Clone, Debug, Serialize, Deserialize)]
16 +pub enum CommandExecutionFailureReason {
17 + /// The viewer does not have sufficient permissions to run commands.
18 + InsufficientPermissions,
19 +
20 + /// The buffer for which the command execution was requested is old.
21 + /// Specifically, there is either a newer buffer or a command
22 + /// is in-progress for the given buffer.
23 + StaleBuffer,
24 +}
src/common/control_action.rs new
+43
@@ -0,0 +1,43 @@
1 +use serde::{Deserialize, Serialize};
2 +use uuid::Uuid;
3 +
4 +use crate::common::ServerConversationToken;
5 +
6 +/// A unique id to track control action requests.
7 +#[derive(Clone, Debug, Serialize, Deserialize)]
8 +pub struct ControlActionRequestId(String);
9 +
10 +impl ControlActionRequestId {
11 + #[allow(clippy::new_without_default)]
12 + pub fn new() -> Self {
13 + Self(Uuid::new_v4().to_string())
14 + }
15 +
16 + /// Returns the underlying opaque id string.
17 + pub fn id(&self) -> &str {
18 + &self.0
19 + }
20 +}
21 +
22 +/// Higher-level control messages that don't correspond 1:1 to terminal actions/inputs
23 +/// (or imply a warp-specific action outside of their normal terminal use).
24 +#[derive(Clone, Debug, Serialize, Deserialize)]
25 +pub enum ControlAction {
26 + /// Request that a shared-session AI conversation be cancelled by the sharer.
27 + CancelConversation {
28 + server_conversation_token: ServerConversationToken,
29 + },
30 +}
31 +
32 +/// Reasons a control action request from a viewer might fail.
33 +#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
34 +pub enum ControlActionFailureReason {
35 + /// The viewer does not have permission to perform this control action.
36 + InsufficientPermissions,
37 + /// The session no longer exists.
38 + SessionNotFound,
39 + /// There is no sharer currently connected to handle the action.
40 + SharerUnavailable,
41 + /// Unexpected, something went wrong in the server.
42 + InternalServerError,
43 +}
src/common/feature_support.rs new
+17
@@ -0,0 +1,17 @@
1 +use serde::{Deserialize, Serialize};
2 +
3 +/// Client feature support declaration.
4 +/// Clients include this in their init payloads to declare which protocol features they support.
5 +/// This allows the server to adapt messages for backward compatibility with older clients.
6 +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
7 +pub struct FeatureSupport {
8 + /// Whether the client supports agent view and has it enabled.
9 + #[serde(default)]
10 + pub supports_agent_view: bool,
11 + /// Unused in favor of supports_full_role_for_real. Clients set this to true before they were actually ready.
12 + #[serde(default)]
13 + pub supports_full_role: bool,
14 + /// Whether the client supports the "Full" role ACL.
15 + #[serde(default)]
16 + pub supports_full_role_for_real: bool,
17 +}
src/common/input.rs new
+128
@@ -0,0 +1,128 @@
1 +use super::{BlockId, ParticipantId};
2 +use byte_unit::Byte;
3 +use serde::{Deserialize, Serialize};
4 +
5 +/// The replica ID that a participant's CRDT-compliant input
6 +/// buffer must use. This must be unique across a session.
7 +///
8 +/// The sharer is allowed to choose their own replica ID.
9 +#[derive(Clone, Default, Debug, Serialize, Deserialize)]
10 +pub struct InputReplicaId(String);
11 +
12 +impl From<String> for InputReplicaId {
13 + fn from(value: String) -> Self {
14 + Self(value)
15 + }
16 +}
17 +
18 +impl std::fmt::Display for InputReplicaId {
19 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 + write!(f, "{}", self.0)
21 + }
22 +}
23 +
24 +/// A monotonically increasing sequence number to identify sequential edits for a given buffer.
25 +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
26 +pub struct InputOperationSeqNo(usize);
27 +
28 +impl InputOperationSeqNo {
29 + pub fn zero() -> Self {
30 + Self(0)
31 + }
32 +
33 + pub fn advance(&mut self) {
34 + self.0 += 1;
35 + }
36 +
37 + pub fn as_usize(&self) -> usize {
38 + self.0
39 + }
40 +}
41 +
42 +impl From<usize> for InputOperationSeqNo {
43 + fn from(value: usize) -> Self {
44 + Self(value)
45 + }
46 +}
47 +
48 +/// A [`BufferId`] identifies an instance of the buffer in a session.
49 +/// For example, suppose a session starts with buffer_id=B1.
50 +/// When a command is executed and the buffer is reset, the
51 +/// buffer_id=B2, where B1 != B2.
52 +///
53 +/// Today, a [`BufferId`] masquerades as a [`BlockId`].
54 +#[derive(Clone, Debug, Default, Deserialize, Hash, Serialize, Eq, PartialEq)]
55 +pub struct BufferId(String);
56 +
57 +impl std::fmt::Display for BufferId {
58 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 + write!(f, "{}", self.0)
60 + }
61 +}
62 +
63 +impl From<String> for BufferId {
64 + fn from(value: String) -> Self {
65 + Self(value)
66 + }
67 +}
68 +
69 +impl From<BlockId> for BufferId {
70 + fn from(value: BlockId) -> Self {
71 + Self(value.to_string())
72 + }
73 +}
74 +
75 +impl From<BufferId> for BlockId {
76 + fn from(value: BufferId) -> Self {
77 + value.0.into()
78 + }
79 +}
80 +
81 +/// A CRDT-compliant operation.
82 +/// For now, this is a arbitrary payload that clients should know
83 +/// how to serialize / deserialize. Eventually, this will be a
84 +/// strongly-typed data structure.
85 +#[derive(Clone, Debug, Deserialize, Serialize)]
86 +pub struct CrdtOperation(pub Vec<u8>);
87 +
88 +/// A unique identifier for an input operation. Specifically,
89 +/// this uniquely identifies an operation for a specific buffer,
90 +/// for a given participant.
91 +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
92 +pub struct InputOperationId {
93 + /// The participant that made the change.
94 + pub participant_id: ParticipantId,
95 +
96 + /// The ID of the buffer that this operation was applied to.
97 + pub buffer_id: BufferId,
98 +
99 + /// A monotonically increasing sequence number to identify sequential edits
100 + /// for a specific buffer.
101 + pub op_no: InputOperationSeqNo,
102 +}
103 +
104 +#[derive(Clone, Debug, Deserialize, Serialize)]
105 +pub struct InputUpdate {
106 + pub id: InputOperationId,
107 +
108 + /// A single input operation consists of a batch
109 + /// of updates.
110 + pub ops: Vec<CrdtOperation>,
111 +}
112 +
113 +impl InputUpdate {
114 + pub fn num_bytes(&self) -> Byte {
115 + self.ops
116 + .iter()
117 + .map(|op| op.0.len() as u64)
118 + .fold(0, u64::saturating_add)
119 + .into()
120 + }
121 +}
122 +
123 +/// A set of reasons why a request to edit the input might fail.
124 +#[derive(Clone, Debug, Deserialize, Serialize)]
125 +pub enum InputUpdateFailureReason {
126 + /// The viewer does not have sufficient permissions.
127 + InsufficientPermissions,
128 +}
src/common/mod.rs new
+39
@@ -0,0 +1,39 @@
1 +//! Common types used by both sharer and viewer.
2 +
3 +mod agent_prompt;
4 +mod command_execution;
5 +mod control_action;
6 +mod feature_support;
7 +mod input;
8 +mod ordered_terminal_events;
9 +mod participant;
10 +mod permissions;
11 +mod presence;
12 +mod prompt;
13 +mod roles;
14 +mod scrollback;
15 +mod session_params;
16 +mod team;
17 +mod telemetry;
18 +mod ui_state;
19 +mod user;
20 +mod write_to_pty;
21 +
22 +pub use agent_prompt::*;
23 +pub use command_execution::*;
24 +pub use control_action::*;
25 +pub use feature_support::*;
26 +pub use input::*;
27 +pub use ordered_terminal_events::*;
28 +pub use participant::*;
29 +pub use permissions::*;
30 +pub use presence::*;
31 +pub use prompt::*;
32 +pub use roles::*;
33 +pub use scrollback::*;
34 +pub use session_params::*;
35 +pub use team::*;
36 +pub use telemetry::*;
37 +pub use ui_state::*;
38 +pub use user::*;
39 +pub use write_to_pty::*;
src/common/ordered_terminal_events.rs new
+116
@@ -0,0 +1,116 @@
1 +use super::{BlockId, ParticipantId};
2 +use byte_unit::Byte;
3 +use serde::{Deserialize, Serialize};
4 +
5 +/// AI metadata for correlating terminal blocks with agent commands.
6 +/// This allows viewers in shared sessions to associate terminal command blocks
7 +/// with the agent tool calls that triggered them.
8 +#[derive(Clone, Debug, Serialize, Deserialize)]
9 +pub struct AICommandMetadata {
10 + /// The tool call ID from the Multi-Agent API protocol.
11 + /// Corresponds to action_id on the sharer side.
12 + pub tool_call_id: String,
13 +
14 + /// Whether this command is being monitored by an agent as a long-running command.
15 + #[serde(default)]
16 + pub is_agent_monitored: bool,
17 +}
18 +
19 +/// Types of terminal events that need to be ordered against each other.
20 +#[derive(Clone, Deserialize, Serialize)]
21 +pub enum OrderedTerminalEventType {
22 + /// Bytes read off the sharer's pty (session contents).
23 + PtyBytesRead {
24 + bytes: Vec<u8>,
25 + },
26 + /// A command is beginning to execute.
27 + CommandExecutionStarted {
28 + /// The ID of the participant who ran the command.
29 + participant_id: ParticipantId,
30 + /// AI metadata if this command was executed by an agent.
31 + #[serde(default)]
32 + ai_metadata: Option<AICommandMetadata>,
33 + },
34 + CommandExecutionFinished {
35 + next_block_id: BlockId,
36 + },
37 + /// The sharer's terminal was resized.
38 + Resize {
39 + window_size: WindowSize,
40 + },
41 + /// The sharer received an AI agent response event. Response events include all information needed to reconstruct a conversation, including:
42 + // * The start and end of individual requests
43 + // * Incremental agent output
44 + // * Echoed user messages and tool call results
45 + /// See https://github.com/warpdotdev/warp-proto-apis/blob/6310871f081b5f44b2d4e3e5d8fdfa3008b750b0/apis/multi_agent/v1/response.proto#L16-L17
46 + AgentResponseEvent {
47 + /// The ID of the participant who sent the query to initiate this agent response.
48 + response_initiator: Option<ParticipantId>,
49 + /// The base64-encoded MAA ResponseEvent protocol buffer message.
50 + response_event: String,
51 + /// For forked conversations, this is the original conversation token that the
52 + /// conversation was forked from. Viewers use this to link the new server-assigned
53 + /// conversation token to an existing conversation created during historical replay.
54 + #[serde(default)]
55 + forked_from_conversation_token: Option<String>,
56 + },
57 + /// Marks the start of historical agent conversation replay.
58 + /// Viewers should use this to suppress live-conversation specific actions until replay ends
59 + /// (e.g. the insertion of the ambient agent conversation tombstone).
60 + AgentConversationReplayStarted,
61 + /// Marks the end of historical agent conversation replay.
62 + AgentConversationReplayEnded,
63 +}
64 +
65 +/// Represents the size of a PTY. Mimics the winsize struct that
66 +/// can be queried via [ioctl](https://man7.org/linux/man-pages/man2/ioctl_tty.2.html).
67 +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
68 +pub struct WindowSize {
69 + pub num_rows: usize,
70 + pub num_cols: usize,
71 +}
72 +
73 +/// Override the Debug impl to avoid accidentally leaking sensitive
74 +/// data in logs.
75 +impl std::fmt::Debug for OrderedTerminalEventType {
76 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 + match self {
78 + Self::PtyBytesRead { .. } => f.write_str("PtyBytesRead"),
79 + Self::CommandExecutionStarted { .. } => f.write_str("CommandExecutionStarted"),
80 + Self::CommandExecutionFinished { .. } => f.write_str("CommandExecutionFinished"),
81 + Self::Resize { .. } => f.write_str("Resize"),
82 + Self::AgentResponseEvent { .. } => f.write_str("AgentResponseEvent"),
83 + Self::AgentConversationReplayStarted => f.write_str("AgentConversationReplayStarted"),
84 + Self::AgentConversationReplayEnded => f.write_str("AgentConversationReplayEnded"),
85 + }
86 + }
87 +}
88 +
89 +impl OrderedTerminalEventType {
90 + pub fn num_bytes(&self) -> Byte {
91 + match &self {
92 + OrderedTerminalEventType::PtyBytesRead { bytes } => bytes.len().into(),
93 + OrderedTerminalEventType::AgentResponseEvent { response_event, .. } => {
94 + response_event.len().into()
95 + }
96 + OrderedTerminalEventType::CommandExecutionStarted { .. }
97 + | OrderedTerminalEventType::CommandExecutionFinished { .. }
98 + | OrderedTerminalEventType::AgentConversationReplayStarted
99 + | OrderedTerminalEventType::AgentConversationReplayEnded
100 + | OrderedTerminalEventType::Resize { .. } => Byte::from_u64(0),
101 + }
102 + }
103 +}
104 +
105 +/// Any terminal event where strict ordering against other terminal events is important.
106 +#[derive(Clone, Debug, Deserialize, Serialize)]
107 +pub struct OrderedTerminalEvent {
108 + pub event_no: usize,
109 + pub event_type: OrderedTerminalEventType,
110 +}
111 +
112 +impl OrderedTerminalEvent {
113 + pub fn num_bytes(&self) -> Byte {
114 + self.event_type.num_bytes()
115 + }
116 +}
src/common/participant.rs new
+154
@@ -0,0 +1,154 @@
1 +use super::{InputReplicaId, Role, Selection};
2 +use serde::{Deserialize, Serialize};
3 +use uuid::Uuid;
4 +
5 +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
6 +pub enum ParticipantType {
7 + Sharer,
8 + Viewer { role: Role },
9 +}
10 +
11 +/// An ID for a shared session participant that is unique across all participants across all shared sessions.
12 +/// If a viewer joins a shared session multiple times from the same machine, they are treated as separate participants with their own ParticipantId.
13 +/// A participant reconnecting should keep the same participant ID.
14 +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15 +pub struct ParticipantId(String);
16 +
17 +impl From<String> for ParticipantId {
18 + fn from(value: String) -> Self {
19 + ParticipantId(value)
20 + }
21 +}
22 +
23 +impl ParticipantId {
24 + pub fn new() -> ParticipantId {
25 + ParticipantId(Uuid::new_v4().to_string())
26 + }
27 +}
28 +
29 +impl Default for ParticipantId {
30 + fn default() -> Self {
31 + ParticipantId::new()
32 + }
33 +}
34 +
35 +impl std::fmt::Display for ParticipantId {
36 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 + std::fmt::Display::fmt(&self.0, f)
38 + }
39 +}
40 +
41 +/// Mostly static information about a participant.
42 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
43 +pub struct ProfileData {
44 + pub firebase_uid: String,
45 + pub display_name: String,
46 +
47 + /// If None, the client should render an avatar themselves.
48 + pub photo_url: Option<String>,
49 + pub email: Option<String>,
50 +
51 + pub input_replica_id: InputReplicaId,
52 +}
53 +
54 +/// Contains information about a shared session participant.
55 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
56 +pub struct ParticipantInfo {
57 + pub id: ParticipantId,
58 + pub profile_data: ProfileData,
59 + pub selection: Selection,
60 +}
61 +
62 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
63 +pub struct Sharer {
64 + pub info: ParticipantInfo,
65 +}
66 +
67 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
68 +pub struct Viewer {
69 + pub info: ParticipantInfo,
70 + pub role: Role,
71 + /// Whether or not this viewer is still part of the session.
72 + pub is_present: bool,
73 +}
74 +
75 +/// Information about a viewer that is still part of the session.
76 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
77 +pub struct PresentViewer {
78 + pub info: ParticipantInfo,
79 + /// The maximum access level this viewer has been given.
80 + pub max_acl: Role,
81 +}
82 +
83 +/// Information about a viewer that is no longer a part of the session.
84 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
85 +pub struct AbsentViewer {
86 + pub info: ParticipantInfo,
87 +}
88 +
89 +/// Information about a user who is a direct guest on the session.
90 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
91 +pub struct Guest {
92 + pub profile_data: ProfileData,
93 + /// The direct access level that this guest has been given.
94 + pub direct_acl: Role,
95 +}
96 +
97 +/// Information about a non-Warp user who has been invited to the session.
98 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
99 +pub struct PendingGuest {
100 + pub email: String,
101 + /// The direct access level that this guest has been given.
102 + pub direct_acl: Role,
103 +}
104 +
105 +/// Information about the full list of all participants in a shared session.
106 +///
107 +/// To derive the session's direct guests, all of the users with direct acls
108 +/// across `present_viewers`, `absent_viewers`, and `non_viewer_guests` must be
109 +/// accumulated.
110 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
111 +pub struct ParticipantList {
112 + pub sharer: Sharer,
113 +
114 + /// Legacy field kept for backwards compatibility. After the ACL transition
115 + /// we should be using `present_viewers`, `absent_viewers`, and
116 + /// `non_viewer_guests` and can remove this field.
117 + pub viewers: Vec<Viewer>,
118 +
119 + /// Viewers that are currently on the session.
120 + pub present_viewers: Vec<PresentViewer>,
121 + /// Viewers that are no longer on the session.
122 + pub absent_viewers: Vec<AbsentViewer>,
123 + /// Users that have a direct ACL on the session.
124 + pub guests: Vec<Guest>,
125 + /// Non-Warp users who have been invited to the session.
126 + pub pending_guests: Vec<PendingGuest>,
127 +}
128 +
129 +impl ParticipantList {
130 + /// Downgrades all `Role::Full` fields to `Role::Executor`.
131 + /// Used for backward compatibility with clients that don't support the Full role.
132 + pub fn downgrade_full_roles(&mut self) {
133 + for viewer in &mut self.viewers {
134 + viewer.role.downgrade_full();
135 + }
136 + for viewer in &mut self.present_viewers {
137 + viewer.max_acl.downgrade_full();
138 + }
139 + for guest in &mut self.guests {
140 + guest.direct_acl.downgrade_full();
141 + }
142 + for guest in &mut self.pending_guests {
143 + guest.direct_acl.downgrade_full();
144 + }
145 + }
146 +}
147 +
148 +/// Information received from fetching and processing the participant list.
149 +pub struct ParticipantListInfo {
150 + /// The full list of participants in a shared session.
151 + pub list: ParticipantList,
152 + /// The list of present participants who do not have access to the session.
153 + pub viewers_without_access: Vec<ParticipantId>,
154 +}
src/common/permissions.rs new
+65
@@ -0,0 +1,65 @@
1 +//! Permission-related types shared between the sharer and viewer protocols.
2 +
3 +use super::{Role, TeamAclData};
4 +use serde::{Deserialize, Serialize};
5 +
6 +#[derive(Clone, Serialize, Deserialize, Debug)]
7 +pub enum LinkAccessLevelUpdateResponse {
8 + Ok { role: Option<Role> },
9 + Error,
10 +}
11 +
12 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
13 +pub enum AddGuestsResponse {
14 + Success,
15 + Error(FailedToAddGuestsReason),
16 +}
17 +
18 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
19 +pub enum FailedToAddGuestsReason {
20 + /// Unexpected, something went wrong in the server.
21 + Invalid,
22 + /// One or more of the emails did not correspond with Warp users.
23 + NotWarpUsers,
24 + /// One or more of the guests has already been added to the session.
25 + GuestAlreadyAdded,
26 +}
27 +
28 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
29 +pub enum RemoveGuestResponse {
30 + Success,
31 + Error(FailedToRemoveGuestReason),
32 +}
33 +
34 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
35 +pub enum FailedToRemoveGuestReason {
36 + /// Unexpected, something went wrong in the server.
37 + Invalid,
38 +}
39 +
40 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
41 +pub enum UpdatePendingUserRoleResponse {
42 + Success,
43 + Error(FailedToUpdatePendingUserRoleReason),
44 +}
45 +
46 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
47 +pub enum FailedToUpdatePendingUserRoleReason {
48 + /// Unexpected, something went wrong in the server.
49 + Invalid,
50 +}
51 +
52 +#[derive(Serialize, Deserialize, Clone, Debug)]
53 +pub enum TeamAccessLevelUpdateResponse {
54 + Success {
55 + team_uid: String,
56 + team_acl: Option<TeamAclData>,
57 + },
58 + Error(FailedToUpdateTeamAccessLevelReason),
59 +}
60 +
61 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
62 +pub enum FailedToUpdateTeamAccessLevelReason {
63 + /// Unexpected, something went wrong in the server.
64 + Invalid,
65 +}
src/common/presence.rs new
+102
@@ -0,0 +1,102 @@
1 +use super::ParticipantId;
2 +use serde::{Deserialize, Serialize};
3 +
4 +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
5 +pub enum GridType {
6 + Prompt,
7 + /// Right side prompt
8 + Rprompt,
9 + Output,
10 + /// Combined prompt/command grid, used for same-line prompt
11 + PromptAndCommand,
12 +}
13 +
14 +/// A point in a grid.
15 +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
16 +pub struct Point {
17 + pub row: usize,
18 + pub col: usize,
19 +}
20 +
21 +/// A point in a grid within a block.
22 +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
23 +pub struct BlockPoint {
24 + pub block_id: BlockId,
25 + pub grid_type: GridType,
26 + pub point: Point,
27 +}
28 +
29 +/// An ID for a block that is unique only within a single shared session.
30 +#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize)]
31 +pub struct BlockId(String);
32 +
33 +impl std::fmt::Display for BlockId {
34 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 + write!(f, "{}", self.0)
36 + }
37 +}
38 +
39 +impl From<String> for BlockId {
40 + fn from(value: String) -> Self {
41 + Self(value)
42 + }
43 +}
44 +
45 +/// What a shared session participant has selected.
46 +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
47 +pub enum Selection {
48 + #[default]
49 + None,
50 + Blocks {
51 + block_ids: Vec<BlockId>,
52 + },
53 + /// Start is always before end.
54 + BlockText {
55 + start: BlockPoint,
56 + end: BlockPoint,
57 + /// If true, the user selected from the end point to the start point (useful for knowing where the cursor should be)
58 + is_reversed: bool,
59 + },
60 + /// Start is always before end
61 + AltScreenText {
62 + start: Point,
63 + end: Point,
64 + /// If true, the user selected from the end point to the start point (useful for knowing where the cursor should be)
65 + is_reversed: bool,
66 + },
67 +}
68 +
69 +#[derive(
70 + Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize,
71 +)]
72 +pub struct SelectionEventNo(usize);
73 +
74 +impl From<usize> for SelectionEventNo {
75 + fn from(value: usize) -> Self {
76 + Self(value)
77 + }
78 +}
79 +#[derive(Clone, Debug, Deserialize, Serialize)]
80 +pub struct SelectionUpdate {
81 + pub selection: Selection,
82 + pub event_no: SelectionEventNo,
83 +}
84 +
85 +#[derive(Clone, Debug, Deserialize, Serialize)]
86 +pub enum PresenceUpdate {
87 + Selection(Selection),
88 + // other stuff in the future, like scroll state
89 +}
90 +
91 +#[derive(Clone, Debug, Deserialize, Serialize)]
92 +pub struct ParticipantPresenceUpdate {
93 + pub participant_id: ParticipantId,
94 + pub update: PresenceUpdate,
95 +}
96 +
97 +/// One participant's current selection.
98 +#[derive(Clone, Debug, Default, Deserialize, Serialize)]
99 +pub struct ParticipantSelection {
100 + pub id: ParticipantId,
101 + pub selection: Selection,
102 +}
src/common/prompt.rs new
+30
@@ -0,0 +1,30 @@
1 +use serde::{Deserialize, Serialize};
2 +
3 +// The prompt for the active block.
4 +#[derive(Clone, Default, Deserialize, PartialEq, Eq, Serialize)]
5 +pub enum ActivePrompt {
6 + /// Using the PS1 prompt, which is included in forwarded pty bytes.
7 + #[default]
8 + PS1,
9 + /// JSON serialization of PromptSnapshot
10 + WarpPrompt(String),
11 +}
12 +
13 +/// Override the Debug impl to avoid accidentally leaking sensitive
14 +/// data in logs.
15 +impl std::fmt::Debug for ActivePrompt {
16 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 + match self {
18 + Self::PS1 { .. } => f.write_str("ActivePrompt::PS1"),
19 + Self::WarpPrompt(..) => f.write_str("ActivePrompt::WarpPrompt"),
20 + }
21 + }
22 +}
23 +
24 +// An update to the active block's prompt.
25 +#[derive(Clone, Debug, Deserialize, Serialize)]
26 +pub struct ActivePromptUpdate {
27 + pub active_prompt: ActivePrompt,
28 + /// The event_no of the last OrderedTerminalEvent shared.
29 + pub last_event_no: usize,
30 +}
src/common/roles.rs new
+86
@@ -0,0 +1,86 @@
1 +use super::ParticipantId;
2 +use serde::{Deserialize, Serialize};
3 +use uuid::Uuid;
4 +
5 +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
6 +pub enum Role {
7 + Reader,
8 + Executor,
9 + /// Executor, and can change ACLs of others
10 + Full,
11 +}
12 +
13 +impl Role {
14 + /// Returns true if this role has execution permissions.
15 + pub fn can_execute(&self) -> bool {
16 + matches!(self, Role::Executor | Role::Full)
17 + }
18 +
19 + /// Downgrades `Full` to `Executor` for clients that don't support the Full role.
20 + pub fn downgrade_full(&mut self) {
21 + if *self == Role::Full {
22 + *self = Role::Executor;
23 + }
24 + }
25 +}
26 +
27 +impl Default for Role {
28 + fn default() -> Self {
29 + Self::Reader
30 + }
31 +}
32 +
33 +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
34 +/// Info about different types of ACLs for a user.
35 +pub struct AccessLevels {
36 + /// The maximum ACL given to the user, could be direct, link-based, etc.
37 + pub max_acl: Role,
38 + /// The direct ACL given to the user.
39 + pub direct_acl: Option<Role>,
40 +}
41 +
42 +/// An ID for a role request that is unique across all participants across all shared sessions.
43 +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
44 +pub struct RoleRequestId(String);
45 +
46 +impl From<String> for RoleRequestId {
47 + fn from(value: String) -> Self {
48 + RoleRequestId(value)
49 + }
50 +}
51 +
52 +impl RoleRequestId {
53 + pub fn new() -> RoleRequestId {
54 + RoleRequestId(Uuid::new_v4().to_string())
55 + }
56 +}
57 +
58 +impl Default for RoleRequestId {
59 + fn default() -> Self {
60 + RoleRequestId::new()
61 + }
62 +}
63 +
64 +impl std::fmt::Display for RoleRequestId {
65 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 + std::fmt::Display::fmt(&self.0, f)
67 + }
68 +}
69 +
70 +#[derive(Serialize, Deserialize, Clone, Debug)]
71 +pub enum RoleRequestRejectedReason {
72 + RejectedBySharer,
73 +}
74 +
75 +#[derive(Serialize, Deserialize, Clone, Debug)]
76 +pub enum RoleRequestResponse {
77 + Approved { new_role: Role },
78 + Rejected { reason: RoleRequestRejectedReason },
79 +}
80 +
81 +#[derive(Serialize, Deserialize, Clone, Debug)]
82 +pub struct PendingRoleRequest {
83 + participant_id: ParticipantId,
84 + request_id: RoleRequestId,
85 + role: Role,
86 +}
src/common/scrollback.rs new
+65
@@ -0,0 +1,65 @@
1 +use byte_unit::Byte;
2 +use serde::{Deserialize, Serialize};
3 +
4 +/// Scrollback is the set of session contents that weren't shared live
5 +/// but are still part of the shared session.
6 +#[derive(Clone, Deserialize, Serialize)]
7 +pub struct Scrollback {
8 + /// The blocks that make up the scrollback. Clients are expected
9 + /// to be able to serialize and deserialize accordingly.
10 + pub blocks: Vec<ScrollbackBlock>,
11 +
12 + /// True iff the session is in alt-screen mode
13 + /// at time of share.
14 + pub is_alt_screen_active: bool,
15 +}
16 +
17 +impl Scrollback {
18 + pub fn num_bytes(&self) -> Byte {
19 + self.blocks
20 + .iter()
21 + .map(|b| b.num_bytes().as_u64())
22 + .fold(0, u64::saturating_add)
23 + .into()
24 + }
25 +
26 + /// Returns true if the scrollback size exceeds |size_bytes|.
27 + pub fn exceeds_size_bytes(&self, size_bytes: Byte) -> bool {
28 + self.num_bytes() > size_bytes
29 + }
30 +}
31 +
32 +/// Override the Debug impl to avoid accidentally leaking sensitive
33 +/// data in logs.
34 +impl std::fmt::Debug for Scrollback {
35 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 + write!(
37 + f,
38 + "Scrollback {{ num_blocks: {}, is_alt_screen_active: {} }}",
39 + self.blocks.len(),
40 + self.is_alt_screen_active
41 + )
42 + }
43 +}
44 +
45 +/// An individual scrollback block.
46 +#[derive(Clone, Deserialize, Serialize)]
47 +pub struct ScrollbackBlock {
48 + /// The raw contents of the block. Clients are expected to be able to
49 + /// serialize and deserialize from [`SerializedBlock`] in the Warp client.
50 + pub raw: Vec<u8>,
51 +}
52 +
53 +impl ScrollbackBlock {
54 + pub fn num_bytes(&self) -> Byte {
55 + self.raw.len().into()
56 + }
57 +}
58 +
59 +/// Override the Debug impl to avoid accidentally leaking sensitive
60 +/// data in logs.
61 +impl std::fmt::Debug for ScrollbackBlock {
62 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 + write!(f, "ScrollbackBlock {{ num_bytes: {} }}", self.num_bytes())
64 + }
65 +}
src/common/session_params.rs new
+99
@@ -0,0 +1,99 @@
1 +use serde::{Deserialize, Serialize};
2 +
3 +use uuid::Uuid;
4 +
5 +/// The canonical identifier for a shared session.
6 +/// A [`SessionId`] on its own cannot be used to access
7 +/// a shared session; you need the corresponding [`SessionSecret`].
8 +/// TODO: consider making the internal type a plain old String.
9 +#[derive(Debug, Hash, Serialize, Deserialize, Eq, PartialEq, Clone, Copy)]
10 +#[serde(transparent)]
11 +pub struct SessionId(Uuid);
12 +impl SessionId {
13 + #[allow(clippy::new_without_default)]
14 + pub fn new() -> Self {
15 + Self(Uuid::new_v4())
16 + }
17 +}
18 +
19 +impl std::fmt::Display for SessionId {
20 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 + write!(f, "{}", self.0)
22 + }
23 +}
24 +
25 +/// The `warp` server framework uses [`FromStr`] to deserialize
26 +/// the string from the route.
27 +impl std::str::FromStr for SessionId {
28 + type Err = uuid::Error;
29 + fn from_str(s: &str) -> Result<Self, Self::Err> {
30 + Uuid::from_str(s).map(SessionId)
31 + }
32 +}
33 +
34 +/// The secret for a shared session.
35 +/// A shared session cannot be accessed without its secret.
36 +/// The client should treat this as some opaque string.
37 +#[derive(Hash, Serialize, Deserialize, Eq, PartialEq, Clone, Default)]
38 +#[serde(transparent)]
39 +pub struct SessionSecret(String);
40 +impl SessionSecret {
41 + #[allow(clippy::new_without_default)]
42 + pub fn new() -> Self {
43 + Self(Uuid::new_v4().to_string())
44 + }
45 +}
46 +
47 +/// Override the Display impl for the secret to return a mask.
48 +/// This makes it harder to leak the secret by accident.
49 +impl std::fmt::Display for SessionSecret {
50 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 + write!(f, "***")
52 + }
53 +}
54 +
55 +/// Override the Debug impl for the secret to return a mask.
56 +/// This makes it harder to leak the secret by accident.
57 +impl std::fmt::Debug for SessionSecret {
58 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 + write!(f, "***")
60 + }
61 +}
62 +
63 +/// The `warp` server framework uses [`FromStr`] to deserialize
64 +/// the string from the route.
65 +impl std::str::FromStr for SessionSecret {
66 + type Err = core::convert::Infallible;
67 + fn from_str(s: &str) -> Result<Self, Self::Err> {
68 + String::from_str(s).map(SessionSecret)
69 + }
70 +}
71 +
72 +/// The parameters needed to attempt to join a shared session.
73 +/// This is different from [`viewer::InitPayload`] which
74 +/// is the state that a viewer must pass up _after_ successfully
75 +/// joining a shared session.
76 +#[derive(Clone)]
77 +pub struct JoinSessionLinkArgs {
78 + pub session_id: SessionId,
79 + pub session_secret: SessionSecret,
80 +}
81 +
82 +impl JoinSessionLinkArgs {
83 + // TODO: ideally, the protocol should just generate the full
84 + // link for the client to consume. This will make more sense
85 + // once we move away from app URIs.
86 + pub fn to_join_route(&self) -> String {
87 + format!(
88 + "/sessions/join/{}?pwd={}",
89 + self.session_id,
90 + self.secret_to_string(),
91 + )
92 + }
93 +
94 + /// Returns the [`SessionSecret`] as a [`String`] for joining purposes.
95 + pub fn secret_to_string(&self) -> String {
96 + // We can't use the [`SessionSecret`]'s display because it's overriden to be masked.
97 + self.session_secret.0.to_string()
98 + }
99 +}
src/common/team.rs new
+10
@@ -0,0 +1,10 @@
1 +use serde::{Deserialize, Serialize};
2 +
3 +use super::Role;
4 +
5 +#[derive(Clone, Debug, Deserialize, Serialize)]
6 +pub struct TeamAclData {
7 + pub acl: Role,
8 + pub uid: String,
9 + pub name: String,
10 +}
src/common/telemetry.rs new
+11
@@ -0,0 +1,11 @@
1 +use serde::{Deserialize, Serialize};
2 +use serde_json::Value;
3 +
4 +/// Context about the end-client that we want to attach to telemetry events.
5 +///
6 +/// This is an opaque blob that the client chooses because we want to respect
7 +/// whatever context it wants to declare rather than a custom schema.
8 +/// Since we use Rudderstack, it should still respect the named fields
9 +/// here: https://www.rudderstack.com/docs/event-spec/standard-events/common-fields/#contextual-fields
10 +#[derive(Clone, Debug, Serialize, Deserialize)]
11 +pub struct TelemetryContext(pub Value);
src/common/ui_state.rs new
+255
@@ -0,0 +1,255 @@
1 +use serde::{Deserialize, Serialize};
2 +
3 +use crate::common::ServerConversationToken;
4 +
5 +/// The active base model selection for agent mode.
6 +/// This represents the UI state of which model is selected in the model picker chip.
7 +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
8 +pub struct SelectedAgentModel(String);
9 +
10 +impl SelectedAgentModel {
11 + pub fn new(model_id: impl Into<String>) -> Self {
12 + Self(model_id.into())
13 + }
14 +
15 + pub fn model_id(&self) -> &str {
16 + &self.0
17 + }
18 +}
19 +
20 +/// The selected conversation for agent mode.
21 +/// When agent view is enabled, this represents the conversation that is currently expanded.
22 +/// When agent view is disabled, this represents the conversation the next query will follow up in.
23 +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
24 +pub enum SelectedConversation {
25 + /// An existing conversation identified by a server token
26 + ExistingConversation(ServerConversationToken),
27 + /// The next query will start a new conversation
28 + /// (when agent view is enabled, this looks like an empty expanded view).
29 + #[default]
30 + NewConversation,
31 + /// No conversation selected
32 + /// (when agent view is enabled, this means that no agent view is expanded).
33 + NoConversation,
34 +}
35 +
36 +impl SelectedConversation {
37 + pub fn new(server_token: Option<ServerConversationToken>) -> Self {
38 + match server_token {
39 + Some(token) => Self::ExistingConversation(token),
40 + None => Self::NewConversation,
41 + }
42 + }
43 +
44 + pub fn server_token(&self) -> Option<&ServerConversationToken> {
45 + match self {
46 + Self::ExistingConversation(token) => Some(token),
47 + Self::NewConversation | Self::NoConversation => None,
48 + }
49 + }
50 +
51 + pub fn is_new_conversation(&self) -> bool {
52 + matches!(self, Self::NewConversation)
53 + }
54 +
55 + pub fn is_no_conversation(&self) -> bool {
56 + matches!(self, Self::NoConversation)
57 + }
58 +}
59 +
60 +/// The input type for the universal developer input.
61 +#[derive(Clone, Default, Deserialize, Serialize, PartialEq, Eq, Debug, Copy)]
62 +pub enum InputType {
63 + /// The user input is a shell command.
64 + #[default]
65 + Shell,
66 +
67 + /// The user input is a natural language query to AI.
68 + AI,
69 +}
70 +
71 +/// The input mode for the universal developer input
72 +/// (i.e. the input type and whether the input is locked in said type)
73 +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
74 +pub struct InputMode {
75 + pub input_type: InputType,
76 + pub is_locked: bool,
77 +}
78 +
79 +impl InputMode {
80 + pub fn new(input_type: InputType, is_locked: bool) -> Self {
81 + Self {
82 + input_type,
83 + is_locked,
84 + }
85 + }
86 +}
87 +
88 +/// Whether a CLI agent (e.g. Claude Code, Gemini CLI) is active in the
89 +/// terminal. Synced during shared sessions so viewers see the agent footer.
90 +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
91 +pub enum CLIAgentSessionState {
92 + /// A CLI agent is running.
93 + Active {
94 + /// Serialized `CLIAgent` enum value (e.g. "Claude", "Gemini", "Codex").
95 + cli_agent: String,
96 + /// Whether the CLI agent rich input composer is open.
97 + is_rich_input_open: bool,
98 + },
99 + /// No CLI agent is running (or the previous one ended).
100 + #[default]
101 + Inactive,
102 +}
103 +
104 +/// How the agent is interacting with the current long running command (if at all).
105 +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
106 +pub enum LongRunningCommandAgentInteractionState {
107 + /// The agent is not interacting with any long running command.
108 + NotInteracting,
109 + /// The user started a long running command and tagged the agent into it.
110 + TaggedIn,
111 + /// The agent started and is controlling a long running command.
112 + InControl,
113 +}
114 +
115 +/// The combined state container for universal developer input context.
116 +/// This includes model selection, input mode, and selected conversation.
117 +#[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq, Eq)]
118 +pub struct UniversalDeveloperInputContext {
119 + /// Which agent model is selected as the primary model.
120 + pub selected_model: Option<SelectedAgentModel>,
121 +
122 + /// The input mode for the universal developer input.
123 + pub input_mode: Option<InputMode>,
124 +
125 + /// The selected conversation (identified by server token) for the next agent query.
126 + pub selected_conversation: Option<SelectedConversation>,
127 +
128 + /// How the agent is interacting with the current long running command (if at all).
129 + pub long_running_command_agent_interaction_state:
130 + Option<LongRunningCommandAgentInteractionState>,
131 +
132 + /// Whether auto-approve is enabled for agent actions.
133 + pub auto_approve_agent_actions: Option<bool>,
134 +
135 + /// Whether a CLI agent is active in this terminal.
136 + #[serde(default)]
137 + pub cli_agent_session: CLIAgentSessionState,
138 +}
139 +
140 +/// Update message for universal developer input context - only contains fields that changed.
141 +#[derive(Clone, Debug, Deserialize, Serialize, Default)]
142 +pub struct UniversalDeveloperInputContextUpdate {
143 + #[serde(skip_serializing_if = "Option::is_none")]
144 + pub selected_model: Option<SelectedAgentModel>,
145 +
146 + #[serde(skip_serializing_if = "Option::is_none")]
147 + pub input_mode: Option<InputMode>,
148 +
149 + #[serde(skip_serializing_if = "Option::is_none")]
150 + pub selected_conversation: Option<SelectedConversation>,
151 +
152 + /// How the agent is interacting with the current long running command (if at all).
153 + #[serde(skip_serializing_if = "Option::is_none")]
154 + pub long_running_command_agent_interaction_state:
155 + Option<LongRunningCommandAgentInteractionState>,
156 +
157 + #[serde(skip_serializing_if = "Option::is_none")]
158 + pub auto_approve_agent_actions: Option<bool>,
159 +
160 + /// Whether a CLI agent is active. `None` = no change.
161 + #[serde(skip_serializing_if = "Option::is_none")]
162 + pub cli_agent_session: Option<CLIAgentSessionState>,
163 +}
164 +
165 +impl UniversalDeveloperInputContextUpdate {
166 + /// Returns true if this update would actually change the given cached context.
167 + pub fn changes_cached_context(&self, cached: &UniversalDeveloperInputContext) -> bool {
168 + // We destructure here to ensure that, when new fields are added, we check said fields.
169 + let UniversalDeveloperInputContextUpdate {
170 + selected_model: updated_selected_model,
171 + input_mode: updated_input_mode,
172 + selected_conversation: updated_selected_conversation,
173 + auto_approve_agent_actions: updated_auto_approve_agent_actions,
174 + long_running_command_agent_interaction_state:
175 + updated_long_running_command_agent_interaction_state,
176 + cli_agent_session: updated_cli_agent_session,
177 + } = self;
178 + let UniversalDeveloperInputContext {
179 + selected_model: cached_selected_model,
180 + input_mode: cached_input_mode,
181 + selected_conversation: cached_selected_conversation,
182 + auto_approve_agent_actions: cached_auto_approve_agent_actions,
183 + long_running_command_agent_interaction_state:
184 + cached_long_running_command_agent_interaction_state,
185 + cli_agent_session: cached_cli_agent_session,
186 + } = cached;
187 +
188 + // If any of the fields are present and different from the cached context, return true
189 + // (as the update will change the cached context)
190 + (updated_selected_model.is_some()
191 + && updated_selected_model.as_ref() != cached_selected_model.as_ref())
192 + || (updated_input_mode.is_some()
193 + && updated_input_mode.as_ref() != cached_input_mode.as_ref())
194 + || (updated_selected_conversation.is_some()
195 + && updated_selected_conversation != cached_selected_conversation)
196 + || (updated_auto_approve_agent_actions.is_some()
197 + && updated_auto_approve_agent_actions != cached_auto_approve_agent_actions)
198 + || (updated_long_running_command_agent_interaction_state.is_some()
199 + && updated_long_running_command_agent_interaction_state
200 + != cached_long_running_command_agent_interaction_state)
201 + || (updated_cli_agent_session.is_some()
202 + && updated_cli_agent_session.as_ref() != Some(cached_cli_agent_session))
203 + }
204 +
205 + /// Merges this update into the current context, returning the new merged state.
206 + pub fn merge_into(
207 + self,
208 + current: UniversalDeveloperInputContext,
209 + ) -> UniversalDeveloperInputContext {
210 + let UniversalDeveloperInputContextUpdate {
211 + selected_model: updated_selected_model,
212 + input_mode: updated_input_mode,
213 + selected_conversation: updated_selected_conversation,
214 + auto_approve_agent_actions: updated_auto_approve_agent_actions,
215 + long_running_command_agent_interaction_state:
216 + updated_long_running_command_agent_interaction_state,
217 + cli_agent_session: updated_cli_agent_session,
218 + } = self;
219 + let UniversalDeveloperInputContext {
220 + selected_model: current_selected_model,
221 + input_mode: current_input_mode,
222 + selected_conversation: current_selected_conversation,
223 + auto_approve_agent_actions: current_auto_approve_agent_actions,
224 + long_running_command_agent_interaction_state:
225 + current_long_running_command_agent_interaction_state,
226 + cli_agent_session: current_cli_agent_session,
227 + } = current;
228 +
229 + UniversalDeveloperInputContext {
230 + selected_model: updated_selected_model.or(current_selected_model),
231 + input_mode: updated_input_mode.or(current_input_mode),
232 + selected_conversation: updated_selected_conversation.or(current_selected_conversation),
233 + auto_approve_agent_actions: updated_auto_approve_agent_actions
234 + .or(current_auto_approve_agent_actions),
235 + long_running_command_agent_interaction_state:
236 + updated_long_running_command_agent_interaction_state
237 + .or(current_long_running_command_agent_interaction_state),
238 + cli_agent_session: updated_cli_agent_session.unwrap_or(current_cli_agent_session),
239 + }
240 + }
241 +}
242 +
243 +impl From<UniversalDeveloperInputContext> for UniversalDeveloperInputContextUpdate {
244 + fn from(context: UniversalDeveloperInputContext) -> Self {
245 + Self {
246 + selected_model: context.selected_model,
247 + input_mode: context.input_mode,
248 + selected_conversation: context.selected_conversation,
249 + auto_approve_agent_actions: context.auto_approve_agent_actions,
250 + long_running_command_agent_interaction_state: context
251 + .long_running_command_agent_interaction_state,
252 + cli_agent_session: Some(context.cli_agent_session),
253 + }
254 + }
255 +}
src/common/user.rs new
+46
@@ -0,0 +1,46 @@
1 +use serde::{Deserialize, Serialize};
2 +use uuid::Uuid;
3 +
4 +#[derive(Clone, Debug, Deserialize, Serialize)]
5 +/// Contains information for identifying the end-user.
6 +///
7 +/// Different [`UserID`]'s might correspond to the same end-user;
8 +/// we use the [`UserID`] to translate to a canonical user.
9 +pub struct UserID {
10 + /// Randomly generated ID for the user, which exists whether or not they are logged in.
11 + pub anonymous_id: String,
12 +
13 + /// The client's access token. This is either:
14 + /// * A short-lived firebase ID token (not refresh token).
15 + /// * A Warp API key.
16 + ///
17 + /// [`Some`] iff we know who the end-user is (i.e. they're logged in).
18 + #[serde(rename = "firebase_id_token")]
19 + pub access_token: Option<String>,
20 +}
21 +
22 +impl Default for UserID {
23 + fn default() -> Self {
24 + Self {
25 + anonymous_id: Uuid::new_v4().to_string(),
26 + access_token: None,
27 + }
28 + }
29 +}
30 +
31 +/// A newtype for a firebase uid.
32 +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
33 +#[serde(transparent)]
34 +pub struct FirebaseUid(String);
35 +
36 +impl From<String> for FirebaseUid {
37 + fn from(value: String) -> Self {
38 + Self(value)
39 + }
40 +}
41 +
42 +impl From<FirebaseUid> for String {
43 + fn from(value: FirebaseUid) -> Self {
44 + value.0
45 + }
46 +}
src/common/write_to_pty.rs new
+42
@@ -0,0 +1,42 @@
1 +use super::ParticipantId;
2 +use serde::{Deserialize, Serialize};
3 +
4 +/// A monotonically increasing sequence number to identify sequential writes to the pty.
5 +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
6 +pub struct WriteToPtySeqNo(usize);
7 +
8 +impl WriteToPtySeqNo {
9 + pub fn zero() -> Self {
10 + Self(0)
11 + }
12 +
13 + pub fn advance(&mut self) {
14 + self.0 += 1;
15 + }
16 +
17 + pub fn as_usize(&self) -> usize {
18 + self.0
19 + }
20 +}
21 +
22 +impl From<usize> for WriteToPtySeqNo {
23 + fn from(value: usize) -> Self {
24 + Self(value)
25 + }
26 +}
27 +
28 +#[derive(Clone, Debug, Serialize, Deserialize)]
29 +pub struct WriteToPtyRequestId {
30 + pub participant_id: ParticipantId,
31 + pub op_no: WriteToPtySeqNo,
32 +}
33 +
34 +#[derive(Clone, Debug, Serialize, Deserialize)]
35 +pub enum WriteToPtyFailureReason {
36 + /// The viewer does not have sufficient permissions to write to pty.
37 + InsufficientPermissions,
38 +
39 + /// The buffer for which the write to pty was requested is old.
40 + /// Specificaly, there is a new buffer and the command is no longer in-progress.
41 + StaleBuffer,
42 +}
src/lib.rs new
+10
@@ -0,0 +1,10 @@
1 +//! The session-sharing protocol.
2 +//! All messages defined here are shared across the client and server,
3 +//! and are serialized/deserialized to/from JSON.
4 +//!
5 +//! When modifying them, changes need to be backward-compatible. This usually means
6 +//! defining a default value for every new field added that is used during
7 +//! deserialization when the field is missing.
8 +pub mod common;
9 +pub mod sharer;
10 +pub mod viewer;
src/sharer.rs new
+603
@@ -0,0 +1,603 @@
1 +//! The message types that are communicated between the
2 +//! server and a sharer client.
3 +//!
4 +//! When a client wants to create a shared session, the client
5 +//! will make a request against /sessions/create. The client
6 +//! must then send an [`Initialize`] message with the relevant data
7 +//! to start the shared session. If successful, the server
8 +//! will acknowledge the creation of the shared
9 +//! session via the [`SessionInitialized`] message.
10 +//!
11 +//! Remember to annotate #[serde(default)] to every new field added for backward compatibility,
12 +//! since old clients may not specify new fields expected by the server.
13 +
14 +use crate::common::{
15 + ActivePrompt, ActivePromptUpdate, AgentPromptFailureReason, AgentPromptRequest,
16 + AgentPromptRequestId, BlockId, BufferId, CommandExecutionFailureReason,
17 + CommandExecutionRequestId, ControlAction, ControlActionFailureReason, ControlActionRequestId,
18 + FeatureSupport, InputOperationId, InputReplicaId, InputUpdate, InputUpdateFailureReason,
19 + OrderedTerminalEvent, ParticipantId, ParticipantList, ParticipantPresenceUpdate, Role,
20 + RoleRequestId, RoleRequestResponse, Selection, SelectionUpdate, SessionId, SessionSecret,
21 + TelemetryContext, UniversalDeveloperInputContext, UniversalDeveloperInputContextUpdate, UserID,
22 + WindowSize, WriteToPtyFailureReason, WriteToPtyRequestId,
23 +};
24 +
25 +use super::common::Scrollback;
26 +use byte_unit::Byte;
27 +use serde::{Deserialize, Deserializer, Serialize};
28 +use uuid::Uuid;
29 +
30 +/// Possible reasons why the server might gracefully terminate
31 +/// a shared session.
32 +#[derive(Clone, Serialize, Deserialize, Debug)]
33 +pub enum SessionTerminatedReason {
34 + /// Unknown error occurred. Session cannot continue.
35 + InternalServerError {
36 + /// Details about what happened. This should
37 + /// 1. only be provided to the sharer client,
38 + /// 2. not necessarily be user-facing, and
39 + /// 3. clients should _not_ try to match on the exact message
40 + details: String,
41 + },
42 + /// The session exceeded its size limit.
43 + ExceededSizeLimit,
44 + /// The user does not have any more quota remaining.
45 + NoUserQuotaRemaining {
46 + // This is left as an empty struct to make it
47 + // easier to add fields (e.g. next refresh time)
48 + // in the future in a backwards-compatible way.
49 + },
50 +}
51 +
52 +impl SessionTerminatedReason {
53 + pub fn internal_server_error(details: impl Into<String>) -> Self {
54 + Self::InternalServerError {
55 + details: details.into(),
56 + }
57 + }
58 + pub fn internal_server_error_without_details() -> Self {
59 + Self::internal_server_error(String::new())
60 + }
61 +}
62 +
63 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
64 +/// Client-side reasons for ending the shared session.
65 +pub enum SessionEndedReason {
66 + /// The session was ended gracefully.
67 + EndedBySharer,
68 + /// The sharer was idle for too long.
69 + InactivityLimitReached,
70 + /// The session exceeded its size limit.
71 + // TODO: remove as part of quota enforcement work
72 + ExceededSizeLimit,
73 +}
74 +
75 +#[derive(Deserialize, Serialize, Debug)]
76 +pub enum ReconnectionFailedReason {
77 + /// Unexpected, means something went wrong in the server.
78 + Invalid,
79 + /// The session with the specified ID does not exist.
80 + SessionNotFound,
81 + /// The specified password was incorrect.
82 + WrongPassword,
83 + /// The specified reconnection token was incorrect.
84 + WrongReconnectionToken,
85 + /// The firebase ID of the sharer was missing or doesn't match the original one.
86 + WrongFirebaseUid,
87 + /// The sharer does not have any remaining quota.
88 + NoUserQuotaRemaining,
89 + /// The session is not accessible.
90 + SessionNotAccessible,
91 +}
92 +
93 +#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy)]
94 +pub enum RoleUpdateReason {
95 + #[default]
96 + UpdatedBySharer,
97 + InactivityLimitReached,
98 +}
99 +
100 +#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
101 +pub enum QuotaType {
102 + BytesUsed,
103 + SessionsCreated,
104 +}
105 +
106 +/// The reasons we might fail to initialize a new session.
107 +#[derive(Clone, Serialize, Deserialize, Debug)]
108 +pub enum FailedToInitializeSessionReason {
109 + /// The scrollback exceeds the user's quota.
110 + ScrollbackTooLarge {
111 + // This is left as an empty struct to make it
112 + // easier to add fields (e.g. remaining scrollback size)
113 + // in the future in a backwards-compatible way.
114 + },
115 + /// The sharer does not have any remaining quota.
116 + NoUserQuotaRemaining { quota_type: QuotaType },
117 + /// The sharer could not be attributed to a Warp user.
118 + UserNotFound,
119 + /// Something unexpectedly went wrong.
120 + InternalServerError {
121 + /// Details about what happened. This should
122 + /// not necessarily be user-facing, and clients should
123 + /// _not_ try to match on the exact message.
124 + details: String,
125 + },
126 +}
127 +
128 +impl FailedToInitializeSessionReason {
129 + pub fn internal_server_error_without_details() -> Self {
130 + FailedToInitializeSessionReason::InternalServerError {
131 + details: String::new(),
132 + }
133 + }
134 +}
135 +
136 +// Permission response types are now in common::permissions.
137 +// Re-exported here for backward compatibility.
138 +pub use crate::common::{
139 + AddGuestsResponse, FailedToAddGuestsReason, FailedToRemoveGuestReason,
140 + FailedToUpdatePendingUserRoleReason, FailedToUpdateTeamAccessLevelReason,
141 + LinkAccessLevelUpdateResponse, RemoveGuestResponse, TeamAccessLevelUpdateResponse,
142 + UpdatePendingUserRoleResponse,
143 +};
144 +
145 +#[derive(Clone, Debug, Serialize, Default)]
146 +pub enum SessionSourceType {
147 + /// The session was started by a user directly.
148 + #[default]
149 + User,
150 + /// The session was started in the course of spinning up an ambient agent.
151 + AmbientAgent {
152 + #[serde(default)]
153 + task_id: Option<String>,
154 + },
155 +}
156 +
157 +/// Internal helper that mirrors all wire representations of SessionSourceType
158 +/// (both legacy and new) so we don't recursively call SessionSourceType's
159 +/// custom Deserialize impl.
160 +#[derive(Deserialize)]
161 +#[serde(untagged)]
162 +enum SessionSourceTypeWire {
163 + /// Legacy representation: "User" or "AmbientAgent".
164 + Legacy(LegacySessionSourceType),
165 + /// New representation: externally tagged AmbientAgent with fields, e.g.
166 + /// { "AmbientAgent": { "task_id": "..." } }.
167 + New {
168 + #[serde(rename = "AmbientAgent")]
169 + ambient_agent: AmbientAgentFields,
170 + },
171 +}
172 +
173 +#[derive(Deserialize)]
174 +struct AmbientAgentFields {
175 + #[serde(default)]
176 + task_id: Option<String>,
177 +}
178 +
179 +impl From<SessionSourceTypeWire> for SessionSourceType {
180 + fn from(value: SessionSourceTypeWire) -> Self {
181 + match value {
182 + SessionSourceTypeWire::Legacy(LegacySessionSourceType::User) => SessionSourceType::User,
183 + SessionSourceTypeWire::Legacy(LegacySessionSourceType::AmbientAgent) => {
184 + SessionSourceType::AmbientAgent { task_id: None }
185 + }
186 + SessionSourceTypeWire::New {
187 + ambient_agent: AmbientAgentFields { task_id },
188 + } => SessionSourceType::AmbientAgent { task_id },
189 + }
190 + }
191 +}
192 +
193 +impl<'de> Deserialize<'de> for SessionSourceType {
194 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
195 + where
196 + D: Deserializer<'de>,
197 + {
198 + let wire = SessionSourceTypeWire::deserialize(deserializer)?;
199 + Ok(SessionSourceType::from(wire))
200 + }
201 +}
202 +
203 +#[derive(Clone, Debug, Serialize, Deserialize, Default)]
204 +pub enum LegacySessionSourceType {
205 + #[default]
206 + User,
207 + AmbientAgent,
208 +}
209 +
210 +impl From<&SessionSourceType> for LegacySessionSourceType {
211 + fn from(value: &SessionSourceType) -> Self {
212 + match value {
213 + SessionSourceType::User => LegacySessionSourceType::User,
214 + SessionSourceType::AmbientAgent { .. } => LegacySessionSourceType::AmbientAgent,
215 + }
216 + }
217 +}
218 +
219 +/// Configures the lifetime of the session after sharing ends.
220 +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default)]
221 +pub enum Lifetime {
222 + /// The session is deleted immediately when sharing ends.
223 + #[default]
224 + Ephemeral,
225 + /// The session persists after sharing ends.
226 + ///
227 + /// It is not specified how long a lingering session is available for after it ends.
228 + /// Currently, all session contents expire after one week, but this is a server implementation
229 + /// detail that clients must not rely on. In the future, we may expose a lifetime option that
230 + /// includes a client-provided TTL.
231 + Lingering,
232 +}
233 +
234 +/// The initial state that the sharer must supply when starting
235 +/// a shared session.
236 +#[derive(Debug, Deserialize, Serialize)]
237 +pub struct InitPayload {
238 + pub scrollback: Scrollback,
239 +
240 + pub active_prompt: ActivePrompt,
241 +
242 + pub window_size: WindowSize,
243 +
244 + pub user_id: UserID,
245 +
246 + /// What the sharer currently has selected for presence.
247 + pub selection: Selection,
248 +
249 + pub init_block_id: BlockId,
250 +
251 + pub input_replica_id: InputReplicaId,
252 +
253 + pub telemetry_context: Option<TelemetryContext>,
254 +
255 + #[serde(default)]
256 + pub lifetime: Lifetime,
257 +
258 + /// The universal developer input context state.
259 + #[serde(default)]
260 + pub universal_developer_input_context: Option<UniversalDeveloperInputContext>,
261 +
262 + /// The source type for this shared session (i.e. user or ambient agent).
263 + #[serde(default)]
264 + pub source_type: SessionSourceType,
265 +
266 + /// Client feature support declaration.
267 + #[serde(default)]
268 + pub feature_support: FeatureSupport,
269 +}
270 +
271 +/// The reconnection token for a shared session.
272 +/// A sharer must specify this to reconnect to the session and resume sharing.
273 +/// The client should treat this as some opaque string.
274 +#[derive(Hash, Serialize, Deserialize, Eq, PartialEq, Clone)]
275 +#[serde(transparent)]
276 +pub struct ReconnectToken(String);
277 +impl ReconnectToken {
278 + pub fn new() -> Self {
279 + Self::default()
280 + }
281 +}
282 +
283 +impl Default for ReconnectToken {
284 + fn default() -> Self {
285 + Self(Uuid::new_v4().to_string())
286 + }
287 +}
288 +
289 +/// Override the Display impl for the token to return a mask.
290 +/// This makes it harder to leak the token by accident.
291 +impl std::fmt::Display for ReconnectToken {
292 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 + write!(f, "***")
294 + }
295 +}
296 +
297 +/// Override the Debug impl for the token to return a mask.
298 +/// This makes it harder to leak the token by accident.
299 +impl std::fmt::Debug for ReconnectToken {
300 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 + write!(f, "***")
302 + }
303 +}
304 +
305 +/// The `warp` server framework uses [`FromStr`] to deserialize
306 +/// the string from the route.
307 +impl std::str::FromStr for ReconnectToken {
308 + type Err = core::convert::Infallible;
309 + fn from_str(s: &str) -> Result<Self, Self::Err> {
310 + String::from_str(s).map(ReconnectToken)
311 + }
312 +}
313 +
314 +/// Payload sharer must supply to reconnect to an existing shared session
315 +/// when the websocket was terminated by the server.
316 +#[derive(Debug, Deserialize, Serialize)]
317 +pub struct ReconnectPayload {
318 + // TODO: Remove in favour of ACLs
319 + pub session_secret: SessionSecret,
320 + pub reconnect_token: ReconnectToken,
321 +
322 + pub user_id: UserID,
323 +
324 + /// The ID of the latest block when reconnecting.
325 + /// This allows the sharer to catch up on any
326 + /// missed input updates while they were disconnected.
327 + pub latest_block_id: BlockId,
328 +
329 + /// What the sharer currently has selected for presence.
330 + pub selection: Selection,
331 +
332 + /// Client feature support declaration.
333 + #[serde(default)]
334 + pub feature_support: FeatureSupport,
335 +}
336 +
337 +/// The possible messages sent from server to client (sharer).
338 +#[derive(Deserialize, Serialize)]
339 +pub enum DownstreamMessage {
340 + /// The server sends this message when the session was successfully created.
341 + SessionInitialized {
342 + session_id: SessionId,
343 + // TODO: Remove in favour of ACLs
344 + session_secret: SessionSecret,
345 + reconnect_token: ReconnectToken,
346 + /// The ID assigned to the sharer
347 + sharer_id: ParticipantId,
348 + /// The Firebase UID assigned to the sharer.
349 + sharer_firebase_uid: String,
350 + },
351 +
352 + /// The server denied the initialization request. No further messages will be processed.
353 + FailedToInitializeSession {
354 + reason: FailedToInitializeSessionReason,
355 + },
356 +
357 + /// The session was terminated. No further messages
358 + /// will be processed.
359 + SessionTerminated { reason: SessionTerminatedReason },
360 +
361 + /// The server accepted the reconnection request.
362 + SessionReconnected {
363 + /// The last event no received by the server.
364 + /// The sharer can use this to update the server with any newer events created while disconnected.
365 + last_received_event_no: Option<usize>,
366 + participant_list: ParticipantList,
367 + },
368 +
369 + /// The server denied the reconnection request. No further messages will be processed.
370 + FailedToReconnect { reason: ReconnectionFailedReason },
371 +
372 + /// The server sends this to confirm it has fully processed events up to the latest_processed_event_no,
373 + /// and the sharer can safely remove them from memory.
374 + EventsProcessedAck { latest_processed_event_no: usize },
375 +
376 + /// Sent when the list of participants in the shared session changes.
377 + ParticipantListUpdated(ParticipantList),
378 +
379 + /// Sent when a participant's presence changes.
380 + ParticipantPresenceUpdated(ParticipantPresenceUpdate),
381 +
382 + /// The participant (identified by `participant_id`) requested the `role` role.
383 + RoleRequested {
384 + participant_id: ParticipantId,
385 + request_id: RoleRequestId,
386 + role: Role,
387 + },
388 +
389 + /// The participant (identified by `participant_id`) has cancelled their role request.
390 + RoleRequestCancelled {
391 + participant_id: ParticipantId,
392 + request_id: RoleRequestId,
393 + },
394 +
395 + /// A participant's (identified by `participant_id`) role was updated.
396 + ParticipantRoleChanged {
397 + participant_id: ParticipantId,
398 + role: Role,
399 + },
400 +
401 + /// A participant requested a control action (e.g. cancel conversation).
402 + ControlActionRequested {
403 + participant_id: ParticipantId,
404 + request_id: ControlActionRequestId,
405 + action: ControlAction,
406 + },
407 +
408 + /// The input was updated by a participant.
409 + /// When we receive our own update, we can treat it as an ack.
410 + InputUpdated(InputUpdate),
411 +
412 + /// The rejection was successfully applied (does not need to be retried by the client).
413 + InputUpdateRejectedAck { id: InputOperationId },
414 +
415 + /// A participant requested that the given `command` be run in the given buffer.
416 + CommandExecutionRequested {
417 + id: CommandExecutionRequestId,
418 + participant_id: ParticipantId,
419 + buffer_id: BufferId,
420 + command: String,
421 + },
422 +
423 + /// A participant requested to write to the pty, specifically for a long running command.
424 + WriteToPtyRequested {
425 + id: WriteToPtyRequestId,
426 + bytes: Vec<u8>,
427 + },
428 +
429 + /// A participant requested to send an agent prompt.
430 + AgentPromptRequested {
431 + id: AgentPromptRequestId,
432 + participant_id: ParticipantId,
433 + request: AgentPromptRequest,
434 + },
435 +
436 + /// The sharer's link access level update request was responded to.
437 + LinkAccessLevelUpdateResponse(LinkAccessLevelUpdateResponse),
438 +
439 + /// The request to add guests was responded to.
440 + AddGuestsResponse(AddGuestsResponse),
441 +
442 + /// The request to remove a guest was responded to.
443 + RemoveGuestResponse(RemoveGuestResponse),
444 +
445 + /// The request to update a pending user role was responded to.
446 + UpdatePendingUserRoleResponse(UpdatePendingUserRoleResponse),
447 +
448 + /// The sharer's team access level update request was responsed to.
449 + TeamAccessLevelUpdateResponse(TeamAccessLevelUpdateResponse),
450 +
451 + /// Update to the universal developer input context from sharer or editor viewers.
452 + UniversalDeveloperInputContextUpdated(UniversalDeveloperInputContextUpdate),
453 +
454 + /// A viewer reported its terminal size.
455 + /// Used for remote-control sessions where the viewer's viewport should drive the PTY size.
456 + ViewerTerminalSizeReported {
457 + participant_id: ParticipantId,
458 + window_size: WindowSize,
459 + },
460 +
461 + /// A response to a [`UpstreamMessage::Ping`].
462 + /// Used to demonstrate that the server is still alive.
463 + Pong { data: Vec<u8> },
464 +}
465 +
466 +impl DownstreamMessage {
467 + pub fn from_json(json: &str) -> serde_json::Result<Self> {
468 + serde_json::from_str(json)
469 + }
470 +
471 + pub fn to_json(&self) -> serde_json::Result<String> {
472 + serde_json::to_string(self)
473 + }
474 +}
475 +
476 +/// The possible messages sent from client (sharer) to server.
477 +#[derive(Debug, Deserialize, Serialize)]
478 +pub enum UpstreamMessage {
479 + /// The client sends this message to start a shared session.
480 + /// supplying any necessary initial state.
481 + /// TODO: add size info, etc.
482 + Initialize(InitPayload),
483 +
484 + /// A heartbeat message to demonstrate that the
485 + /// client is still alive.
486 + Ping { data: Vec<u8> },
487 +
488 + /// The client sends this message to explicitly end a session
489 + /// and notify viewers before the websocket closes.
490 + EndSession { reason: SessionEndedReason },
491 +
492 + /// Update to the sharer's active prompt.
493 + UpdateActivePrompt(ActivePromptUpdate),
494 +
495 + /// Update to the universal developer input context (model selection, etc.).
496 + UpdateUniversalDeveloperInputContext(UniversalDeveloperInputContextUpdate),
497 +
498 + /// Sent when there is any ordered terminal event.
499 + OrderedTerminalEvent(OrderedTerminalEvent),
500 +
501 + /// Sent to reconnect to the server after disconnection.
502 + Reconnect(ReconnectPayload),
503 +
504 + /// Sent when the sharer changes what they have selected.
505 + UpdateSelection(SelectionUpdate),
506 +
507 + /// Changes the participant's (identified by `participant_id`) role.
508 + UpdateRole {
509 + participant_id: ParticipantId,
510 + role: Role,
511 + },
512 +
513 + /// Changes the user's role (applied to all participants with the same UID).
514 + UpdateUserRole { user_uid: String, role: Role },
515 +
516 + /// Changes the pending user's role (applied to all participants with the same UID).
517 + UpdatePendingUserRole { email: String, role: Role },
518 +
519 + /// Responds to the participant's (identified by `participant_id`) role request.
520 + RespondToRoleRequest {
521 + participant_id: ParticipantId,
522 + request_id: RoleRequestId,
523 + response: RoleRequestResponse,
524 + },
525 +
526 + /// Updates all participants' roles to be [Role::Reader].
527 + UpdateAllRolesToReader { reason: RoleUpdateReason },
528 +
529 + /// The sharer updated the input.
530 + UpdateInput(InputUpdate),
531 +
532 + /// The given operation should be undone on all participants.
533 + RejectInputUpdate {
534 + id: InputOperationId,
535 + reason: InputUpdateFailureReason,
536 + },
537 +
538 + /// The given command execution request was denied for the specified `reason`.
539 + RejectCommandExecutionRequest {
540 + id: CommandExecutionRequestId,
541 + participant_id: ParticipantId,
542 + reason: CommandExecutionFailureReason,
543 + },
544 +
545 + /// The given write to pty request was denied for the specified `reason`.
546 + RejectWriteToPtyRequest {
547 + id: WriteToPtyRequestId,
548 + reason: WriteToPtyFailureReason,
549 + },
550 +
551 + /// The given agent prompt request was denied for the specified `reason`.
552 + RejectAgentPromptRequest {
553 + id: AgentPromptRequestId,
554 + participant_id: ParticipantId,
555 + reason: AgentPromptFailureReason,
556 + },
557 +
558 + /// The given control action request was denied for the specified `reason`.
559 + RejectControlActionRequest {
560 + participant_id: ParticipantId,
561 + request_id: ControlActionRequestId,
562 + reason: ControlActionFailureReason,
563 + },
564 +
565 + /// The sharer updated the session's link permissions.
566 + UpdateLinkAccessLevel { role: Option<Role> },
567 +
568 + /// The sharer updated the session's team permissions.
569 + UpdateTeamAccessLevel {
570 + team_uid: String,
571 + role: Option<Role>,
572 + },
573 +
574 + /// The sharer added users as session guests by email.
575 + AddGuests { emails: Vec<String>, role: Role },
576 +
577 + /// The sharer removed a user as a session guest.
578 + RemoveGuest { user_uid: String },
579 +
580 + /// The sharer removed a pending user as a session guest.
581 + RemovePendingGuest { email: String },
582 +}
583 +
584 +impl UpstreamMessage {
585 + pub fn from_json(json: &str) -> serde_json::Result<Self> {
586 + serde_json::from_str(json)
587 + }
588 +
589 + pub fn to_json(&self) -> serde_json::Result<String> {
590 + serde_json::to_string(self)
591 + }
592 +
593 + pub fn num_bytes(&self) -> Byte {
594 + match self {
595 + UpstreamMessage::Initialize(init_payload) => init_payload.scrollback.num_bytes(),
596 + UpstreamMessage::OrderedTerminalEvent(ordered_terminal_event) => {
597 + ordered_terminal_event.num_bytes()
598 + }
599 + UpstreamMessage::UpdateInput(input_update) => input_update.num_bytes(),
600 + _ => Byte::from_u64(0),
601 + }
602 + }
603 +}
src/viewer.rs new
+431
@@ -0,0 +1,431 @@
1 +//! The message types that are communicated between the
2 +//! server and a viewer client.
3 +//!
4 +//! When a client wants to join a shared session, the client
5 +//! will make a request against /sessions/join/:uuid. The client
6 +//! must then send an [`Initialize`] message with the relevant data
7 +//! to join the shared session. If successful, the server
8 +//! will acknowledge the joining of the shared
9 +//! session via the [`JoinedSuccessfully`] message.
10 +//!
11 +//! To reconnect to a shared session, the viewer can use the same
12 +//! join endpoint, setting the init payload appropriately.
13 +//! The server will acknowledge the rejoining via the
14 +//! [`RejoinedSuccessfully`] message.
15 +
16 +use crate::{
17 + common::{
18 + ActivePrompt, ActivePromptUpdate, AgentAttachment, AgentPromptFailureReason,
19 + AgentPromptRequest, AgentPromptRequestId, BlockId, BufferId, CommandExecutionFailureReason,
20 + CommandExecutionRequestId, ControlAction, ControlActionFailureReason, FeatureSupport,
21 + InputOperationId, InputReplicaId, InputUpdate, InputUpdateFailureReason,
22 + LinkAccessLevelUpdateResponse, OrderedTerminalEvent, ParticipantId, ParticipantList,
23 + ParticipantPresenceUpdate, Role, RoleRequestId, RoleRequestResponse, Scrollback,
24 + SelectionUpdate, TeamAccessLevelUpdateResponse, TeamAclData, TelemetryContext,
25 + UniversalDeveloperInputContext, UniversalDeveloperInputContextUpdate, UserID, WindowSize,
26 + WriteToPtyFailureReason, WriteToPtyRequestId,
27 + },
28 + sharer::{self, LegacySessionSourceType, SessionSourceType},
29 +};
30 +use byte_unit::Byte;
31 +use serde::{Deserialize, Serialize};
32 +
33 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
34 +/// Sent by sharer client or server
35 +/// when the shared session has been ended.
36 +pub enum SessionEndedReason {
37 + /// Unexpected, means something went wrong in the server.
38 + InternalServerError,
39 + /// The session was ended gracefully.
40 + EndedBySharer,
41 + /// The sharer was idle for too long.
42 + InactivityLimitReached,
43 +
44 + /// DEPRECATED
45 + ExceededSizeLimit,
46 +}
47 +
48 +#[derive(Serialize, Deserialize, Clone, Debug)]
49 +pub enum FailedToJoinReason {
50 + /// Unexpected, means something went wrong in the server.
51 + Invalid,
52 + SessionNotFound,
53 + WrongPassword,
54 + InternalServerError,
55 + MaxNumberOfParticipantsReached,
56 + SessionNotAccessible,
57 +}
58 +
59 +#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy)]
60 +pub enum RoleUpdatedReason {
61 + #[default]
62 + UpdatedBySharer,
63 + InactivityLimitReached,
64 +}
65 +
66 +impl From<sharer::RoleUpdateReason> for RoleUpdatedReason {
67 + fn from(value: sharer::RoleUpdateReason) -> Self {
68 + match value {
69 + sharer::RoleUpdateReason::UpdatedBySharer => RoleUpdatedReason::UpdatedBySharer,
70 + sharer::RoleUpdateReason::InactivityLimitReached => {
71 + RoleUpdatedReason::InactivityLimitReached
72 + }
73 + }
74 + }
75 +}
76 +
77 +#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
78 +pub enum ViewerRemovedReason {
79 + LostAccess,
80 +}
81 +
82 +/// The initial state that the viewer must supply when joining or rejoining
83 +/// a shared session.
84 +#[derive(Debug, Deserialize, Serialize)]
85 +pub struct InitPayload {
86 + /// The ID previously assigned to this viewer when joining.
87 + /// Should be specified if the viewer is rejoining.
88 + pub viewer_id: Option<ParticipantId>,
89 + pub user_id: UserID,
90 + /// If the viewer is reconnecting, they should specify the last event no received.
91 + /// The server will only send events after this event no.
92 + pub last_received_event_no: Option<usize>,
93 +
94 + /// The ID of the last block the viewer has seen.
95 + /// Should only be specified when re-joining.
96 + pub latest_block_id: Option<BlockId>,
97 +
98 + pub telemetry_context: Option<TelemetryContext>,
99 +
100 + /// Client feature support declaration.
101 + #[serde(default)]
102 + pub feature_support: FeatureSupport,
103 +}
104 +
105 +/// The possible messages sent from server to client (viewer).
106 +#[derive(Serialize, Deserialize, Clone)]
107 +pub enum DownstreamMessage {
108 + /// The server sends this message when the session was successfully joined.
109 + /// TODO: add initial state to pass to viewer (e.g. size info)
110 + JoinedSuccessfully {
111 + scrollback: Box<Scrollback>,
112 + /// The sharer's active prompt.
113 + active_prompt: ActivePrompt,
114 + /// The latest event no of the session the viewer will be catching up to.
115 + /// If None, there are no events to catch up to.
116 + latest_event_no: Option<usize>,
117 + window_size: WindowSize,
118 +
119 + participant_list: Box<ParticipantList>,
120 +
121 + /// The ID assigned to this viewer
122 + viewer_id: ParticipantId,
123 + /// The Firebase UID assigned to this viewer.
124 + viewer_firebase_uid: String,
125 +
126 + /// The block ID of the first block after scrollback.
127 + /// The viewer can use this to identify buffer updates for
128 + /// the first block.
129 + init_block_id: BlockId,
130 +
131 + input_replica_id: InputReplicaId,
132 +
133 + /// The universal developer input context (model selection, etc.).
134 + #[serde(default)]
135 + universal_developer_input_context: Option<UniversalDeveloperInputContext>,
136 +
137 + /// The legacy source type for this shared session (i.e. user or ambient agent).
138 + #[serde(default)]
139 + #[deprecated(note = "please use `detailed_source_type` instead")]
140 + source_type: LegacySessionSourceType,
141 +
142 + /// The detailed source type for this shared session.
143 + #[serde(default)]
144 + detailed_source_type: SessionSourceType,
145 + },
146 +
147 + /// The server sends this message when the session was successfully rejoined.
148 + RejoinedSuccessfully {
149 + participant_list: Box<ParticipantList>,
150 + },
151 +
152 + /// Sent when the viewer fails to join the shared session.
153 + /// The client should not expect any more messages after this.
154 + FailedToJoin { reason: FailedToJoinReason },
155 +
156 + /// Sent when the shared session has been ended.
157 + /// The client should not expect any more messages after this.
158 + SessionEnded { reason: SessionEndedReason },
159 +
160 + /// Update to the sharer's active prompt.
161 + ActivePromptUpdated(ActivePromptUpdate),
162 +
163 + /// Update to the universal developer input context (model selection, etc.) from sharer or editor viewers.
164 + UniversalDeveloperInputContextUpdated(UniversalDeveloperInputContextUpdate),
165 +
166 + /// Sent when there is any ordered terminal event.
167 + /// These messages are only sent _after_ [`DownstreamMessage::JoinedSuccessfully`].
168 + OrderedTerminalEvent(OrderedTerminalEvent),
169 +
170 + /// Sent when the list of participants in the shared session changes.
171 + ParticipantListUpdated(ParticipantList),
172 +
173 + /// Sent when a participant's presence changes.
174 + ParticipantPresenceUpdated(ParticipantPresenceUpdate),
175 +
176 + /// The server has acknowledged the role request and sent it to the sharer.
177 + /// There can only be at most one role request in flight per participant.
178 + RoleRequestInFlight(RoleRequestId),
179 +
180 + /// The viewer's role request was responded to.
181 + RoleRequestResponse(RoleRequestResponse),
182 +
183 + /// A participant's (identified by `participant_id`) role was updated.
184 + ParticipantRoleChanged {
185 + participant_id: ParticipantId,
186 + reason: RoleUpdatedReason,
187 + role: Role,
188 + },
189 +
190 + /// The input was updated by a participant.
191 + /// When we receive our own update, we can treat it as an ack.
192 + InputUpdated(InputUpdate),
193 +
194 + /// An input operation was rejected and should be undone.
195 + InputUpdateRejected {
196 + id: InputOperationId,
197 + reason: InputUpdateFailureReason,
198 + },
199 +
200 + /// The server has acknowledged the command execution request and sent it to the sharer.
201 + /// There can only be at most one command execution request in flight per participant.
202 + CommandExecutionRequestInFlight(CommandExecutionRequestId),
203 +
204 + /// The viewer's command execution request failed.
205 + /// Note: there is no "success" response; that is implicitly handled
206 + /// by the fact that the command is executed.
207 + CommandExecutionRequestFailed {
208 + id: CommandExecutionRequestId,
209 + reason: CommandExecutionFailureReason,
210 + },
211 +
212 + /// The viewer's write to pty request failed.
213 + WriteToPtyRequestFailed { reason: WriteToPtyFailureReason },
214 +
215 + /// The server has acknowledged the agent prompt request and sent it to the sharer.
216 + AgentPromptRequestInFlight(AgentPromptRequestId),
217 +
218 + /// The viewer's agent prompt request failed.
219 + /// Note: there is no "success" response; that is implicitly handled
220 + /// by the fact that agent response events start streaming.
221 + AgentPromptRequestFailed { reason: AgentPromptFailureReason },
222 +
223 + /// The viewer's control action request failed.
224 + ControlActionRequestFailed { reason: ControlActionFailureReason },
225 +
226 + /// The viewer was removed from the session by the sharer.
227 + ViewerRemoved { reason: ViewerRemovedReason },
228 +
229 + /// Deprecated: superseded by [`DownstreamMessage::LinkAccessLevelUpdateResponse`].
230 + /// Kept temporarily for backward compatibility with older clients. Remove once
231 + /// all clients handle `LinkAccessLevelUpdateResponse`.
232 + LinkAccessLevelUpdated { role: Option<Role> },
233 +
234 + /// Deprecated: superseded by [`DownstreamMessage::TeamAccessLevelUpdateResponse`].
235 + /// Kept temporarily for backward compatibility with older clients. Remove once
236 + /// all clients handle `TeamAccessLevelUpdateResponse`.
237 + TeamAccessLevelUpdated {
238 + /// The UID of the updated team.
239 + team_uid: String,
240 + /// The ACL of the updated team. None if team has no ACL.
241 + team_acl: Option<TeamAclData>,
242 + },
243 +
244 + /// The viewer's link access level update request was responded to.
245 + LinkAccessLevelUpdateResponse(crate::common::LinkAccessLevelUpdateResponse),
246 +
247 + /// The request to add guests was responded to.
248 + AddGuestsResponse(crate::common::AddGuestsResponse),
249 +
250 + /// The request to remove a guest was responded to.
251 + RemoveGuestResponse(crate::common::RemoveGuestResponse),
252 +
253 + /// The request to update a pending user role was responded to.
254 + UpdatePendingUserRoleResponse(crate::common::UpdatePendingUserRoleResponse),
255 +
256 + /// The viewer's team access level update request was responded to.
257 + TeamAccessLevelUpdateResponse(crate::common::TeamAccessLevelUpdateResponse),
258 +
259 + /// A response to a [`UpstreamMessage::Ping`].
260 + /// Used to demonstrate that the server is still alive.
261 + Pong { data: Vec<u8> },
262 +}
263 +
264 +impl DownstreamMessage {
265 + pub fn from_json(json: &str) -> serde_json::Result<Self> {
266 + serde_json::from_str(json)
267 + }
268 +
269 + pub fn to_json(&self) -> serde_json::Result<String> {
270 + serde_json::to_string(self)
271 + }
272 +
273 + /// Downgrades all `Role::Full` fields to `Role::Executor`.
274 + /// Used for backward compatibility with clients that don't support the Full role.
275 + #[allow(deprecated)]
276 + pub fn downgrade_full_roles(&mut self) {
277 + match self {
278 + Self::JoinedSuccessfully {
279 + participant_list, ..
280 + } => participant_list.downgrade_full_roles(),
281 + Self::RejoinedSuccessfully { participant_list } => {
282 + participant_list.downgrade_full_roles()
283 + }
284 + Self::ParticipantListUpdated(list) => list.downgrade_full_roles(),
285 + Self::ParticipantRoleChanged { role, .. } => role.downgrade_full(),
286 + Self::RoleRequestResponse(RoleRequestResponse::Approved { new_role }) => {
287 + new_role.downgrade_full()
288 + }
289 + Self::LinkAccessLevelUpdated { role: Some(role) } => {
290 + role.downgrade_full();
291 + }
292 + Self::TeamAccessLevelUpdated {
293 + team_acl: Some(team_acl),
294 + ..
295 + } => {
296 + team_acl.acl.downgrade_full();
297 + }
298 + Self::LinkAccessLevelUpdateResponse(LinkAccessLevelUpdateResponse::Ok {
299 + role: Some(role),
300 + }) => {
301 + role.downgrade_full();
302 + }
303 + Self::TeamAccessLevelUpdateResponse(TeamAccessLevelUpdateResponse::Success {
304 + team_acl: Some(team_acl),
305 + ..
306 + }) => {
307 + team_acl.acl.downgrade_full();
308 + }
309 + _ => {}
310 + }
311 + }
312 +}
313 +
314 +/// The possible messages sent from client (viewer) to server.
315 +#[derive(Debug, Serialize, Deserialize)]
316 +pub enum UpstreamMessage {
317 + /// The client sends this message to join the shared session.
318 + Initialize(InitPayload),
319 +
320 + /// A heartbeat message to demonstrate that the
321 + /// client is still alive.
322 + Ping { data: Vec<u8> },
323 +
324 + /// Sent when the viewer changes what they have selected.
325 + UpdateSelection(SelectionUpdate),
326 +
327 + /// The viewer is requesting a new role.
328 + RequestRole(Role),
329 +
330 + /// The viewer no longer wants to change roles.
331 + CancelRoleRequest(RoleRequestId),
332 +
333 + /// The viewer updated their input.
334 + /// This is an optimistic update and thus was already applied on the viewer's client.
335 + UpdateInput(InputUpdate),
336 +
337 + /// The viewer is requesting the sharer to execute the provided command
338 + /// in the given buffer.
339 + ExecuteCommand {
340 + buffer_id: BufferId,
341 + command: String,
342 + },
343 +
344 + /// The viewer is requesting to write to the pty,
345 + /// specifically to a long running command.
346 + WriteToPty {
347 + request_id: WriteToPtyRequestId,
348 + bytes: Vec<u8>,
349 + },
350 +
351 + /// The viewer is requesting to send an agent prompt.
352 + /// If there's an existing in-flight request for the same conversation,
353 + /// it will be cancelled and replaced with this new request.
354 + SendAgentPrompt(AgentPromptRequest),
355 +
356 + /// The viewer (with Editor role) is updating the universal developer input context.
357 + UpdateUniversalDeveloperInputContext(UniversalDeveloperInputContextUpdate),
358 +
359 + /// The viewer is requesting a one-off control action to be applied to the shared session.
360 + SendControlAction(ControlAction),
361 +
362 + /// The viewer has reauthenticated.
363 + Reauthenticated { user_id: UserID },
364 +
365 + /// The viewer updated the session's link permissions.
366 + UpdateLinkAccessLevel { role: Option<Role> },
367 +
368 + /// The viewer updated the session's team permissions.
369 + UpdateTeamAccessLevel {
370 + team_uid: String,
371 + role: Option<Role>,
372 + },
373 +
374 + /// The viewer added users as session guests by email.
375 + AddGuests { emails: Vec<String>, role: Role },
376 +
377 + /// The viewer removed a user as a session guest.
378 + RemoveGuest { user_uid: String },
379 +
380 + /// The viewer removed a pending user as a session guest.
381 + RemovePendingGuest { email: String },
382 +
383 + /// The viewer changed a user's role.
384 + UpdateUserRole { user_uid: String, role: Role },
385 +
386 + /// The viewer changed a pending user's role.
387 + UpdatePendingUserRole { email: String, role: Role },
388 +
389 + /// The viewer is reporting its terminal size to the sharer.
390 + /// Used for remote-control sessions where the viewer's viewport should drive the PTY size.
391 + ReportTerminalSize { window_size: WindowSize },
392 +}
393 +
394 +impl UpstreamMessage {
395 + pub fn from_json(json: &str) -> serde_json::Result<Self> {
396 + serde_json::from_str(json)
397 + }
398 +
399 + pub fn to_json(&self) -> serde_json::Result<String> {
400 + serde_json::to_string(self)
401 + }
402 +
403 + pub fn num_bytes(&self) -> Byte {
404 + match self {
405 + UpstreamMessage::UpdateInput(input_update) => input_update.num_bytes(),
406 + UpstreamMessage::ExecuteCommand { command, .. } => command.len().into(),
407 + UpstreamMessage::WriteToPty { bytes, .. } => bytes.len().into(),
408 + UpstreamMessage::SendAgentPrompt(request) => {
409 + // Count prompt length + attachments
410 + let prompt_bytes: Byte = request.prompt.len().into();
411 + let attachments_bytes: Byte = request
412 + .attachments
413 + .iter()
414 + .map(|att| match att {
415 + AgentAttachment::PlainText { content } => content.len(),
416 + // Block's are already included in the shared session thus far,
417 + // so we do not have to count them again here.
418 + AgentAttachment::BlockReference { .. } => 0,
419 + // FileReference is just IDs — the actual data is in GCS.
420 + AgentAttachment::FileReference { .. } => 0,
421 + })
422 + .sum::<usize>()
423 + .into();
424 + prompt_bytes
425 + .add(attachments_bytes)
426 + .unwrap_or(u64::MAX.into())
427 + }
428 + _ => Byte::from_u64(0),
429 + }
430 + }
431 +}