1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[repr(u8)]
10pub enum ClientFrameType {
11 AuthResponse = 0,
13 UnlockVault = 1,
15 SecretsList = 2,
17 SecretsGet = 3,
19 SecretsStore = 4,
21 SecretsDelete = 5,
23 SecretsPeek = 6,
25 SecretsSetPolicy = 7,
27 SecretsSetDisabled = 8,
29 SecretsDeleteCredential = 9,
31 SecretsHasTotp = 10,
33 SecretsSetupTotp = 11,
35 SecretsVerifyTotp = 12,
37 SecretsRemoveTotp = 13,
39 Reload = 14,
41 Cancel = 15,
43 Chat = 16,
45 ToolApprovalResponse = 17,
47 UserPromptResponse = 18,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[repr(u8)]
54pub enum ServerFrameType {
55 AuthChallenge = 0,
57 AuthResult = 1,
59 AuthLocked = 2,
61 Hello = 3,
63 Status = 4,
65 VaultUnlocked = 5,
67 SecretsListResult = 6,
69 SecretsStoreResult = 7,
71 SecretsGetResult = 8,
73 SecretsDeleteResult = 9,
75 SecretsPeekResult = 10,
77 SecretsSetPolicyResult = 11,
79 SecretsSetDisabledResult = 12,
81 SecretsDeleteCredentialResult = 13,
83 SecretsHasTotpResult = 14,
85 SecretsSetupTotpResult = 15,
87 SecretsVerifyTotpResult = 16,
89 SecretsRemoveTotpResult = 17,
91 ReloadResult = 18,
93 Error = 19,
95 Info = 20,
97 StreamStart = 21,
99 Chunk = 22,
101 ThinkingStart = 23,
103 ThinkingDelta = 24,
105 ThinkingEnd = 25,
107 ToolCall = 26,
109 ToolResult = 27,
111 ResponseDone = 28,
113 ToolApprovalRequest = 29,
115 UserPromptRequest = 30,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[repr(u8)]
122pub enum StatusType {
123 ModelConfigured = 0,
125 CredentialsLoaded = 1,
127 CredentialsMissing = 2,
129 ModelConnecting = 3,
131 ModelReady = 4,
133 ModelError = 5,
135 NoModel = 6,
137 VaultLocked = 7,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ClientFrame {
148 pub frame_type: ClientFrameType,
149 pub payload: ClientPayload,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub enum ClientPayload {
155 Empty,
156 AuthChallenge {
157 method: String,
158 },
159 AuthResponse {
160 code: String,
161 },
162 UnlockVault {
163 password: String,
164 },
165 Reload,
166 Chat {
167 messages: Vec<super::types::ChatMessage>,
168 },
169 SecretsList,
170 SecretsGet {
171 key: String,
172 },
173 SecretsStore {
174 key: String,
175 value: String,
176 },
177 SecretsDelete {
178 key: String,
179 },
180 SecretsPeek {
181 name: String,
182 },
183 SecretsSetPolicy {
184 name: String,
185 policy: String,
186 skills: Vec<String>,
187 },
188 SecretsSetDisabled {
189 name: String,
190 disabled: bool,
191 },
192 SecretsDeleteCredential {
193 name: String,
194 },
195 SecretsHasTotp,
196 SecretsSetupTotp,
197 SecretsVerifyTotp {
198 code: String,
199 },
200 SecretsRemoveTotp,
201 ToolApprovalResponse {
202 id: String,
203 approved: bool,
204 },
205 UserPromptResponse {
206 id: String,
207 dismissed: bool,
208 value: crate::user_prompt_types::PromptResponseValue,
209 },
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ServerFrame {
215 pub frame_type: ServerFrameType,
216 pub payload: ServerPayload,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub enum ServerPayload {
222 Empty,
223 Hello {
224 agent: String,
225 settings_dir: String,
226 vault_locked: bool,
227 provider: Option<String>,
228 model: Option<String>,
229 },
230 AuthChallenge {
231 method: String,
232 },
233 AuthResult {
234 ok: bool,
235 message: Option<String>,
236 retry: Option<bool>,
237 },
238 AuthLocked {
239 message: String,
240 retry_after: Option<u64>,
241 },
242 Status {
243 status: StatusType,
244 detail: String,
245 },
246 VaultUnlocked {
247 ok: bool,
248 message: Option<String>,
249 },
250 SecretsListResult {
251 ok: bool,
252 entries: Vec<SecretEntryDto>,
253 },
254 SecretsStoreResult {
255 ok: bool,
256 message: String,
257 },
258 SecretsGetResult {
259 ok: bool,
260 key: String,
261 value: Option<String>,
262 message: Option<String>,
263 },
264 SecretsDeleteResult {
265 ok: bool,
266 message: Option<String>,
267 },
268 SecretsPeekResult {
269 ok: bool,
270 fields: Vec<(String, String)>,
271 message: Option<String>,
272 },
273 SecretsSetPolicyResult {
274 ok: bool,
275 message: Option<String>,
276 },
277 SecretsSetDisabledResult {
278 ok: bool,
279 message: Option<String>,
280 },
281 SecretsDeleteCredentialResult {
282 ok: bool,
283 message: Option<String>,
284 },
285 SecretsHasTotpResult {
286 has_totp: bool,
287 },
288 SecretsSetupTotpResult {
289 ok: bool,
290 uri: Option<String>,
291 message: Option<String>,
292 },
293 SecretsVerifyTotpResult {
294 ok: bool,
295 message: Option<String>,
296 },
297 SecretsRemoveTotpResult {
298 ok: bool,
299 message: Option<String>,
300 },
301 ReloadResult {
302 ok: bool,
303 provider: String,
304 model: String,
305 message: Option<String>,
306 },
307 Error {
308 ok: bool,
309 message: String,
310 },
311 Info {
312 message: String,
313 },
314 StreamStart,
315 Chunk {
316 delta: String,
317 },
318 ThinkingStart,
319 ThinkingDelta {
320 delta: String,
321 },
322 ThinkingEnd,
323 ToolCall {
324 id: String,
325 name: String,
326 arguments: String,
327 },
328 ToolResult {
329 id: String,
330 name: String,
331 result: String,
332 is_error: bool,
333 },
334 ResponseDone {
335 ok: bool,
336 },
337 ToolApprovalRequest {
338 id: String,
339 name: String,
340 arguments: String,
341 },
342 UserPromptRequest {
343 id: String,
344 prompt: crate::user_prompt_types::UserPrompt,
345 },
346}
347
348#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350pub struct SecretEntryDto {
351 pub name: String,
352 pub label: String,
353 pub kind: String,
354 pub policy: String,
355 pub disabled: bool,
356}
357
358pub fn serialize_frame<T: serde::Serialize>(frame: &T) -> Result<Vec<u8>, String> {
364 bincode::serde::encode_to_vec(frame, bincode::config::standard()).map_err(|e| e.to_string())
365}
366
367pub fn deserialize_frame<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, String> {
369 let (result, _) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())
370 .map_err(|e| e.to_string())?;
371 Ok(result)
372}
373
374#[macro_export]
376macro_rules! send_binary_frame {
377 ($writer:expr, $frame:expr) => {{
378 let bytes = $crate::gateway::serialize_frame(&$frame)
379 .map_err(|e| anyhow::anyhow!("Failed to serialize frame: {}", e))?;
380 $writer
381 .send(tokio_tungstenite::tungstenite::Message::Binary(bytes))
382 .await
383 .map_err(|e| anyhow::anyhow!("Failed to send frame: {}", e))
384 }};
385}
386
387#[macro_export]
389macro_rules! parse_binary_client_frame {
390 ($bytes:expr) => {{
391 $crate::gateway::deserialize_frame::<$crate::gateway::ClientFrame>($bytes)
392 .map_err(|e| anyhow::anyhow!("Failed to parse client frame: {}", e))
393 }};
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 mod serialization {
401 use super::*;
402
403 #[test]
404 fn test_server_frame_type_values() {
405 assert_eq!(ServerFrameType::AuthChallenge as u8, 0);
406 assert_eq!(ServerFrameType::AuthResult as u8, 1);
407 assert_eq!(ServerFrameType::AuthLocked as u8, 2);
408 assert_eq!(ServerFrameType::Hello as u8, 3);
409 assert_eq!(ServerFrameType::Status as u8, 4);
410 assert_eq!(ServerFrameType::VaultUnlocked as u8, 5);
411 assert_eq!(ServerFrameType::SecretsListResult as u8, 6);
412 assert_eq!(ServerFrameType::SecretsStoreResult as u8, 7);
413 assert_eq!(ServerFrameType::SecretsGetResult as u8, 8);
414 assert_eq!(ServerFrameType::SecretsDeleteResult as u8, 9);
415 assert_eq!(ServerFrameType::SecretsPeekResult as u8, 10);
416 assert_eq!(ServerFrameType::SecretsSetPolicyResult as u8, 11);
417 assert_eq!(ServerFrameType::SecretsSetDisabledResult as u8, 12);
418 assert_eq!(ServerFrameType::SecretsDeleteCredentialResult as u8, 13);
419 assert_eq!(ServerFrameType::SecretsHasTotpResult as u8, 14);
420 assert_eq!(ServerFrameType::SecretsSetupTotpResult as u8, 15);
421 assert_eq!(ServerFrameType::SecretsVerifyTotpResult as u8, 16);
422 assert_eq!(ServerFrameType::SecretsRemoveTotpResult as u8, 17);
423 assert_eq!(ServerFrameType::ReloadResult as u8, 18);
424 assert_eq!(ServerFrameType::Error as u8, 19);
425 assert_eq!(ServerFrameType::Info as u8, 20);
426 assert_eq!(ServerFrameType::StreamStart as u8, 21);
427 assert_eq!(ServerFrameType::Chunk as u8, 22);
428 assert_eq!(ServerFrameType::ThinkingStart as u8, 23);
429 assert_eq!(ServerFrameType::ThinkingDelta as u8, 24);
430 assert_eq!(ServerFrameType::ThinkingEnd as u8, 25);
431 assert_eq!(ServerFrameType::ToolCall as u8, 26);
432 assert_eq!(ServerFrameType::ToolResult as u8, 27);
433 assert_eq!(ServerFrameType::ResponseDone as u8, 28);
434 assert_eq!(ServerFrameType::ToolApprovalRequest as u8, 29);
435 assert_eq!(ServerFrameType::UserPromptRequest as u8, 30);
436 }
437
438 #[test]
439 fn test_client_frame_type_values() {
440 assert_eq!(ClientFrameType::AuthResponse as u8, 0);
441 assert_eq!(ClientFrameType::UnlockVault as u8, 1);
442 assert_eq!(ClientFrameType::SecretsList as u8, 2);
443 assert_eq!(ClientFrameType::SecretsGet as u8, 3);
444 assert_eq!(ClientFrameType::SecretsStore as u8, 4);
445 assert_eq!(ClientFrameType::SecretsDelete as u8, 5);
446 assert_eq!(ClientFrameType::SecretsPeek as u8, 6);
447 assert_eq!(ClientFrameType::SecretsSetPolicy as u8, 7);
448 assert_eq!(ClientFrameType::SecretsSetDisabled as u8, 8);
449 assert_eq!(ClientFrameType::SecretsDeleteCredential as u8, 9);
450 assert_eq!(ClientFrameType::SecretsHasTotp as u8, 10);
451 assert_eq!(ClientFrameType::SecretsSetupTotp as u8, 11);
452 assert_eq!(ClientFrameType::SecretsVerifyTotp as u8, 12);
453 assert_eq!(ClientFrameType::SecretsRemoveTotp as u8, 13);
454 assert_eq!(ClientFrameType::Reload as u8, 14);
455 assert_eq!(ClientFrameType::Cancel as u8, 15);
456 assert_eq!(ClientFrameType::Chat as u8, 16);
457 assert_eq!(ClientFrameType::ToolApprovalResponse as u8, 17);
458 assert_eq!(ClientFrameType::UserPromptResponse as u8, 18);
459 }
460
461 #[test]
462 fn test_status_type_values() {
463 assert_eq!(StatusType::ModelConfigured as u8, 0);
464 assert_eq!(StatusType::CredentialsLoaded as u8, 1);
465 assert_eq!(StatusType::CredentialsMissing as u8, 2);
466 assert_eq!(StatusType::ModelConnecting as u8, 3);
467 assert_eq!(StatusType::ModelReady as u8, 4);
468 assert_eq!(StatusType::ModelError as u8, 5);
469 assert_eq!(StatusType::NoModel as u8, 6);
470 assert_eq!(StatusType::VaultLocked as u8, 7);
471 }
472
473 #[test]
474 fn test_server_frame_roundtrip_hello() {
475 let frame = ServerFrame {
476 frame_type: ServerFrameType::Hello,
477 payload: ServerPayload::Hello {
478 agent: "test-agent".into(),
479 settings_dir: "/tmp/settings".into(),
480 vault_locked: false,
481 provider: Some("anthropic".into()),
482 model: Some("claude-3".into()),
483 },
484 };
485
486 let bytes = serialize_frame(&frame).expect("serialize should succeed");
487 let decoded: ServerFrame =
488 deserialize_frame(&bytes).expect("deserialize should succeed");
489
490 match decoded.payload {
491 ServerPayload::Hello {
492 agent,
493 settings_dir,
494 vault_locked,
495 provider,
496 model,
497 } => {
498 assert_eq!(agent, "test-agent");
499 assert_eq!(settings_dir, "/tmp/settings");
500 assert!(!vault_locked);
501 assert_eq!(provider, Some("anthropic".into()));
502 assert_eq!(model, Some("claude-3".into()));
503 }
504 _ => panic!("Expected Hello payload"),
505 }
506 }
507
508 #[test]
509 fn test_server_frame_roundtrip_chunk() {
510 let frame = ServerFrame {
511 frame_type: ServerFrameType::Chunk,
512 payload: ServerPayload::Chunk {
513 delta: "Hello, world!".into(),
514 },
515 };
516
517 let bytes = serialize_frame(&frame).expect("serialize should succeed");
518 let decoded: ServerFrame =
519 deserialize_frame(&bytes).expect("deserialize should succeed");
520
521 match decoded.payload {
522 ServerPayload::Chunk { delta } => {
523 assert_eq!(delta, "Hello, world!");
524 }
525 _ => panic!("Expected Chunk payload"),
526 }
527 }
528
529 #[test]
530 fn test_server_frame_roundtrip_status() {
531 let frame = ServerFrame {
532 frame_type: ServerFrameType::Status,
533 payload: ServerPayload::Status {
534 status: StatusType::ModelReady,
535 detail: "Connected to Claude 3.5 Sonnet".into(),
536 },
537 };
538
539 let bytes = serialize_frame(&frame).expect("serialize should succeed");
540 let decoded: ServerFrame =
541 deserialize_frame(&bytes).expect("deserialize should succeed");
542
543 match decoded.payload {
544 ServerPayload::Status { status, detail } => {
545 assert_eq!(status, StatusType::ModelReady);
546 assert_eq!(detail, "Connected to Claude 3.5 Sonnet");
547 }
548 _ => panic!("Expected Status payload"),
549 }
550 }
551
552 #[test]
553 fn test_server_frame_roundtrip_auth_result() {
554 let frame = ServerFrame {
555 frame_type: ServerFrameType::AuthResult,
556 payload: ServerPayload::AuthResult {
557 ok: true,
558 message: Some("Authenticated successfully".into()),
559 retry: None,
560 },
561 };
562
563 let bytes = serialize_frame(&frame).expect("serialize should succeed");
564 let decoded: ServerFrame =
565 deserialize_frame(&bytes).expect("deserialize should succeed");
566
567 match decoded.payload {
568 ServerPayload::AuthResult { ok, message, retry } => {
569 assert!(ok);
570 assert_eq!(message, Some("Authenticated successfully".into()));
571 assert!(retry.is_none());
572 }
573 _ => panic!("Expected AuthResult payload"),
574 }
575 }
576
577 #[test]
578 fn test_client_frame_roundtrip_chat() {
579 let frame = ClientFrame {
580 frame_type: ClientFrameType::Chat,
581 payload: ClientPayload::Empty,
582 };
583
584 let bytes = serialize_frame(&frame).expect("serialize should succeed");
585 let decoded: ClientFrame =
586 deserialize_frame(&bytes).expect("deserialize should succeed");
587
588 assert_eq!(decoded.frame_type, ClientFrameType::Chat);
589 matches!(decoded.payload, ClientPayload::Empty);
590 }
591
592 #[test]
593 fn test_client_frame_roundtrip_secrets_store() {
594 let frame = ClientFrame {
595 frame_type: ClientFrameType::SecretsStore,
596 payload: ClientPayload::SecretsStore {
597 key: "OPENAI_API_KEY".into(),
598 value: "sk-test123".into(),
599 },
600 };
601
602 let bytes = serialize_frame(&frame).expect("serialize should succeed");
603 let decoded: ClientFrame =
604 deserialize_frame(&bytes).expect("deserialize should succeed");
605
606 match decoded.payload {
607 ClientPayload::SecretsStore { key, value } => {
608 assert_eq!(key, "OPENAI_API_KEY");
609 assert_eq!(value, "sk-test123");
610 }
611 _ => panic!("Expected SecretsStore payload"),
612 }
613 }
614
615 #[test]
616 fn test_secret_entry_dto_roundtrip() {
617 let entry = SecretEntryDto {
618 name: "api_key".into(),
619 label: "OpenAI API Key".into(),
620 kind: "ApiKey".into(),
621 policy: "always".into(),
622 disabled: false,
623 };
624
625 let json = serde_json::to_string(&entry).expect("JSON serialize should succeed");
626 let decoded: SecretEntryDto =
627 serde_json::from_str(&json).expect("JSON deserialize should succeed");
628
629 assert_eq!(decoded.name, "api_key");
630 assert_eq!(decoded.label, "OpenAI API Key");
631 assert_eq!(decoded.kind, "ApiKey");
632 assert_eq!(decoded.policy, "always");
633 assert!(!decoded.disabled);
634 }
635
636 #[test]
637 fn test_user_prompt_response_bincode_roundtrip() {
638 use crate::user_prompt_types::PromptResponseValue;
639
640 let frame = ClientFrame {
641 frame_type: ClientFrameType::UserPromptResponse,
642 payload: ClientPayload::UserPromptResponse {
643 id: "call_456".into(),
644 dismissed: false,
645 value: PromptResponseValue::Text("hello world".into()),
646 },
647 };
648 let bytes = serialize_frame(&frame).expect("serialize should succeed");
649 let decoded: ClientFrame =
650 deserialize_frame(&bytes).expect("deserialize should succeed");
651 match decoded.payload {
652 ClientPayload::UserPromptResponse { id, dismissed, value } => {
653 assert_eq!(id, "call_456");
654 assert!(!dismissed);
655 assert_eq!(value, PromptResponseValue::Text("hello world".into()));
656 }
657 _ => panic!("Expected UserPromptResponse payload"),
658 }
659 }
660
661 #[test]
662 fn test_server_user_prompt_request_bincode_roundtrip() {
663 use crate::user_prompt_types::{UserPrompt, PromptType};
664
665 let prompt = UserPrompt {
666 id: "call_789".into(),
667 title: "What is your name?".into(),
668 description: Some("Please enter your full name".into()),
669 prompt_type: PromptType::TextInput {
670 placeholder: Some("John Doe".into()),
671 default: None,
672 },
673 };
674
675 let frame = ServerFrame {
676 frame_type: ServerFrameType::UserPromptRequest,
677 payload: ServerPayload::UserPromptRequest {
678 id: "call_789".into(),
679 prompt: prompt.clone(),
680 },
681 };
682
683 let bytes = serialize_frame(&frame).expect("serialize should succeed");
684 let decoded: ServerFrame =
685 deserialize_frame(&bytes).expect("deserialize should succeed");
686
687 assert_eq!(decoded.frame_type, ServerFrameType::UserPromptRequest);
688 match decoded.payload {
689 ServerPayload::UserPromptRequest { id, prompt: p } => {
690 assert_eq!(id, "call_789");
691 assert_eq!(p.title, "What is your name?");
692 assert_eq!(p.description, Some("Please enter your full name".into()));
693 assert!(matches!(p.prompt_type, PromptType::TextInput { .. }));
694 }
695 _ => panic!("Expected UserPromptRequest payload"),
696 }
697 }
698
699 #[test]
700 fn test_client_frame_roundtrip_auth_response() {
701 let frame = ClientFrame {
702 frame_type: ClientFrameType::AuthResponse,
703 payload: ClientPayload::AuthResponse {
704 code: "123456".into(),
705 },
706 };
707
708 let bytes = serialize_frame(&frame).expect("serialize should succeed");
709 let decoded: ClientFrame =
710 deserialize_frame(&bytes).expect("deserialize should succeed");
711
712 assert_eq!(decoded.frame_type, ClientFrameType::AuthResponse);
713 match decoded.payload {
714 ClientPayload::AuthResponse { code } => {
715 assert_eq!(code, "123456");
716 }
717 _ => panic!("Expected AuthResponse payload"),
718 }
719 }
720
721 #[test]
722 fn test_server_frame_roundtrip_auth_challenge() {
723 let frame = ServerFrame {
724 frame_type: ServerFrameType::AuthChallenge,
725 payload: ServerPayload::AuthChallenge {
726 method: "totp".into(),
727 },
728 };
729
730 let bytes = serialize_frame(&frame).expect("serialize should succeed");
731 let decoded: ServerFrame =
732 deserialize_frame(&bytes).expect("deserialize should succeed");
733
734 assert_eq!(decoded.frame_type, ServerFrameType::AuthChallenge);
735 match decoded.payload {
736 ServerPayload::AuthChallenge { method } => {
737 assert_eq!(method, "totp");
738 }
739 _ => panic!("Expected AuthChallenge payload"),
740 }
741 }
742
743 #[test]
744 fn test_server_tool_call_bincode_roundtrip() {
745 let frame = ServerFrame {
746 frame_type: ServerFrameType::ToolCall,
747 payload: ServerPayload::ToolCall {
748 id: "call_001".into(),
749 name: "read_file".into(),
750 arguments: r#"{"path":"/tmp/test"}"#.into(),
751 },
752 };
753
754 let bytes = serialize_frame(&frame).expect("serialize should succeed");
755 let decoded: ServerFrame =
756 deserialize_frame(&bytes).expect("deserialize should succeed");
757
758 match decoded.payload {
759 ServerPayload::ToolCall { id, name, arguments } => {
760 assert_eq!(id, "call_001");
761 assert_eq!(name, "read_file");
762 assert_eq!(arguments, r#"{"path":"/tmp/test"}"#);
763 }
764 _ => panic!("Expected ToolCall payload"),
765 }
766 }
767 }
768}