1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum PermissionType {
15 AddRules,
17 SetMode,
19 Unknown(String),
21}
22
23impl PermissionType {
24 pub fn as_str(&self) -> &str {
25 match self {
26 Self::AddRules => "addRules",
27 Self::SetMode => "setMode",
28 Self::Unknown(s) => s.as_str(),
29 }
30 }
31}
32
33impl fmt::Display for PermissionType {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 f.write_str(self.as_str())
36 }
37}
38
39impl From<&str> for PermissionType {
40 fn from(s: &str) -> Self {
41 match s {
42 "addRules" => Self::AddRules,
43 "setMode" => Self::SetMode,
44 other => Self::Unknown(other.to_string()),
45 }
46 }
47}
48
49impl Serialize for PermissionType {
50 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
51 serializer.serialize_str(self.as_str())
52 }
53}
54
55impl<'de> Deserialize<'de> for PermissionType {
56 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
57 let s = String::deserialize(deserializer)?;
58 Ok(Self::from(s.as_str()))
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub enum PermissionDestination {
65 Session,
67 Project,
69 Unknown(String),
71}
72
73impl PermissionDestination {
74 pub fn as_str(&self) -> &str {
75 match self {
76 Self::Session => "session",
77 Self::Project => "project",
78 Self::Unknown(s) => s.as_str(),
79 }
80 }
81}
82
83impl fmt::Display for PermissionDestination {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.write_str(self.as_str())
86 }
87}
88
89impl From<&str> for PermissionDestination {
90 fn from(s: &str) -> Self {
91 match s {
92 "session" => Self::Session,
93 "project" => Self::Project,
94 other => Self::Unknown(other.to_string()),
95 }
96 }
97}
98
99impl Serialize for PermissionDestination {
100 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
101 serializer.serialize_str(self.as_str())
102 }
103}
104
105impl<'de> Deserialize<'de> for PermissionDestination {
106 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
107 let s = String::deserialize(deserializer)?;
108 Ok(Self::from(s.as_str()))
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Hash)]
114pub enum PermissionBehavior {
115 Allow,
117 Deny,
119 Unknown(String),
121}
122
123impl PermissionBehavior {
124 pub fn as_str(&self) -> &str {
125 match self {
126 Self::Allow => "allow",
127 Self::Deny => "deny",
128 Self::Unknown(s) => s.as_str(),
129 }
130 }
131}
132
133impl fmt::Display for PermissionBehavior {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.write_str(self.as_str())
136 }
137}
138
139impl From<&str> for PermissionBehavior {
140 fn from(s: &str) -> Self {
141 match s {
142 "allow" => Self::Allow,
143 "deny" => Self::Deny,
144 other => Self::Unknown(other.to_string()),
145 }
146 }
147}
148
149impl Serialize for PermissionBehavior {
150 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
151 serializer.serialize_str(self.as_str())
152 }
153}
154
155impl<'de> Deserialize<'de> for PermissionBehavior {
156 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
157 let s = String::deserialize(deserializer)?;
158 Ok(Self::from(s.as_str()))
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Hash)]
164pub enum PermissionModeName {
165 AcceptEdits,
167 BypassPermissions,
169 Unknown(String),
171}
172
173impl PermissionModeName {
174 pub fn as_str(&self) -> &str {
175 match self {
176 Self::AcceptEdits => "acceptEdits",
177 Self::BypassPermissions => "bypassPermissions",
178 Self::Unknown(s) => s.as_str(),
179 }
180 }
181}
182
183impl fmt::Display for PermissionModeName {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 f.write_str(self.as_str())
186 }
187}
188
189impl From<&str> for PermissionModeName {
190 fn from(s: &str) -> Self {
191 match s {
192 "acceptEdits" => Self::AcceptEdits,
193 "bypassPermissions" => Self::BypassPermissions,
194 other => Self::Unknown(other.to_string()),
195 }
196 }
197}
198
199impl Serialize for PermissionModeName {
200 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
201 serializer.serialize_str(self.as_str())
202 }
203}
204
205impl<'de> Deserialize<'de> for PermissionModeName {
206 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
207 let s = String::deserialize(deserializer)?;
208 Ok(Self::from(s.as_str()))
209 }
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct ControlRequest {
223 pub request_id: String,
225 pub request: ControlRequestPayload,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(tag = "subtype", rename_all = "snake_case")]
232pub enum ControlRequestPayload {
233 CanUseTool(ToolPermissionRequest),
235 HookCallback(HookCallbackRequest),
237 McpMessage(McpMessageRequest),
239 Initialize(InitializeRequest),
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
260pub struct Permission {
261 #[serde(rename = "type")]
263 pub permission_type: PermissionType,
264 pub destination: PermissionDestination,
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub mode: Option<PermissionModeName>,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub behavior: Option<PermissionBehavior>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub rules: Option<Vec<PermissionRule>>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
279pub struct PermissionRule {
280 #[serde(rename = "toolName")]
282 pub tool_name: String,
283 #[serde(rename = "ruleContent")]
285 pub rule_content: String,
286}
287
288impl Permission {
289 pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
302 Permission {
303 permission_type: PermissionType::AddRules,
304 destination: PermissionDestination::Session,
305 mode: None,
306 behavior: Some(PermissionBehavior::Allow),
307 rules: Some(vec![PermissionRule {
308 tool_name: tool_name.into(),
309 rule_content: rule_content.into(),
310 }]),
311 }
312 }
313
314 pub fn allow_tool_with_destination(
324 tool_name: impl Into<String>,
325 rule_content: impl Into<String>,
326 destination: PermissionDestination,
327 ) -> Self {
328 Permission {
329 permission_type: PermissionType::AddRules,
330 destination,
331 mode: None,
332 behavior: Some(PermissionBehavior::Allow),
333 rules: Some(vec![PermissionRule {
334 tool_name: tool_name.into(),
335 rule_content: rule_content.into(),
336 }]),
337 }
338 }
339
340 pub fn set_mode(mode: PermissionModeName, destination: PermissionDestination) -> Self {
350 Permission {
351 permission_type: PermissionType::SetMode,
352 destination,
353 mode: Some(mode),
354 behavior: None,
355 rules: None,
356 }
357 }
358
359 pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
378 Permission {
379 permission_type: suggestion.suggestion_type.clone(),
380 destination: suggestion.destination.clone(),
381 mode: suggestion.mode.clone(),
382 behavior: suggestion.behavior.clone(),
383 rules: suggestion.rules.as_ref().map(|rules| {
384 rules
385 .iter()
386 .filter_map(|v| {
387 Some(PermissionRule {
388 tool_name: v.get("toolName")?.as_str()?.to_string(),
389 rule_content: v.get("ruleContent")?.as_str()?.to_string(),
390 })
391 })
392 .collect()
393 }),
394 }
395 }
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
409pub struct PermissionSuggestion {
410 #[serde(rename = "type")]
412 pub suggestion_type: PermissionType,
413 pub destination: PermissionDestination,
415 #[serde(skip_serializing_if = "Option::is_none")]
417 pub mode: Option<PermissionModeName>,
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub behavior: Option<PermissionBehavior>,
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub rules: Option<Vec<Value>>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct ToolPermissionRequest {
453 pub tool_name: String,
455 pub input: Value,
457 #[serde(default)]
459 pub permission_suggestions: Vec<PermissionSuggestion>,
460 #[serde(skip_serializing_if = "Option::is_none")]
462 pub blocked_path: Option<String>,
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub decision_reason: Option<String>,
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub tool_use_id: Option<String>,
469}
470
471impl ToolPermissionRequest {
472 pub fn allow(&self, request_id: &str) -> ControlResponse {
489 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
490 }
491
492 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
514 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
515 }
516
517 pub fn allow_with_permissions(
521 &self,
522 modified_input: Value,
523 permissions: Vec<Value>,
524 request_id: &str,
525 ) -> ControlResponse {
526 ControlResponse::from_result(
527 request_id,
528 PermissionResult::allow_with_permissions(modified_input, permissions),
529 )
530 }
531
532 pub fn allow_and_remember(
558 &self,
559 permissions: Vec<Permission>,
560 request_id: &str,
561 ) -> ControlResponse {
562 ControlResponse::from_result(
563 request_id,
564 PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
565 )
566 }
567
568 pub fn allow_with_and_remember(
572 &self,
573 modified_input: Value,
574 permissions: Vec<Permission>,
575 request_id: &str,
576 ) -> ControlResponse {
577 ControlResponse::from_result(
578 request_id,
579 PermissionResult::allow_with_typed_permissions(modified_input, permissions),
580 )
581 }
582
583 pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
609 self.permission_suggestions.first().map(|suggestion| {
610 let perm = Permission::from_suggestion(suggestion);
611 self.allow_and_remember(vec![perm], request_id)
612 })
613 }
614
615 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
634 ControlResponse::from_result(request_id, PermissionResult::deny(message))
635 }
636
637 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
641 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
642 }
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize)]
650#[serde(tag = "behavior", rename_all = "snake_case")]
651pub enum PermissionResult {
652 Allow {
654 #[serde(rename = "updatedInput")]
656 updated_input: Value,
657 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
659 updated_permissions: Option<Vec<Value>>,
660 },
661 Deny {
663 message: String,
665 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
667 interrupt: bool,
668 },
669}
670
671impl PermissionResult {
672 pub fn allow(input: Value) -> Self {
674 PermissionResult::Allow {
675 updated_input: input,
676 updated_permissions: None,
677 }
678 }
679
680 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
684 PermissionResult::Allow {
685 updated_input: input,
686 updated_permissions: Some(permissions),
687 }
688 }
689
690 pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
706 let permission_values: Vec<Value> = permissions
707 .into_iter()
708 .filter_map(|p| serde_json::to_value(p).ok())
709 .collect();
710 PermissionResult::Allow {
711 updated_input: input,
712 updated_permissions: Some(permission_values),
713 }
714 }
715
716 pub fn deny(message: impl Into<String>) -> Self {
718 PermissionResult::Deny {
719 message: message.into(),
720 interrupt: false,
721 }
722 }
723
724 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
726 PermissionResult::Deny {
727 message: message.into(),
728 interrupt: true,
729 }
730 }
731}
732
733#[derive(Debug, Clone, Serialize, Deserialize)]
735pub struct HookCallbackRequest {
736 pub callback_id: String,
737 pub input: Value,
738 #[serde(skip_serializing_if = "Option::is_none")]
739 pub tool_use_id: Option<String>,
740}
741
742#[derive(Debug, Clone, Serialize, Deserialize)]
744pub struct McpMessageRequest {
745 pub server_name: String,
746 pub message: Value,
747}
748
749#[derive(Debug, Clone, Serialize, Deserialize)]
751pub struct InitializeRequest {
752 #[serde(skip_serializing_if = "Option::is_none")]
753 pub hooks: Option<Value>,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct ControlResponse {
762 pub response: ControlResponsePayload,
764}
765
766impl ControlResponse {
767 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
771 let response_value = serde_json::to_value(&result)
773 .expect("PermissionResult serialization should never fail");
774 ControlResponse {
775 response: ControlResponsePayload::Success {
776 request_id: request_id.to_string(),
777 response: Some(response_value),
778 },
779 }
780 }
781
782 pub fn success(request_id: &str, response_data: Value) -> Self {
784 ControlResponse {
785 response: ControlResponsePayload::Success {
786 request_id: request_id.to_string(),
787 response: Some(response_data),
788 },
789 }
790 }
791
792 pub fn success_empty(request_id: &str) -> Self {
794 ControlResponse {
795 response: ControlResponsePayload::Success {
796 request_id: request_id.to_string(),
797 response: None,
798 },
799 }
800 }
801
802 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
804 ControlResponse {
805 response: ControlResponsePayload::Error {
806 request_id: request_id.to_string(),
807 error: error_message.into(),
808 },
809 }
810 }
811}
812
813#[derive(Debug, Clone, Serialize, Deserialize)]
815#[serde(tag = "subtype", rename_all = "snake_case")]
816pub enum ControlResponsePayload {
817 Success {
818 request_id: String,
819 #[serde(skip_serializing_if = "Option::is_none")]
820 response: Option<Value>,
821 },
822 Error {
823 request_id: String,
824 error: String,
825 },
826}
827
828#[derive(Debug, Clone, Serialize, Deserialize)]
830pub struct ControlResponseMessage {
831 #[serde(rename = "type")]
832 pub message_type: String,
833 pub response: ControlResponsePayload,
834}
835
836impl From<ControlResponse> for ControlResponseMessage {
837 fn from(resp: ControlResponse) -> Self {
838 ControlResponseMessage {
839 message_type: "control_response".to_string(),
840 response: resp.response,
841 }
842 }
843}
844
845#[derive(Debug, Clone, Serialize, Deserialize)]
847pub struct ControlRequestMessage {
848 #[serde(rename = "type")]
849 pub message_type: String,
850 pub request_id: String,
851 pub request: ControlRequestPayload,
852}
853
854impl ControlRequestMessage {
855 pub fn initialize(request_id: impl Into<String>) -> Self {
857 ControlRequestMessage {
858 message_type: "control_request".to_string(),
859 request_id: request_id.into(),
860 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
861 }
862 }
863
864 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
866 ControlRequestMessage {
867 message_type: "control_request".to_string(),
868 request_id: request_id.into(),
869 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
870 }
871 }
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use crate::io::ClaudeOutput;
878
879 #[test]
880 fn test_deserialize_control_request_can_use_tool() {
881 let json = r#"{
882 "type": "control_request",
883 "request_id": "perm-abc123",
884 "request": {
885 "subtype": "can_use_tool",
886 "tool_name": "Write",
887 "input": {
888 "file_path": "/home/user/hello.py",
889 "content": "print('hello')"
890 },
891 "permission_suggestions": [],
892 "blocked_path": null
893 }
894 }"#;
895
896 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
897 assert!(output.is_control_request());
898
899 if let ClaudeOutput::ControlRequest(req) = output {
900 assert_eq!(req.request_id, "perm-abc123");
901 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
902 assert_eq!(perm_req.tool_name, "Write");
903 assert_eq!(
904 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
905 "/home/user/hello.py"
906 );
907 } else {
908 panic!("Expected CanUseTool payload");
909 }
910 } else {
911 panic!("Expected ControlRequest");
912 }
913 }
914
915 #[test]
916 fn test_deserialize_control_request_edit_tool_real() {
917 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"}}"#;
919
920 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
921 assert!(output.is_control_request());
922 assert_eq!(output.message_type(), "control_request");
923
924 if let ClaudeOutput::ControlRequest(req) = output {
925 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
926 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
927 assert_eq!(perm_req.tool_name, "Edit");
928 assert_eq!(
929 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
930 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
931 );
932 assert!(perm_req.input.get("old_string").is_some());
933 assert!(perm_req.input.get("new_string").is_some());
934 assert!(!perm_req
935 .input
936 .get("replace_all")
937 .unwrap()
938 .as_bool()
939 .unwrap());
940 } else {
941 panic!("Expected CanUseTool payload");
942 }
943 } else {
944 panic!("Expected ControlRequest");
945 }
946 }
947
948 #[test]
949 fn test_tool_permission_request_allow() {
950 let req = ToolPermissionRequest {
951 tool_name: "Read".to_string(),
952 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
953 permission_suggestions: vec![],
954 blocked_path: None,
955 decision_reason: None,
956 tool_use_id: None,
957 };
958
959 let response = req.allow("req-123");
960 let message: ControlResponseMessage = response.into();
961
962 let json = serde_json::to_string(&message).unwrap();
963 assert!(json.contains("\"type\":\"control_response\""));
964 assert!(json.contains("\"subtype\":\"success\""));
965 assert!(json.contains("\"request_id\":\"req-123\""));
966 assert!(json.contains("\"behavior\":\"allow\""));
967 assert!(json.contains("\"updatedInput\""));
968 }
969
970 #[test]
971 fn test_tool_permission_request_allow_with_modified_input() {
972 let req = ToolPermissionRequest {
973 tool_name: "Write".to_string(),
974 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
975 permission_suggestions: vec![],
976 blocked_path: None,
977 decision_reason: None,
978 tool_use_id: None,
979 };
980
981 let modified_input = serde_json::json!({
982 "file_path": "/tmp/safe/passwd",
983 "content": "test"
984 });
985 let response = req.allow_with(modified_input, "req-456");
986 let message: ControlResponseMessage = response.into();
987
988 let json = serde_json::to_string(&message).unwrap();
989 assert!(json.contains("/tmp/safe/passwd"));
990 assert!(!json.contains("/etc/passwd"));
991 }
992
993 #[test]
994 fn test_tool_permission_request_deny() {
995 let req = ToolPermissionRequest {
996 tool_name: "Bash".to_string(),
997 input: serde_json::json!({"command": "sudo rm -rf /"}),
998 permission_suggestions: vec![],
999 blocked_path: None,
1000 decision_reason: None,
1001 tool_use_id: None,
1002 };
1003
1004 let response = req.deny("Dangerous command blocked", "req-789");
1005 let message: ControlResponseMessage = response.into();
1006
1007 let json = serde_json::to_string(&message).unwrap();
1008 assert!(json.contains("\"behavior\":\"deny\""));
1009 assert!(json.contains("Dangerous command blocked"));
1010 assert!(!json.contains("\"interrupt\":true"));
1011 }
1012
1013 #[test]
1014 fn test_tool_permission_request_deny_and_stop() {
1015 let req = ToolPermissionRequest {
1016 tool_name: "Bash".to_string(),
1017 input: serde_json::json!({"command": "rm -rf /"}),
1018 permission_suggestions: vec![],
1019 blocked_path: None,
1020 decision_reason: None,
1021 tool_use_id: None,
1022 };
1023
1024 let response = req.deny_and_stop("Security violation", "req-000");
1025 let message: ControlResponseMessage = response.into();
1026
1027 let json = serde_json::to_string(&message).unwrap();
1028 assert!(json.contains("\"behavior\":\"deny\""));
1029 assert!(json.contains("\"interrupt\":true"));
1030 }
1031
1032 #[test]
1033 fn test_permission_result_serialization() {
1034 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1036 let json = serde_json::to_string(&allow).unwrap();
1037 assert!(json.contains("\"behavior\":\"allow\""));
1038 assert!(json.contains("\"updatedInput\""));
1039
1040 let deny = PermissionResult::deny("Not allowed");
1042 let json = serde_json::to_string(&deny).unwrap();
1043 assert!(json.contains("\"behavior\":\"deny\""));
1044 assert!(json.contains("\"message\":\"Not allowed\""));
1045 assert!(!json.contains("\"interrupt\""));
1046
1047 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1049 let json = serde_json::to_string(&deny_stop).unwrap();
1050 assert!(json.contains("\"interrupt\":true"));
1051 }
1052
1053 #[test]
1054 fn test_control_request_message_initialize() {
1055 let init = ControlRequestMessage::initialize("init-1");
1056
1057 let json = serde_json::to_string(&init).unwrap();
1058 assert!(json.contains("\"type\":\"control_request\""));
1059 assert!(json.contains("\"request_id\":\"init-1\""));
1060 assert!(json.contains("\"subtype\":\"initialize\""));
1061 }
1062
1063 #[test]
1064 fn test_control_response_error() {
1065 let response = ControlResponse::error("req-err", "Something went wrong");
1066 let message: ControlResponseMessage = response.into();
1067
1068 let json = serde_json::to_string(&message).unwrap();
1069 assert!(json.contains("\"subtype\":\"error\""));
1070 assert!(json.contains("\"error\":\"Something went wrong\""));
1071 }
1072
1073 #[test]
1074 fn test_roundtrip_control_request() {
1075 let original_json = r#"{
1076 "type": "control_request",
1077 "request_id": "test-123",
1078 "request": {
1079 "subtype": "can_use_tool",
1080 "tool_name": "Bash",
1081 "input": {"command": "ls -la"},
1082 "permission_suggestions": []
1083 }
1084 }"#;
1085
1086 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1087
1088 let reserialized = serde_json::to_string(&output).unwrap();
1089 assert!(reserialized.contains("control_request"));
1090 assert!(reserialized.contains("test-123"));
1091 assert!(reserialized.contains("Bash"));
1092 }
1093
1094 #[test]
1095 fn test_permission_suggestions_parsing() {
1096 let json = r#"{
1097 "type": "control_request",
1098 "request_id": "perm-456",
1099 "request": {
1100 "subtype": "can_use_tool",
1101 "tool_name": "Bash",
1102 "input": {"command": "npm test"},
1103 "permission_suggestions": [
1104 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1105 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1106 ]
1107 }
1108 }"#;
1109
1110 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1111 if let ClaudeOutput::ControlRequest(req) = output {
1112 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1113 assert_eq!(perm_req.permission_suggestions.len(), 2);
1114 assert_eq!(
1115 perm_req.permission_suggestions[0].suggestion_type,
1116 PermissionType::SetMode
1117 );
1118 assert_eq!(
1119 perm_req.permission_suggestions[0].mode,
1120 Some(PermissionModeName::AcceptEdits)
1121 );
1122 assert_eq!(
1123 perm_req.permission_suggestions[0].destination,
1124 PermissionDestination::Session
1125 );
1126 assert_eq!(
1127 perm_req.permission_suggestions[1].suggestion_type,
1128 PermissionType::SetMode
1129 );
1130 assert_eq!(
1131 perm_req.permission_suggestions[1].mode,
1132 Some(PermissionModeName::BypassPermissions)
1133 );
1134 assert_eq!(
1135 perm_req.permission_suggestions[1].destination,
1136 PermissionDestination::Project
1137 );
1138 } else {
1139 panic!("Expected CanUseTool payload");
1140 }
1141 } else {
1142 panic!("Expected ControlRequest");
1143 }
1144 }
1145
1146 #[test]
1147 fn test_permission_suggestion_set_mode_roundtrip() {
1148 let suggestion = PermissionSuggestion {
1149 suggestion_type: PermissionType::SetMode,
1150 destination: PermissionDestination::Session,
1151 mode: Some(PermissionModeName::AcceptEdits),
1152 behavior: None,
1153 rules: None,
1154 };
1155
1156 let json = serde_json::to_string(&suggestion).unwrap();
1157 assert!(json.contains("\"type\":\"setMode\""));
1158 assert!(json.contains("\"mode\":\"acceptEdits\""));
1159 assert!(json.contains("\"destination\":\"session\""));
1160 assert!(!json.contains("\"behavior\""));
1161 assert!(!json.contains("\"rules\""));
1162
1163 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1164 assert_eq!(parsed, suggestion);
1165 }
1166
1167 #[test]
1168 fn test_permission_suggestion_add_rules_roundtrip() {
1169 let suggestion = PermissionSuggestion {
1170 suggestion_type: PermissionType::AddRules,
1171 destination: PermissionDestination::Session,
1172 mode: None,
1173 behavior: Some(PermissionBehavior::Allow),
1174 rules: Some(vec![serde_json::json!({
1175 "toolName": "Read",
1176 "ruleContent": "//tmp/**"
1177 })]),
1178 };
1179
1180 let json = serde_json::to_string(&suggestion).unwrap();
1181 assert!(json.contains("\"type\":\"addRules\""));
1182 assert!(json.contains("\"behavior\":\"allow\""));
1183 assert!(json.contains("\"destination\":\"session\""));
1184 assert!(json.contains("\"rules\""));
1185 assert!(json.contains("\"toolName\":\"Read\""));
1186 assert!(!json.contains("\"mode\""));
1187
1188 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1189 assert_eq!(parsed, suggestion);
1190 }
1191
1192 #[test]
1193 fn test_permission_suggestion_add_rules_from_real_json() {
1194 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1195
1196 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1197 assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1198 assert_eq!(parsed.destination, PermissionDestination::Session);
1199 assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1200 assert!(parsed.rules.is_some());
1201 assert!(parsed.mode.is_none());
1202 }
1203
1204 #[test]
1205 fn test_permission_allow_tool() {
1206 let perm = Permission::allow_tool("Bash", "npm test");
1207
1208 assert_eq!(perm.permission_type, PermissionType::AddRules);
1209 assert_eq!(perm.destination, PermissionDestination::Session);
1210 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1211 assert!(perm.mode.is_none());
1212
1213 let rules = perm.rules.unwrap();
1214 assert_eq!(rules.len(), 1);
1215 assert_eq!(rules[0].tool_name, "Bash");
1216 assert_eq!(rules[0].rule_content, "npm test");
1217 }
1218
1219 #[test]
1220 fn test_permission_allow_tool_with_destination() {
1221 let perm = Permission::allow_tool_with_destination(
1222 "Read",
1223 "/tmp/**",
1224 PermissionDestination::Project,
1225 );
1226
1227 assert_eq!(perm.permission_type, PermissionType::AddRules);
1228 assert_eq!(perm.destination, PermissionDestination::Project);
1229 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1230
1231 let rules = perm.rules.unwrap();
1232 assert_eq!(rules[0].tool_name, "Read");
1233 assert_eq!(rules[0].rule_content, "/tmp/**");
1234 }
1235
1236 #[test]
1237 fn test_permission_set_mode() {
1238 let perm = Permission::set_mode(
1239 PermissionModeName::AcceptEdits,
1240 PermissionDestination::Session,
1241 );
1242
1243 assert_eq!(perm.permission_type, PermissionType::SetMode);
1244 assert_eq!(perm.destination, PermissionDestination::Session);
1245 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1246 assert!(perm.behavior.is_none());
1247 assert!(perm.rules.is_none());
1248 }
1249
1250 #[test]
1251 fn test_permission_serialization() {
1252 let perm = Permission::allow_tool("Bash", "npm test");
1253 let json = serde_json::to_string(&perm).unwrap();
1254
1255 assert!(json.contains("\"type\":\"addRules\""));
1256 assert!(json.contains("\"destination\":\"session\""));
1257 assert!(json.contains("\"behavior\":\"allow\""));
1258 assert!(json.contains("\"toolName\":\"Bash\""));
1259 assert!(json.contains("\"ruleContent\":\"npm test\""));
1260 }
1261
1262 #[test]
1263 fn test_permission_from_suggestion_set_mode() {
1264 let suggestion = PermissionSuggestion {
1265 suggestion_type: PermissionType::SetMode,
1266 destination: PermissionDestination::Session,
1267 mode: Some(PermissionModeName::AcceptEdits),
1268 behavior: None,
1269 rules: None,
1270 };
1271
1272 let perm = Permission::from_suggestion(&suggestion);
1273
1274 assert_eq!(perm.permission_type, PermissionType::SetMode);
1275 assert_eq!(perm.destination, PermissionDestination::Session);
1276 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1277 }
1278
1279 #[test]
1280 fn test_permission_from_suggestion_add_rules() {
1281 let suggestion = PermissionSuggestion {
1282 suggestion_type: PermissionType::AddRules,
1283 destination: PermissionDestination::Session,
1284 mode: None,
1285 behavior: Some(PermissionBehavior::Allow),
1286 rules: Some(vec![serde_json::json!({
1287 "toolName": "Read",
1288 "ruleContent": "/tmp/**"
1289 })]),
1290 };
1291
1292 let perm = Permission::from_suggestion(&suggestion);
1293
1294 assert_eq!(perm.permission_type, PermissionType::AddRules);
1295 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1296
1297 let rules = perm.rules.unwrap();
1298 assert_eq!(rules.len(), 1);
1299 assert_eq!(rules[0].tool_name, "Read");
1300 assert_eq!(rules[0].rule_content, "/tmp/**");
1301 }
1302
1303 #[test]
1304 fn test_permission_result_allow_with_typed_permissions() {
1305 let result = PermissionResult::allow_with_typed_permissions(
1306 serde_json::json!({"command": "npm test"}),
1307 vec![Permission::allow_tool("Bash", "npm test")],
1308 );
1309
1310 let json = serde_json::to_string(&result).unwrap();
1311 assert!(json.contains("\"behavior\":\"allow\""));
1312 assert!(json.contains("\"updatedPermissions\""));
1313 assert!(json.contains("\"toolName\":\"Bash\""));
1314 }
1315
1316 #[test]
1317 fn test_tool_permission_request_allow_and_remember() {
1318 let req = ToolPermissionRequest {
1319 tool_name: "Bash".to_string(),
1320 input: serde_json::json!({"command": "npm test"}),
1321 permission_suggestions: vec![],
1322 blocked_path: None,
1323 decision_reason: None,
1324 tool_use_id: None,
1325 };
1326
1327 let response =
1328 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1329 let message: ControlResponseMessage = response.into();
1330 let json = serde_json::to_string(&message).unwrap();
1331
1332 assert!(json.contains("\"type\":\"control_response\""));
1333 assert!(json.contains("\"behavior\":\"allow\""));
1334 assert!(json.contains("\"updatedPermissions\""));
1335 assert!(json.contains("\"toolName\":\"Bash\""));
1336 }
1337
1338 #[test]
1339 fn test_tool_permission_request_allow_and_remember_suggestion() {
1340 let req = ToolPermissionRequest {
1341 tool_name: "Bash".to_string(),
1342 input: serde_json::json!({"command": "npm test"}),
1343 permission_suggestions: vec![PermissionSuggestion {
1344 suggestion_type: PermissionType::SetMode,
1345 destination: PermissionDestination::Session,
1346 mode: Some(PermissionModeName::AcceptEdits),
1347 behavior: None,
1348 rules: None,
1349 }],
1350 blocked_path: None,
1351 decision_reason: None,
1352 tool_use_id: None,
1353 };
1354
1355 let response = req.allow_and_remember_suggestion("req-123");
1356 assert!(response.is_some());
1357
1358 let message: ControlResponseMessage = response.unwrap().into();
1359 let json = serde_json::to_string(&message).unwrap();
1360
1361 assert!(json.contains("\"type\":\"setMode\""));
1362 assert!(json.contains("\"mode\":\"acceptEdits\""));
1363 }
1364
1365 #[test]
1366 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1367 let req = ToolPermissionRequest {
1368 tool_name: "Bash".to_string(),
1369 input: serde_json::json!({"command": "npm test"}),
1370 permission_suggestions: vec![], blocked_path: None,
1372 decision_reason: None,
1373 tool_use_id: None,
1374 };
1375
1376 let response = req.allow_and_remember_suggestion("req-123");
1377 assert!(response.is_none());
1378 }
1379}