1use super::*;
2
3const MAX_REQUEST_ID_BYTES: usize = 256;
4
5#[derive(Debug, Clone, PartialEq, Serialize)]
7pub struct ClientFrame {
8 pub version: u16,
9 #[serde(flatten)]
10 pub message: ClientMessage,
11}
12
13impl<'de> Deserialize<'de> for ClientFrame {
14 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
15 where
16 D: serde::Deserializer<'de>,
17 {
18 let (version, message) = deserialize_frame(deserializer)?;
19 if let Some(request_id) = message.get("request_id")
20 && !request_id
21 .as_str()
22 .is_some_and(|id| !id.is_empty() && id.len() <= MAX_REQUEST_ID_BYTES)
23 {
24 return Err(D::Error::custom("request ID must be 1–256 bytes"));
25 }
26 let message = serde_json::from_value(message).map_err(D::Error::custom)?;
27 Ok(Self { version, message })
28 }
29}
30
31impl ClientFrame {
32 #[must_use]
34 pub const fn new(message: ClientMessage) -> Self {
35 Self {
36 version: PROTOCOL_VERSION,
37 message,
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(tag = "type", rename_all = "snake_case")]
45#[non_exhaustive]
46pub enum ClientMessage {
47 SetDesktopRuntime {
48 request_id: String,
49 enabled: bool,
50 },
51 DesktopControlReply {
52 request_id: String,
53 response: Value,
54 },
55 Pair {
56 code: String,
57 client_label: String,
58 client_kind: ClientKind,
59 },
60 Authenticate {
61 token: String,
62 client_kind: ClientKind,
63 },
64 ListClients {
65 request_id: String,
66 },
67 UnpairClient {
68 request_id: String,
69 client_id: String,
70 },
71 ListSessions {
72 request_id: String,
73 },
74 ListBotSessions {
75 request_id: String,
76 bot_id: String,
77 },
78 CreateSession {
79 request_id: String,
80 workspace: PathBuf,
81 bot_ids: Vec<String>,
82 },
83 CreateWorkspaceDirectory {
84 request_id: String,
85 parent: PathBuf,
86 name: String,
87 },
88 OpenSession {
89 request_id: String,
90 session_id: String,
91 last_sequence: Option<u64>,
92 },
93 GetSessionHistory {
94 request_id: String,
95 session_id: String,
96 before_sequence: Option<u64>,
97 },
98 ReassignSession {
99 request_id: String,
100 session_id: String,
101 bot_id: String,
102 },
103 RenameSession {
104 request_id: String,
105 session_id: String,
106 title: String,
107 },
108 AttachSessionFolder {
109 request_id: String,
110 session_id: String,
111 folder: PathBuf,
112 },
113 SetSessionPinned {
114 request_id: String,
115 session_id: String,
116 pinned: bool,
117 },
118 DeleteSessions {
119 request_id: String,
120 session_ids: Vec<String>,
121 },
122 Submit {
123 session_id: String,
124 submission: Submission,
125 },
126 StartRealtimeVoice {
127 request_id: String,
128 session_id: String,
129 offer_sdp: String,
130 },
131 EndRealtimeVoice {
132 session_id: String,
133 voice_id: String,
134 },
135 GetContributions {
136 request_id: String,
137 },
138 SubmitContribution {
139 request_id: String,
140 operation: Op,
141 },
142 BeginSessionFileUpload {
143 request_id: String,
144 session_id: String,
145 name: String,
146 size: u64,
147 media_type: String,
148 },
149 UploadSessionFileChunk {
150 request_id: String,
151 session_id: String,
152 upload_id: String,
153 offset: u64,
154 #[serde(with = "base64_bytes")]
155 data: Vec<u8>,
156 },
157 FinishSessionFileUpload {
158 request_id: String,
159 session_id: String,
160 upload_id: String,
161 },
162 DeleteSessionFile {
163 request_id: String,
164 session_id: String,
165 file_id: String,
166 },
167 ListSessionFiles {
168 request_id: String,
169 session_id: String,
170 },
171 ReadSessionFile {
172 request_id: String,
173 session_id: String,
174 file_id: String,
175 offset: u64,
176 max_bytes: usize,
177 },
178 CreateBot {
179 request_id: String,
180 name: String,
181 description: String,
182 },
183 ListBots {
184 request_id: String,
185 },
186 UpdateBot {
187 request_id: String,
188 id: String,
189 expected_revision: u64,
190 name: String,
191 description: String,
192 tint: ProviderTint,
193 config: AgentComposition,
194 },
195 DeleteBot {
196 request_id: String,
197 id: String,
198 expected_revision: u64,
199 },
200 ConfigureBotDefaults {
201 request_id: String,
202 expected_revision: u64,
203 config: AgentComposition,
204 },
205 InstallExtension {
206 request_id: String,
207 source: String,
208 reference: Option<String>,
209 subdirectory: Option<String>,
210 },
211 UpdateExtension {
212 request_id: String,
213 id: String,
214 },
215 UninstallExtension {
216 request_id: String,
217 id: String,
218 },
219 TrustExtensionHooks {
220 request_id: String,
221 id: String,
222 expected_digest: String,
223 },
224 RevokeExtensionHooksTrust {
225 request_id: String,
226 id: String,
227 expected_digest: String,
228 },
229 ProbeGitCredential {
230 request_id: String,
231 target: String,
232 },
233 ApproveGitCredential {
234 request_id: String,
235 target: String,
236 username: String,
237 token: String,
238 },
239 ListSshIdentities {
240 request_id: String,
241 },
242 GenerateSshIdentity {
243 request_id: String,
244 },
245 GetGitDiff {
246 request_id: String,
247 session_id: String,
248 scope: GitDiffScope,
249 },
250 SwitchGitBranch {
251 request_id: String,
252 session_id: String,
253 branch: String,
254 },
255 ListDirectories {
256 request_id: String,
257 path: PathBuf,
258 include_files: bool,
259 },
260 ListWorkspaceFiles {
261 request_id: String,
262 session_id: String,
263 scope: WorkspaceFileScope,
264 },
265 ReadWorkspaceFile {
266 request_id: String,
267 session_id: String,
268 path: String,
269 offset: u64,
270 max_bytes: usize,
271 },
272 WriteWorkspaceFile {
273 request_id: String,
274 session_id: String,
275 path: String,
276 content: String,
277 },
278 ClearProviderCredential {
279 request_id: String,
280 instance: String,
281 },
282 SetProviderCredential {
283 request_id: String,
284 instance: String,
285 provider: String,
286 api_key: String,
287 expires_at: Option<u64>,
288 },
289 SetProviderEndpointCredential {
290 request_id: String,
291 instance: String,
292 provider: String,
293 base_url: String,
294 api_key: String,
295 expires_at: Option<u64>,
296 },
297 RegisterProvider {
298 request_id: String,
299 config: ProviderConfig,
300 label: String,
301 tint: ProviderTint,
302 model_ids: Vec<String>,
303 reasoning_efforts: Vec<String>,
304 },
305 RemoveProvider {
306 request_id: String,
307 instance: String,
308 },
309 CreatePairingCode {
310 request_id: String,
311 },
312 StartProviderLogin {
313 request_id: String,
314 provider: String,
315 },
316 GetProfile {
317 request_id: String,
318 include_provider_usage: bool,
319 },
320 CreateRoutine {
321 request_id: String,
322 bot_id: String,
323 workspace: PathBuf,
324 instructions: String,
325 schedule: RoutineSchedule,
326 ends_at: Option<i64>,
327 },
328 ListRoutines {
329 request_id: String,
330 bot_id: Option<String>,
331 },
332 UpdateRoutine {
333 request_id: String,
334 id: String,
335 bot_id: String,
336 workspace: PathBuf,
337 instructions: String,
338 schedule: RoutineSchedule,
339 ends_at: Option<i64>,
340 enabled: bool,
341 },
342 DeleteRoutine {
343 request_id: String,
344 id: String,
345 },
346 RunRoutine {
347 request_id: String,
348 id: String,
349 },
350 ListRoutineHistory {
351 request_id: String,
352 id: Option<String>,
353 },
354 DeleteRoutineRun {
355 request_id: String,
356 id: String,
357 },
358 GetRoutineRunPreview {
359 request_id: String,
360 id: String,
361 before_sequence: Option<u64>,
362 },
363}
364
365#[derive(Debug, Clone, PartialEq, Serialize)]
367pub struct ServerFrame {
368 pub version: u16,
369 #[serde(flatten)]
370 pub message: ServerMessage,
371}
372
373impl<'de> Deserialize<'de> for ServerFrame {
374 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
375 where
376 D: serde::Deserializer<'de>,
377 {
378 let (version, message) = deserialize_frame(deserializer)?;
379 let message = serde_json::from_value(message).map_err(D::Error::custom)?;
380 Ok(Self { version, message })
381 }
382}
383
384impl ServerFrame {
385 #[must_use]
387 pub const fn new(message: ServerMessage) -> Self {
388 Self {
389 version: PROTOCOL_VERSION,
390 message,
391 }
392 }
393}
394
395#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397#[serde(tag = "type", rename_all = "snake_case")]
398#[non_exhaustive]
399pub enum ServerMessage {
400 DesktopControlRequested {
401 request_id: String,
402 execution_id: String,
403 session_id: String,
404 request: Value,
405 },
406 DesktopControlEnded {
407 execution_id: String,
408 },
409 Paired {
410 client_id: String,
411 token: String,
412 },
413 Authenticated,
414 Ready {
415 payload: ReadyPayload,
416 },
417 SessionOpened {
418 request_id: String,
419 payload: SessionReadyPayload,
420 },
421 SessionReplayComplete {
422 request_id: String,
423 session_id: String,
424 },
425 SessionHistory {
426 request_id: String,
427 session_id: String,
428 records: Vec<RecordedEvent>,
429 next_before_sequence: Option<u64>,
430 },
431 SessionChanged {
432 payload: SessionReadyPayload,
433 },
434 RealtimeVoiceStarted {
435 request_id: String,
436 session_id: String,
437 voice_id: String,
438 answer_sdp: String,
439 },
440 RealtimeVoiceFailed {
441 request_id: String,
442 session_id: String,
443 message: String,
444 },
445 RealtimeVoiceEnded {
446 session_id: String,
447 voice_id: String,
448 reason: Option<String>,
449 },
450 GatewayConfigured {
451 request_id: String,
452 payload: ReadyPayload,
453 },
454 Contributions {
455 request_id: String,
456 contributions: Vec<FrontendContribution>,
457 },
458 Accepted {
459 request_id: String,
460 },
461 SessionFileUploadReady {
462 request_id: String,
463 session_id: String,
464 upload_id: String,
465 max_chunk_bytes: usize,
466 },
467 SessionFileUploadChunkAccepted {
468 request_id: String,
469 session_id: String,
470 upload_id: String,
471 next_offset: u64,
472 },
473 SessionFileUploadCompleted {
474 request_id: String,
475 session_id: String,
476 file: SessionFileReference,
477 },
478 SessionFiles {
479 request_id: String,
480 session_id: String,
481 files: Vec<SessionFileRecord>,
482 },
483 SessionFileChunk {
484 request_id: String,
485 session_id: String,
486 file_id: String,
487 offset: u64,
488 #[serde(with = "base64_bytes")]
489 data: Vec<u8>,
490 next_offset: Option<u64>,
491 },
492 Rejected {
493 request_id: String,
494 code: String,
495 message: String,
496 fatal: bool,
497 },
498 AgentEvent {
499 session_id: String,
500 record: RecordedEvent,
501 },
502 Sessions {
503 #[serde(default, skip_serializing_if = "Option::is_none")]
504 request_id: Option<String>,
505 sessions: Vec<SessionRecord>,
506 },
507 BackgroundApprovals {
508 approvals: Vec<BackgroundApproval>,
509 },
510
511 BotSessions {
512 request_id: String,
513 bot_id: String,
514 sessions: Vec<SessionRecord>,
515 },
516 Bots {
517 #[serde(default, skip_serializing_if = "Option::is_none")]
518 request_id: Option<String>,
519 bots: Vec<BotRecord>,
520 },
521
522 Clients {
523 request_id: String,
524 current_client_id: String,
525 clients: Vec<ClientStatus>,
526 },
527 ProviderCredentialCleared {
528 request_id: String,
529 instance: String,
530 },
531 ProviderCredentialSaved {
532 request_id: String,
533 instance: String,
534 provider: String,
535 },
536 PairingCode {
537 request_id: String,
538 code: String,
539 expires_at: i64,
540 },
541 ProviderLoginStarted {
542 request_id: String,
543 login_id: String,
544 provider: String,
545 verification_url: String,
546 user_code: String,
547 },
548 ProviderLoginFinished {
549 request_id: String,
550 login_id: String,
551 provider: String,
552 },
553 GitCredentialStatus {
554 request_id: String,
555 available: bool,
556 #[serde(skip_serializing_if = "Option::is_none")]
557 username: Option<String>,
558 },
559 SshIdentities {
560 request_id: String,
561 identities: Vec<SshIdentityRecord>,
562 },
563 SshIdentityGenerated {
564 request_id: String,
565 identity: SshIdentityRecord,
566 public_key: String,
567 },
568 Profile {
569 request_id: String,
570 profile: ProfileSnapshot,
571 },
572 GitDiff {
573 request_id: String,
574 session_id: String,
575 scope: GitDiffScope,
576 diff: String,
577 },
578 Directories {
579 request_id: String,
580 listing: DirectoryListing,
581 },
582 WorkspaceFiles {
583 request_id: String,
584 session_id: String,
585 files: Vec<WorkspaceFileRecord>,
586 truncated: bool,
587 },
588 WorkspaceFileChunk {
589 request_id: String,
590 session_id: String,
591 path: String,
592 offset: u64,
593 #[serde(with = "base64_bytes")]
594 data: Vec<u8>,
595 next_offset: Option<u64>,
596 },
597 Routines {
598 request_id: String,
599 routines: Vec<Routine>,
600 },
601 RoutineHistory {
602 request_id: String,
603 runs: Vec<RoutineRun>,
604 },
605 RoutineRunPreview {
606 request_id: String,
607 preview: RoutineRunPreview,
608 },
609 Error {
610 code: String,
611 message: String,
612 fatal: bool,
613 },
614}