1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ControlRequest {
15 pub request_id: String,
17 pub request: ControlRequestPayload,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "subtype", rename_all = "snake_case")]
24pub enum ControlRequestPayload {
25 CanUseTool(ToolPermissionRequest),
27 HookCallback(HookCallbackRequest),
29 McpMessage(McpMessageRequest),
31 Initialize(InitializeRequest),
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
52pub struct Permission {
53 #[serde(rename = "type")]
55 pub permission_type: String,
56 pub destination: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub mode: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub behavior: Option<String>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub rules: Option<Vec<PermissionRule>>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
71pub struct PermissionRule {
72 #[serde(rename = "toolName")]
74 pub tool_name: String,
75 #[serde(rename = "ruleContent")]
77 pub rule_content: String,
78}
79
80impl Permission {
81 pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
94 Permission {
95 permission_type: "addRules".to_string(),
96 destination: "session".to_string(),
97 mode: None,
98 behavior: Some("allow".to_string()),
99 rules: Some(vec![PermissionRule {
100 tool_name: tool_name.into(),
101 rule_content: rule_content.into(),
102 }]),
103 }
104 }
105
106 pub fn allow_tool_with_destination(
116 tool_name: impl Into<String>,
117 rule_content: impl Into<String>,
118 destination: impl Into<String>,
119 ) -> Self {
120 Permission {
121 permission_type: "addRules".to_string(),
122 destination: destination.into(),
123 mode: None,
124 behavior: Some("allow".to_string()),
125 rules: Some(vec![PermissionRule {
126 tool_name: tool_name.into(),
127 rule_content: rule_content.into(),
128 }]),
129 }
130 }
131
132 pub fn set_mode(mode: impl Into<String>, destination: impl Into<String>) -> Self {
142 Permission {
143 permission_type: "setMode".to_string(),
144 destination: destination.into(),
145 mode: Some(mode.into()),
146 behavior: None,
147 rules: None,
148 }
149 }
150
151 pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
170 Permission {
171 permission_type: suggestion.suggestion_type.clone(),
172 destination: suggestion.destination.clone(),
173 mode: suggestion.mode.clone(),
174 behavior: suggestion.behavior.clone(),
175 rules: suggestion.rules.as_ref().map(|rules| {
176 rules
177 .iter()
178 .filter_map(|v| {
179 Some(PermissionRule {
180 tool_name: v.get("toolName")?.as_str()?.to_string(),
181 rule_content: v.get("ruleContent")?.as_str()?.to_string(),
182 })
183 })
184 .collect()
185 }),
186 }
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
201pub struct PermissionSuggestion {
202 #[serde(rename = "type")]
204 pub suggestion_type: String,
205 pub destination: String,
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub mode: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub behavior: Option<String>,
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub rules: Option<Vec<Value>>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct ToolPermissionRequest {
245 pub tool_name: String,
247 pub input: Value,
249 #[serde(default)]
251 pub permission_suggestions: Vec<PermissionSuggestion>,
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub blocked_path: Option<String>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub decision_reason: Option<String>,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub tool_use_id: Option<String>,
261}
262
263impl ToolPermissionRequest {
264 pub fn allow(&self, request_id: &str) -> ControlResponse {
281 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
282 }
283
284 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
306 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
307 }
308
309 pub fn allow_with_permissions(
313 &self,
314 modified_input: Value,
315 permissions: Vec<Value>,
316 request_id: &str,
317 ) -> ControlResponse {
318 ControlResponse::from_result(
319 request_id,
320 PermissionResult::allow_with_permissions(modified_input, permissions),
321 )
322 }
323
324 pub fn allow_and_remember(
350 &self,
351 permissions: Vec<Permission>,
352 request_id: &str,
353 ) -> ControlResponse {
354 ControlResponse::from_result(
355 request_id,
356 PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
357 )
358 }
359
360 pub fn allow_with_and_remember(
364 &self,
365 modified_input: Value,
366 permissions: Vec<Permission>,
367 request_id: &str,
368 ) -> ControlResponse {
369 ControlResponse::from_result(
370 request_id,
371 PermissionResult::allow_with_typed_permissions(modified_input, permissions),
372 )
373 }
374
375 pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
401 self.permission_suggestions.first().map(|suggestion| {
402 let perm = Permission::from_suggestion(suggestion);
403 self.allow_and_remember(vec![perm], request_id)
404 })
405 }
406
407 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
426 ControlResponse::from_result(request_id, PermissionResult::deny(message))
427 }
428
429 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
433 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
434 }
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
442#[serde(tag = "behavior", rename_all = "snake_case")]
443pub enum PermissionResult {
444 Allow {
446 #[serde(rename = "updatedInput")]
448 updated_input: Value,
449 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
451 updated_permissions: Option<Vec<Value>>,
452 },
453 Deny {
455 message: String,
457 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
459 interrupt: bool,
460 },
461}
462
463impl PermissionResult {
464 pub fn allow(input: Value) -> Self {
466 PermissionResult::Allow {
467 updated_input: input,
468 updated_permissions: None,
469 }
470 }
471
472 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
476 PermissionResult::Allow {
477 updated_input: input,
478 updated_permissions: Some(permissions),
479 }
480 }
481
482 pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
498 let permission_values: Vec<Value> = permissions
499 .into_iter()
500 .filter_map(|p| serde_json::to_value(p).ok())
501 .collect();
502 PermissionResult::Allow {
503 updated_input: input,
504 updated_permissions: Some(permission_values),
505 }
506 }
507
508 pub fn deny(message: impl Into<String>) -> Self {
510 PermissionResult::Deny {
511 message: message.into(),
512 interrupt: false,
513 }
514 }
515
516 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
518 PermissionResult::Deny {
519 message: message.into(),
520 interrupt: true,
521 }
522 }
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct HookCallbackRequest {
528 pub callback_id: String,
529 pub input: Value,
530 #[serde(skip_serializing_if = "Option::is_none")]
531 pub tool_use_id: Option<String>,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct McpMessageRequest {
537 pub server_name: String,
538 pub message: Value,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct InitializeRequest {
544 #[serde(skip_serializing_if = "Option::is_none")]
545 pub hooks: Option<Value>,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct ControlResponse {
554 pub response: ControlResponsePayload,
556}
557
558impl ControlResponse {
559 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
563 let response_value = serde_json::to_value(&result)
565 .expect("PermissionResult serialization should never fail");
566 ControlResponse {
567 response: ControlResponsePayload::Success {
568 request_id: request_id.to_string(),
569 response: Some(response_value),
570 },
571 }
572 }
573
574 pub fn success(request_id: &str, response_data: Value) -> Self {
576 ControlResponse {
577 response: ControlResponsePayload::Success {
578 request_id: request_id.to_string(),
579 response: Some(response_data),
580 },
581 }
582 }
583
584 pub fn success_empty(request_id: &str) -> Self {
586 ControlResponse {
587 response: ControlResponsePayload::Success {
588 request_id: request_id.to_string(),
589 response: None,
590 },
591 }
592 }
593
594 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
596 ControlResponse {
597 response: ControlResponsePayload::Error {
598 request_id: request_id.to_string(),
599 error: error_message.into(),
600 },
601 }
602 }
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
607#[serde(tag = "subtype", rename_all = "snake_case")]
608pub enum ControlResponsePayload {
609 Success {
610 request_id: String,
611 #[serde(skip_serializing_if = "Option::is_none")]
612 response: Option<Value>,
613 },
614 Error {
615 request_id: String,
616 error: String,
617 },
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct ControlResponseMessage {
623 #[serde(rename = "type")]
624 pub message_type: String,
625 pub response: ControlResponsePayload,
626}
627
628impl From<ControlResponse> for ControlResponseMessage {
629 fn from(resp: ControlResponse) -> Self {
630 ControlResponseMessage {
631 message_type: "control_response".to_string(),
632 response: resp.response,
633 }
634 }
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
639pub struct ControlRequestMessage {
640 #[serde(rename = "type")]
641 pub message_type: String,
642 pub request_id: String,
643 pub request: ControlRequestPayload,
644}
645
646impl ControlRequestMessage {
647 pub fn initialize(request_id: impl Into<String>) -> Self {
649 ControlRequestMessage {
650 message_type: "control_request".to_string(),
651 request_id: request_id.into(),
652 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
653 }
654 }
655
656 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
658 ControlRequestMessage {
659 message_type: "control_request".to_string(),
660 request_id: request_id.into(),
661 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
662 }
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669 use crate::io::ClaudeOutput;
670
671 #[test]
672 fn test_deserialize_control_request_can_use_tool() {
673 let json = r#"{
674 "type": "control_request",
675 "request_id": "perm-abc123",
676 "request": {
677 "subtype": "can_use_tool",
678 "tool_name": "Write",
679 "input": {
680 "file_path": "/home/user/hello.py",
681 "content": "print('hello')"
682 },
683 "permission_suggestions": [],
684 "blocked_path": null
685 }
686 }"#;
687
688 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
689 assert!(output.is_control_request());
690
691 if let ClaudeOutput::ControlRequest(req) = output {
692 assert_eq!(req.request_id, "perm-abc123");
693 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
694 assert_eq!(perm_req.tool_name, "Write");
695 assert_eq!(
696 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
697 "/home/user/hello.py"
698 );
699 } else {
700 panic!("Expected CanUseTool payload");
701 }
702 } else {
703 panic!("Expected ControlRequest");
704 }
705 }
706
707 #[test]
708 fn test_deserialize_control_request_edit_tool_real() {
709 let json = r#"{"type":"control_request","request_id":"f3cf357c-17d6-4eca-b498-dd17c7ac43dd","request":{"subtype":"can_use_tool","tool_name":"Edit","input":{"file_path":"/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs","old_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n println!(\n \" {} Run: {} to re-authenticate\",\n \"→\".bright_blue(),\n \"claude-portal logout && claude-portal login\".bright_cyan()\n );\n}","new_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n println!(\n \" {} Run: {} to re-authenticate\",\n \"→\".bright_blue(),\n \"claude-portal --reauth\".bright_cyan()\n );\n}","replace_all":false},"permission_suggestions":[{"type":"setMode","mode":"acceptEdits","destination":"session"}],"tool_use_id":"toolu_015BDGtNiqNrRSJSDrWXNckW"}}"#;
711
712 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
713 assert!(output.is_control_request());
714 assert_eq!(output.message_type(), "control_request");
715
716 if let ClaudeOutput::ControlRequest(req) = output {
717 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
718 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
719 assert_eq!(perm_req.tool_name, "Edit");
720 assert_eq!(
721 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
722 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
723 );
724 assert!(perm_req.input.get("old_string").is_some());
725 assert!(perm_req.input.get("new_string").is_some());
726 assert!(!perm_req
727 .input
728 .get("replace_all")
729 .unwrap()
730 .as_bool()
731 .unwrap());
732 } else {
733 panic!("Expected CanUseTool payload");
734 }
735 } else {
736 panic!("Expected ControlRequest");
737 }
738 }
739
740 #[test]
741 fn test_tool_permission_request_allow() {
742 let req = ToolPermissionRequest {
743 tool_name: "Read".to_string(),
744 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
745 permission_suggestions: vec![],
746 blocked_path: None,
747 decision_reason: None,
748 tool_use_id: None,
749 };
750
751 let response = req.allow("req-123");
752 let message: ControlResponseMessage = response.into();
753
754 let json = serde_json::to_string(&message).unwrap();
755 assert!(json.contains("\"type\":\"control_response\""));
756 assert!(json.contains("\"subtype\":\"success\""));
757 assert!(json.contains("\"request_id\":\"req-123\""));
758 assert!(json.contains("\"behavior\":\"allow\""));
759 assert!(json.contains("\"updatedInput\""));
760 }
761
762 #[test]
763 fn test_tool_permission_request_allow_with_modified_input() {
764 let req = ToolPermissionRequest {
765 tool_name: "Write".to_string(),
766 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
767 permission_suggestions: vec![],
768 blocked_path: None,
769 decision_reason: None,
770 tool_use_id: None,
771 };
772
773 let modified_input = serde_json::json!({
774 "file_path": "/tmp/safe/passwd",
775 "content": "test"
776 });
777 let response = req.allow_with(modified_input, "req-456");
778 let message: ControlResponseMessage = response.into();
779
780 let json = serde_json::to_string(&message).unwrap();
781 assert!(json.contains("/tmp/safe/passwd"));
782 assert!(!json.contains("/etc/passwd"));
783 }
784
785 #[test]
786 fn test_tool_permission_request_deny() {
787 let req = ToolPermissionRequest {
788 tool_name: "Bash".to_string(),
789 input: serde_json::json!({"command": "sudo rm -rf /"}),
790 permission_suggestions: vec![],
791 blocked_path: None,
792 decision_reason: None,
793 tool_use_id: None,
794 };
795
796 let response = req.deny("Dangerous command blocked", "req-789");
797 let message: ControlResponseMessage = response.into();
798
799 let json = serde_json::to_string(&message).unwrap();
800 assert!(json.contains("\"behavior\":\"deny\""));
801 assert!(json.contains("Dangerous command blocked"));
802 assert!(!json.contains("\"interrupt\":true"));
803 }
804
805 #[test]
806 fn test_tool_permission_request_deny_and_stop() {
807 let req = ToolPermissionRequest {
808 tool_name: "Bash".to_string(),
809 input: serde_json::json!({"command": "rm -rf /"}),
810 permission_suggestions: vec![],
811 blocked_path: None,
812 decision_reason: None,
813 tool_use_id: None,
814 };
815
816 let response = req.deny_and_stop("Security violation", "req-000");
817 let message: ControlResponseMessage = response.into();
818
819 let json = serde_json::to_string(&message).unwrap();
820 assert!(json.contains("\"behavior\":\"deny\""));
821 assert!(json.contains("\"interrupt\":true"));
822 }
823
824 #[test]
825 fn test_permission_result_serialization() {
826 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
828 let json = serde_json::to_string(&allow).unwrap();
829 assert!(json.contains("\"behavior\":\"allow\""));
830 assert!(json.contains("\"updatedInput\""));
831
832 let deny = PermissionResult::deny("Not allowed");
834 let json = serde_json::to_string(&deny).unwrap();
835 assert!(json.contains("\"behavior\":\"deny\""));
836 assert!(json.contains("\"message\":\"Not allowed\""));
837 assert!(!json.contains("\"interrupt\""));
838
839 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
841 let json = serde_json::to_string(&deny_stop).unwrap();
842 assert!(json.contains("\"interrupt\":true"));
843 }
844
845 #[test]
846 fn test_control_request_message_initialize() {
847 let init = ControlRequestMessage::initialize("init-1");
848
849 let json = serde_json::to_string(&init).unwrap();
850 assert!(json.contains("\"type\":\"control_request\""));
851 assert!(json.contains("\"request_id\":\"init-1\""));
852 assert!(json.contains("\"subtype\":\"initialize\""));
853 }
854
855 #[test]
856 fn test_control_response_error() {
857 let response = ControlResponse::error("req-err", "Something went wrong");
858 let message: ControlResponseMessage = response.into();
859
860 let json = serde_json::to_string(&message).unwrap();
861 assert!(json.contains("\"subtype\":\"error\""));
862 assert!(json.contains("\"error\":\"Something went wrong\""));
863 }
864
865 #[test]
866 fn test_roundtrip_control_request() {
867 let original_json = r#"{
868 "type": "control_request",
869 "request_id": "test-123",
870 "request": {
871 "subtype": "can_use_tool",
872 "tool_name": "Bash",
873 "input": {"command": "ls -la"},
874 "permission_suggestions": []
875 }
876 }"#;
877
878 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
879
880 let reserialized = serde_json::to_string(&output).unwrap();
881 assert!(reserialized.contains("control_request"));
882 assert!(reserialized.contains("test-123"));
883 assert!(reserialized.contains("Bash"));
884 }
885
886 #[test]
887 fn test_permission_suggestions_parsing() {
888 let json = r#"{
889 "type": "control_request",
890 "request_id": "perm-456",
891 "request": {
892 "subtype": "can_use_tool",
893 "tool_name": "Bash",
894 "input": {"command": "npm test"},
895 "permission_suggestions": [
896 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
897 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
898 ]
899 }
900 }"#;
901
902 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
903 if let ClaudeOutput::ControlRequest(req) = output {
904 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
905 assert_eq!(perm_req.permission_suggestions.len(), 2);
906 assert_eq!(
907 perm_req.permission_suggestions[0].suggestion_type,
908 "setMode"
909 );
910 assert_eq!(
911 perm_req.permission_suggestions[0].mode,
912 Some("acceptEdits".to_string())
913 );
914 assert_eq!(perm_req.permission_suggestions[0].destination, "session");
915 assert_eq!(
916 perm_req.permission_suggestions[1].suggestion_type,
917 "setMode"
918 );
919 assert_eq!(
920 perm_req.permission_suggestions[1].mode,
921 Some("bypassPermissions".to_string())
922 );
923 assert_eq!(perm_req.permission_suggestions[1].destination, "project");
924 } else {
925 panic!("Expected CanUseTool payload");
926 }
927 } else {
928 panic!("Expected ControlRequest");
929 }
930 }
931
932 #[test]
933 fn test_permission_suggestion_set_mode_roundtrip() {
934 let suggestion = PermissionSuggestion {
935 suggestion_type: "setMode".to_string(),
936 destination: "session".to_string(),
937 mode: Some("acceptEdits".to_string()),
938 behavior: None,
939 rules: None,
940 };
941
942 let json = serde_json::to_string(&suggestion).unwrap();
943 assert!(json.contains("\"type\":\"setMode\""));
944 assert!(json.contains("\"mode\":\"acceptEdits\""));
945 assert!(json.contains("\"destination\":\"session\""));
946 assert!(!json.contains("\"behavior\""));
947 assert!(!json.contains("\"rules\""));
948
949 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
950 assert_eq!(parsed, suggestion);
951 }
952
953 #[test]
954 fn test_permission_suggestion_add_rules_roundtrip() {
955 let suggestion = PermissionSuggestion {
956 suggestion_type: "addRules".to_string(),
957 destination: "session".to_string(),
958 mode: None,
959 behavior: Some("allow".to_string()),
960 rules: Some(vec![serde_json::json!({
961 "toolName": "Read",
962 "ruleContent": "//tmp/**"
963 })]),
964 };
965
966 let json = serde_json::to_string(&suggestion).unwrap();
967 assert!(json.contains("\"type\":\"addRules\""));
968 assert!(json.contains("\"behavior\":\"allow\""));
969 assert!(json.contains("\"destination\":\"session\""));
970 assert!(json.contains("\"rules\""));
971 assert!(json.contains("\"toolName\":\"Read\""));
972 assert!(!json.contains("\"mode\""));
973
974 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
975 assert_eq!(parsed, suggestion);
976 }
977
978 #[test]
979 fn test_permission_suggestion_add_rules_from_real_json() {
980 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
981
982 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
983 assert_eq!(parsed.suggestion_type, "addRules");
984 assert_eq!(parsed.destination, "session");
985 assert_eq!(parsed.behavior, Some("allow".to_string()));
986 assert!(parsed.rules.is_some());
987 assert!(parsed.mode.is_none());
988 }
989
990 #[test]
991 fn test_permission_allow_tool() {
992 let perm = Permission::allow_tool("Bash", "npm test");
993
994 assert_eq!(perm.permission_type, "addRules");
995 assert_eq!(perm.destination, "session");
996 assert_eq!(perm.behavior, Some("allow".to_string()));
997 assert!(perm.mode.is_none());
998
999 let rules = perm.rules.unwrap();
1000 assert_eq!(rules.len(), 1);
1001 assert_eq!(rules[0].tool_name, "Bash");
1002 assert_eq!(rules[0].rule_content, "npm test");
1003 }
1004
1005 #[test]
1006 fn test_permission_allow_tool_with_destination() {
1007 let perm = Permission::allow_tool_with_destination("Read", "/tmp/**", "project");
1008
1009 assert_eq!(perm.permission_type, "addRules");
1010 assert_eq!(perm.destination, "project");
1011 assert_eq!(perm.behavior, Some("allow".to_string()));
1012
1013 let rules = perm.rules.unwrap();
1014 assert_eq!(rules[0].tool_name, "Read");
1015 assert_eq!(rules[0].rule_content, "/tmp/**");
1016 }
1017
1018 #[test]
1019 fn test_permission_set_mode() {
1020 let perm = Permission::set_mode("acceptEdits", "session");
1021
1022 assert_eq!(perm.permission_type, "setMode");
1023 assert_eq!(perm.destination, "session");
1024 assert_eq!(perm.mode, Some("acceptEdits".to_string()));
1025 assert!(perm.behavior.is_none());
1026 assert!(perm.rules.is_none());
1027 }
1028
1029 #[test]
1030 fn test_permission_serialization() {
1031 let perm = Permission::allow_tool("Bash", "npm test");
1032 let json = serde_json::to_string(&perm).unwrap();
1033
1034 assert!(json.contains("\"type\":\"addRules\""));
1035 assert!(json.contains("\"destination\":\"session\""));
1036 assert!(json.contains("\"behavior\":\"allow\""));
1037 assert!(json.contains("\"toolName\":\"Bash\""));
1038 assert!(json.contains("\"ruleContent\":\"npm test\""));
1039 }
1040
1041 #[test]
1042 fn test_permission_from_suggestion_set_mode() {
1043 let suggestion = PermissionSuggestion {
1044 suggestion_type: "setMode".to_string(),
1045 destination: "session".to_string(),
1046 mode: Some("acceptEdits".to_string()),
1047 behavior: None,
1048 rules: None,
1049 };
1050
1051 let perm = Permission::from_suggestion(&suggestion);
1052
1053 assert_eq!(perm.permission_type, "setMode");
1054 assert_eq!(perm.destination, "session");
1055 assert_eq!(perm.mode, Some("acceptEdits".to_string()));
1056 }
1057
1058 #[test]
1059 fn test_permission_from_suggestion_add_rules() {
1060 let suggestion = PermissionSuggestion {
1061 suggestion_type: "addRules".to_string(),
1062 destination: "session".to_string(),
1063 mode: None,
1064 behavior: Some("allow".to_string()),
1065 rules: Some(vec![serde_json::json!({
1066 "toolName": "Read",
1067 "ruleContent": "/tmp/**"
1068 })]),
1069 };
1070
1071 let perm = Permission::from_suggestion(&suggestion);
1072
1073 assert_eq!(perm.permission_type, "addRules");
1074 assert_eq!(perm.behavior, Some("allow".to_string()));
1075
1076 let rules = perm.rules.unwrap();
1077 assert_eq!(rules.len(), 1);
1078 assert_eq!(rules[0].tool_name, "Read");
1079 assert_eq!(rules[0].rule_content, "/tmp/**");
1080 }
1081
1082 #[test]
1083 fn test_permission_result_allow_with_typed_permissions() {
1084 let result = PermissionResult::allow_with_typed_permissions(
1085 serde_json::json!({"command": "npm test"}),
1086 vec![Permission::allow_tool("Bash", "npm test")],
1087 );
1088
1089 let json = serde_json::to_string(&result).unwrap();
1090 assert!(json.contains("\"behavior\":\"allow\""));
1091 assert!(json.contains("\"updatedPermissions\""));
1092 assert!(json.contains("\"toolName\":\"Bash\""));
1093 }
1094
1095 #[test]
1096 fn test_tool_permission_request_allow_and_remember() {
1097 let req = ToolPermissionRequest {
1098 tool_name: "Bash".to_string(),
1099 input: serde_json::json!({"command": "npm test"}),
1100 permission_suggestions: vec![],
1101 blocked_path: None,
1102 decision_reason: None,
1103 tool_use_id: None,
1104 };
1105
1106 let response =
1107 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1108 let message: ControlResponseMessage = response.into();
1109 let json = serde_json::to_string(&message).unwrap();
1110
1111 assert!(json.contains("\"type\":\"control_response\""));
1112 assert!(json.contains("\"behavior\":\"allow\""));
1113 assert!(json.contains("\"updatedPermissions\""));
1114 assert!(json.contains("\"toolName\":\"Bash\""));
1115 }
1116
1117 #[test]
1118 fn test_tool_permission_request_allow_and_remember_suggestion() {
1119 let req = ToolPermissionRequest {
1120 tool_name: "Bash".to_string(),
1121 input: serde_json::json!({"command": "npm test"}),
1122 permission_suggestions: vec![PermissionSuggestion {
1123 suggestion_type: "setMode".to_string(),
1124 destination: "session".to_string(),
1125 mode: Some("acceptEdits".to_string()),
1126 behavior: None,
1127 rules: None,
1128 }],
1129 blocked_path: None,
1130 decision_reason: None,
1131 tool_use_id: None,
1132 };
1133
1134 let response = req.allow_and_remember_suggestion("req-123");
1135 assert!(response.is_some());
1136
1137 let message: ControlResponseMessage = response.unwrap().into();
1138 let json = serde_json::to_string(&message).unwrap();
1139
1140 assert!(json.contains("\"type\":\"setMode\""));
1141 assert!(json.contains("\"mode\":\"acceptEdits\""));
1142 }
1143
1144 #[test]
1145 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1146 let req = ToolPermissionRequest {
1147 tool_name: "Bash".to_string(),
1148 input: serde_json::json!({"command": "npm test"}),
1149 permission_suggestions: vec![], blocked_path: None,
1151 decision_reason: None,
1152 tool_use_id: None,
1153 };
1154
1155 let response = req.allow_and_remember_suggestion("req-123");
1156 assert!(response.is_none());
1157 }
1158}