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)]
863pub struct SDKControlInterruptRequest {
864 subtype: SDKControlInterruptSubtype,
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize)]
868enum SDKControlInterruptSubtype {
869 #[serde(rename = "interrupt")]
870 Interrupt,
871}
872
873impl SDKControlInterruptRequest {
874 pub fn new() -> Self {
876 SDKControlInterruptRequest {
877 subtype: SDKControlInterruptSubtype::Interrupt,
878 }
879 }
880}
881
882impl Default for SDKControlInterruptRequest {
883 fn default() -> Self {
884 Self::new()
885 }
886}
887
888#[derive(Debug, Clone, Serialize, Deserialize)]
890pub struct ControlRequestMessage {
891 #[serde(rename = "type")]
892 pub message_type: String,
893 pub request_id: String,
894 pub request: ControlRequestPayload,
895}
896
897impl ControlRequestMessage {
898 pub fn initialize(request_id: impl Into<String>) -> Self {
900 ControlRequestMessage {
901 message_type: "control_request".to_string(),
902 request_id: request_id.into(),
903 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
904 }
905 }
906
907 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
909 ControlRequestMessage {
910 message_type: "control_request".to_string(),
911 request_id: request_id.into(),
912 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
913 }
914 }
915}
916
917#[cfg(test)]
918mod tests {
919 use super::*;
920 use crate::io::ClaudeOutput;
921
922 #[test]
923 fn test_deserialize_control_request_can_use_tool() {
924 let json = r#"{
925 "type": "control_request",
926 "request_id": "perm-abc123",
927 "request": {
928 "subtype": "can_use_tool",
929 "tool_name": "Write",
930 "input": {
931 "file_path": "/home/user/hello.py",
932 "content": "print('hello')"
933 },
934 "permission_suggestions": [],
935 "blocked_path": null
936 }
937 }"#;
938
939 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
940 assert!(output.is_control_request());
941
942 if let ClaudeOutput::ControlRequest(req) = output {
943 assert_eq!(req.request_id, "perm-abc123");
944 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
945 assert_eq!(perm_req.tool_name, "Write");
946 assert_eq!(
947 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
948 "/home/user/hello.py"
949 );
950 } else {
951 panic!("Expected CanUseTool payload");
952 }
953 } else {
954 panic!("Expected ControlRequest");
955 }
956 }
957
958 #[test]
959 fn test_deserialize_control_request_edit_tool_real() {
960 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"}}"#;
962
963 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
964 assert!(output.is_control_request());
965 assert_eq!(output.message_type(), "control_request");
966
967 if let ClaudeOutput::ControlRequest(req) = output {
968 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
969 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
970 assert_eq!(perm_req.tool_name, "Edit");
971 assert_eq!(
972 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
973 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
974 );
975 assert!(perm_req.input.get("old_string").is_some());
976 assert!(perm_req.input.get("new_string").is_some());
977 assert!(!perm_req
978 .input
979 .get("replace_all")
980 .unwrap()
981 .as_bool()
982 .unwrap());
983 } else {
984 panic!("Expected CanUseTool payload");
985 }
986 } else {
987 panic!("Expected ControlRequest");
988 }
989 }
990
991 #[test]
992 fn test_tool_permission_request_allow() {
993 let req = ToolPermissionRequest {
994 tool_name: "Read".to_string(),
995 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
996 permission_suggestions: vec![],
997 blocked_path: None,
998 decision_reason: None,
999 tool_use_id: None,
1000 };
1001
1002 let response = req.allow("req-123");
1003 let message: ControlResponseMessage = response.into();
1004
1005 let json = serde_json::to_string(&message).unwrap();
1006 assert!(json.contains("\"type\":\"control_response\""));
1007 assert!(json.contains("\"subtype\":\"success\""));
1008 assert!(json.contains("\"request_id\":\"req-123\""));
1009 assert!(json.contains("\"behavior\":\"allow\""));
1010 assert!(json.contains("\"updatedInput\""));
1011 }
1012
1013 #[test]
1014 fn test_tool_permission_request_allow_with_modified_input() {
1015 let req = ToolPermissionRequest {
1016 tool_name: "Write".to_string(),
1017 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1018 permission_suggestions: vec![],
1019 blocked_path: None,
1020 decision_reason: None,
1021 tool_use_id: None,
1022 };
1023
1024 let modified_input = serde_json::json!({
1025 "file_path": "/tmp/safe/passwd",
1026 "content": "test"
1027 });
1028 let response = req.allow_with(modified_input, "req-456");
1029 let message: ControlResponseMessage = response.into();
1030
1031 let json = serde_json::to_string(&message).unwrap();
1032 assert!(json.contains("/tmp/safe/passwd"));
1033 assert!(!json.contains("/etc/passwd"));
1034 }
1035
1036 #[test]
1037 fn test_tool_permission_request_deny() {
1038 let req = ToolPermissionRequest {
1039 tool_name: "Bash".to_string(),
1040 input: serde_json::json!({"command": "sudo rm -rf /"}),
1041 permission_suggestions: vec![],
1042 blocked_path: None,
1043 decision_reason: None,
1044 tool_use_id: None,
1045 };
1046
1047 let response = req.deny("Dangerous command blocked", "req-789");
1048 let message: ControlResponseMessage = response.into();
1049
1050 let json = serde_json::to_string(&message).unwrap();
1051 assert!(json.contains("\"behavior\":\"deny\""));
1052 assert!(json.contains("Dangerous command blocked"));
1053 assert!(!json.contains("\"interrupt\":true"));
1054 }
1055
1056 #[test]
1057 fn test_tool_permission_request_deny_and_stop() {
1058 let req = ToolPermissionRequest {
1059 tool_name: "Bash".to_string(),
1060 input: serde_json::json!({"command": "rm -rf /"}),
1061 permission_suggestions: vec![],
1062 blocked_path: None,
1063 decision_reason: None,
1064 tool_use_id: None,
1065 };
1066
1067 let response = req.deny_and_stop("Security violation", "req-000");
1068 let message: ControlResponseMessage = response.into();
1069
1070 let json = serde_json::to_string(&message).unwrap();
1071 assert!(json.contains("\"behavior\":\"deny\""));
1072 assert!(json.contains("\"interrupt\":true"));
1073 }
1074
1075 #[test]
1076 fn test_permission_result_serialization() {
1077 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1079 let json = serde_json::to_string(&allow).unwrap();
1080 assert!(json.contains("\"behavior\":\"allow\""));
1081 assert!(json.contains("\"updatedInput\""));
1082
1083 let deny = PermissionResult::deny("Not allowed");
1085 let json = serde_json::to_string(&deny).unwrap();
1086 assert!(json.contains("\"behavior\":\"deny\""));
1087 assert!(json.contains("\"message\":\"Not allowed\""));
1088 assert!(!json.contains("\"interrupt\""));
1089
1090 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1092 let json = serde_json::to_string(&deny_stop).unwrap();
1093 assert!(json.contains("\"interrupt\":true"));
1094 }
1095
1096 #[test]
1097 fn test_control_request_message_initialize() {
1098 let init = ControlRequestMessage::initialize("init-1");
1099
1100 let json = serde_json::to_string(&init).unwrap();
1101 assert!(json.contains("\"type\":\"control_request\""));
1102 assert!(json.contains("\"request_id\":\"init-1\""));
1103 assert!(json.contains("\"subtype\":\"initialize\""));
1104 }
1105
1106 #[test]
1107 fn test_control_response_error() {
1108 let response = ControlResponse::error("req-err", "Something went wrong");
1109 let message: ControlResponseMessage = response.into();
1110
1111 let json = serde_json::to_string(&message).unwrap();
1112 assert!(json.contains("\"subtype\":\"error\""));
1113 assert!(json.contains("\"error\":\"Something went wrong\""));
1114 }
1115
1116 #[test]
1117 fn test_roundtrip_control_request() {
1118 let original_json = r#"{
1119 "type": "control_request",
1120 "request_id": "test-123",
1121 "request": {
1122 "subtype": "can_use_tool",
1123 "tool_name": "Bash",
1124 "input": {"command": "ls -la"},
1125 "permission_suggestions": []
1126 }
1127 }"#;
1128
1129 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1130
1131 let reserialized = serde_json::to_string(&output).unwrap();
1132 assert!(reserialized.contains("control_request"));
1133 assert!(reserialized.contains("test-123"));
1134 assert!(reserialized.contains("Bash"));
1135 }
1136
1137 #[test]
1138 fn test_permission_suggestions_parsing() {
1139 let json = r#"{
1140 "type": "control_request",
1141 "request_id": "perm-456",
1142 "request": {
1143 "subtype": "can_use_tool",
1144 "tool_name": "Bash",
1145 "input": {"command": "npm test"},
1146 "permission_suggestions": [
1147 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1148 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1149 ]
1150 }
1151 }"#;
1152
1153 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1154 if let ClaudeOutput::ControlRequest(req) = output {
1155 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1156 assert_eq!(perm_req.permission_suggestions.len(), 2);
1157 assert_eq!(
1158 perm_req.permission_suggestions[0].suggestion_type,
1159 PermissionType::SetMode
1160 );
1161 assert_eq!(
1162 perm_req.permission_suggestions[0].mode,
1163 Some(PermissionModeName::AcceptEdits)
1164 );
1165 assert_eq!(
1166 perm_req.permission_suggestions[0].destination,
1167 PermissionDestination::Session
1168 );
1169 assert_eq!(
1170 perm_req.permission_suggestions[1].suggestion_type,
1171 PermissionType::SetMode
1172 );
1173 assert_eq!(
1174 perm_req.permission_suggestions[1].mode,
1175 Some(PermissionModeName::BypassPermissions)
1176 );
1177 assert_eq!(
1178 perm_req.permission_suggestions[1].destination,
1179 PermissionDestination::Project
1180 );
1181 } else {
1182 panic!("Expected CanUseTool payload");
1183 }
1184 } else {
1185 panic!("Expected ControlRequest");
1186 }
1187 }
1188
1189 #[test]
1190 fn test_permission_suggestion_set_mode_roundtrip() {
1191 let suggestion = PermissionSuggestion {
1192 suggestion_type: PermissionType::SetMode,
1193 destination: PermissionDestination::Session,
1194 mode: Some(PermissionModeName::AcceptEdits),
1195 behavior: None,
1196 rules: None,
1197 };
1198
1199 let json = serde_json::to_string(&suggestion).unwrap();
1200 assert!(json.contains("\"type\":\"setMode\""));
1201 assert!(json.contains("\"mode\":\"acceptEdits\""));
1202 assert!(json.contains("\"destination\":\"session\""));
1203 assert!(!json.contains("\"behavior\""));
1204 assert!(!json.contains("\"rules\""));
1205
1206 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1207 assert_eq!(parsed, suggestion);
1208 }
1209
1210 #[test]
1211 fn test_permission_suggestion_add_rules_roundtrip() {
1212 let suggestion = PermissionSuggestion {
1213 suggestion_type: PermissionType::AddRules,
1214 destination: PermissionDestination::Session,
1215 mode: None,
1216 behavior: Some(PermissionBehavior::Allow),
1217 rules: Some(vec![serde_json::json!({
1218 "toolName": "Read",
1219 "ruleContent": "//tmp/**"
1220 })]),
1221 };
1222
1223 let json = serde_json::to_string(&suggestion).unwrap();
1224 assert!(json.contains("\"type\":\"addRules\""));
1225 assert!(json.contains("\"behavior\":\"allow\""));
1226 assert!(json.contains("\"destination\":\"session\""));
1227 assert!(json.contains("\"rules\""));
1228 assert!(json.contains("\"toolName\":\"Read\""));
1229 assert!(!json.contains("\"mode\""));
1230
1231 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1232 assert_eq!(parsed, suggestion);
1233 }
1234
1235 #[test]
1236 fn test_permission_suggestion_add_rules_from_real_json() {
1237 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1238
1239 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1240 assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1241 assert_eq!(parsed.destination, PermissionDestination::Session);
1242 assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1243 assert!(parsed.rules.is_some());
1244 assert!(parsed.mode.is_none());
1245 }
1246
1247 #[test]
1248 fn test_permission_allow_tool() {
1249 let perm = Permission::allow_tool("Bash", "npm test");
1250
1251 assert_eq!(perm.permission_type, PermissionType::AddRules);
1252 assert_eq!(perm.destination, PermissionDestination::Session);
1253 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1254 assert!(perm.mode.is_none());
1255
1256 let rules = perm.rules.unwrap();
1257 assert_eq!(rules.len(), 1);
1258 assert_eq!(rules[0].tool_name, "Bash");
1259 assert_eq!(rules[0].rule_content, "npm test");
1260 }
1261
1262 #[test]
1263 fn test_permission_allow_tool_with_destination() {
1264 let perm = Permission::allow_tool_with_destination(
1265 "Read",
1266 "/tmp/**",
1267 PermissionDestination::Project,
1268 );
1269
1270 assert_eq!(perm.permission_type, PermissionType::AddRules);
1271 assert_eq!(perm.destination, PermissionDestination::Project);
1272 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1273
1274 let rules = perm.rules.unwrap();
1275 assert_eq!(rules[0].tool_name, "Read");
1276 assert_eq!(rules[0].rule_content, "/tmp/**");
1277 }
1278
1279 #[test]
1280 fn test_permission_set_mode() {
1281 let perm = Permission::set_mode(
1282 PermissionModeName::AcceptEdits,
1283 PermissionDestination::Session,
1284 );
1285
1286 assert_eq!(perm.permission_type, PermissionType::SetMode);
1287 assert_eq!(perm.destination, PermissionDestination::Session);
1288 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1289 assert!(perm.behavior.is_none());
1290 assert!(perm.rules.is_none());
1291 }
1292
1293 #[test]
1294 fn test_permission_serialization() {
1295 let perm = Permission::allow_tool("Bash", "npm test");
1296 let json = serde_json::to_string(&perm).unwrap();
1297
1298 assert!(json.contains("\"type\":\"addRules\""));
1299 assert!(json.contains("\"destination\":\"session\""));
1300 assert!(json.contains("\"behavior\":\"allow\""));
1301 assert!(json.contains("\"toolName\":\"Bash\""));
1302 assert!(json.contains("\"ruleContent\":\"npm test\""));
1303 }
1304
1305 #[test]
1306 fn test_permission_from_suggestion_set_mode() {
1307 let suggestion = PermissionSuggestion {
1308 suggestion_type: PermissionType::SetMode,
1309 destination: PermissionDestination::Session,
1310 mode: Some(PermissionModeName::AcceptEdits),
1311 behavior: None,
1312 rules: None,
1313 };
1314
1315 let perm = Permission::from_suggestion(&suggestion);
1316
1317 assert_eq!(perm.permission_type, PermissionType::SetMode);
1318 assert_eq!(perm.destination, PermissionDestination::Session);
1319 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1320 }
1321
1322 #[test]
1323 fn test_permission_from_suggestion_add_rules() {
1324 let suggestion = PermissionSuggestion {
1325 suggestion_type: PermissionType::AddRules,
1326 destination: PermissionDestination::Session,
1327 mode: None,
1328 behavior: Some(PermissionBehavior::Allow),
1329 rules: Some(vec![serde_json::json!({
1330 "toolName": "Read",
1331 "ruleContent": "/tmp/**"
1332 })]),
1333 };
1334
1335 let perm = Permission::from_suggestion(&suggestion);
1336
1337 assert_eq!(perm.permission_type, PermissionType::AddRules);
1338 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1339
1340 let rules = perm.rules.unwrap();
1341 assert_eq!(rules.len(), 1);
1342 assert_eq!(rules[0].tool_name, "Read");
1343 assert_eq!(rules[0].rule_content, "/tmp/**");
1344 }
1345
1346 #[test]
1347 fn test_permission_result_allow_with_typed_permissions() {
1348 let result = PermissionResult::allow_with_typed_permissions(
1349 serde_json::json!({"command": "npm test"}),
1350 vec![Permission::allow_tool("Bash", "npm test")],
1351 );
1352
1353 let json = serde_json::to_string(&result).unwrap();
1354 assert!(json.contains("\"behavior\":\"allow\""));
1355 assert!(json.contains("\"updatedPermissions\""));
1356 assert!(json.contains("\"toolName\":\"Bash\""));
1357 }
1358
1359 #[test]
1360 fn test_tool_permission_request_allow_and_remember() {
1361 let req = ToolPermissionRequest {
1362 tool_name: "Bash".to_string(),
1363 input: serde_json::json!({"command": "npm test"}),
1364 permission_suggestions: vec![],
1365 blocked_path: None,
1366 decision_reason: None,
1367 tool_use_id: None,
1368 };
1369
1370 let response =
1371 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1372 let message: ControlResponseMessage = response.into();
1373 let json = serde_json::to_string(&message).unwrap();
1374
1375 assert!(json.contains("\"type\":\"control_response\""));
1376 assert!(json.contains("\"behavior\":\"allow\""));
1377 assert!(json.contains("\"updatedPermissions\""));
1378 assert!(json.contains("\"toolName\":\"Bash\""));
1379 }
1380
1381 #[test]
1382 fn test_tool_permission_request_allow_and_remember_suggestion() {
1383 let req = ToolPermissionRequest {
1384 tool_name: "Bash".to_string(),
1385 input: serde_json::json!({"command": "npm test"}),
1386 permission_suggestions: vec![PermissionSuggestion {
1387 suggestion_type: PermissionType::SetMode,
1388 destination: PermissionDestination::Session,
1389 mode: Some(PermissionModeName::AcceptEdits),
1390 behavior: None,
1391 rules: None,
1392 }],
1393 blocked_path: None,
1394 decision_reason: None,
1395 tool_use_id: None,
1396 };
1397
1398 let response = req.allow_and_remember_suggestion("req-123");
1399 assert!(response.is_some());
1400
1401 let message: ControlResponseMessage = response.unwrap().into();
1402 let json = serde_json::to_string(&message).unwrap();
1403
1404 assert!(json.contains("\"type\":\"setMode\""));
1405 assert!(json.contains("\"mode\":\"acceptEdits\""));
1406 }
1407
1408 #[test]
1409 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1410 let req = ToolPermissionRequest {
1411 tool_name: "Bash".to_string(),
1412 input: serde_json::json!({"command": "npm test"}),
1413 permission_suggestions: vec![], blocked_path: None,
1415 decision_reason: None,
1416 tool_use_id: None,
1417 };
1418
1419 let response = req.allow_and_remember_suggestion("req-123");
1420 assert!(response.is_none());
1421 }
1422}