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 pub fn answer_questions(
682 &self,
683 answers: std::collections::HashMap<String, String>,
684 request_id: &str,
685 ) -> Result<ControlResponse, serde_json::Error> {
686 let mut typed: crate::tool_inputs::AskUserQuestionInput =
687 serde_json::from_value(self.input.clone())?;
688 typed.answers = Some(answers);
689 let updated_input = serde_json::to_value(&typed)?;
690 Ok(ControlResponse::from_result(
691 request_id,
692 PermissionResult::allow(updated_input),
693 ))
694 }
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize)]
702#[serde(tag = "behavior", rename_all = "snake_case")]
703pub enum PermissionResult {
704 Allow {
706 #[serde(rename = "updatedInput")]
708 updated_input: Value,
709 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
711 updated_permissions: Option<Vec<Value>>,
712 },
713 Deny {
715 message: String,
717 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
719 interrupt: bool,
720 },
721}
722
723impl PermissionResult {
724 pub fn allow(input: Value) -> Self {
726 PermissionResult::Allow {
727 updated_input: input,
728 updated_permissions: None,
729 }
730 }
731
732 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
736 PermissionResult::Allow {
737 updated_input: input,
738 updated_permissions: Some(permissions),
739 }
740 }
741
742 pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
758 let permission_values: Vec<Value> = permissions
759 .into_iter()
760 .filter_map(|p| serde_json::to_value(p).ok())
761 .collect();
762 PermissionResult::Allow {
763 updated_input: input,
764 updated_permissions: Some(permission_values),
765 }
766 }
767
768 pub fn deny(message: impl Into<String>) -> Self {
770 PermissionResult::Deny {
771 message: message.into(),
772 interrupt: false,
773 }
774 }
775
776 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
778 PermissionResult::Deny {
779 message: message.into(),
780 interrupt: true,
781 }
782 }
783}
784
785#[derive(Debug, Clone, Serialize, Deserialize)]
787pub struct HookCallbackRequest {
788 pub callback_id: String,
789 pub input: Value,
790 #[serde(skip_serializing_if = "Option::is_none")]
791 pub tool_use_id: Option<String>,
792}
793
794#[derive(Debug, Clone, Serialize, Deserialize)]
796pub struct McpMessageRequest {
797 pub server_name: String,
798 pub message: Value,
799}
800
801#[derive(Debug, Clone, Serialize, Deserialize)]
803pub struct InitializeRequest {
804 #[serde(skip_serializing_if = "Option::is_none")]
805 pub hooks: Option<Value>,
806}
807
808#[derive(Debug, Clone, Serialize, Deserialize)]
813pub struct ControlResponse {
814 pub response: ControlResponsePayload,
816}
817
818impl ControlResponse {
819 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
823 let response_value = serde_json::to_value(&result)
825 .expect("PermissionResult serialization should never fail");
826 ControlResponse {
827 response: ControlResponsePayload::Success {
828 request_id: request_id.to_string(),
829 response: Some(response_value),
830 },
831 }
832 }
833
834 pub fn success(request_id: &str, response_data: Value) -> Self {
836 ControlResponse {
837 response: ControlResponsePayload::Success {
838 request_id: request_id.to_string(),
839 response: Some(response_data),
840 },
841 }
842 }
843
844 pub fn success_empty(request_id: &str) -> Self {
846 ControlResponse {
847 response: ControlResponsePayload::Success {
848 request_id: request_id.to_string(),
849 response: None,
850 },
851 }
852 }
853
854 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
856 ControlResponse {
857 response: ControlResponsePayload::Error {
858 request_id: request_id.to_string(),
859 error: error_message.into(),
860 },
861 }
862 }
863}
864
865#[derive(Debug, Clone, Serialize, Deserialize)]
867#[serde(tag = "subtype", rename_all = "snake_case")]
868pub enum ControlResponsePayload {
869 Success {
870 request_id: String,
871 #[serde(skip_serializing_if = "Option::is_none")]
872 response: Option<Value>,
873 },
874 Error {
875 request_id: String,
876 error: String,
877 },
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize)]
882pub struct ControlResponseMessage {
883 #[serde(rename = "type")]
884 pub message_type: String,
885 pub response: ControlResponsePayload,
886}
887
888impl From<ControlResponse> for ControlResponseMessage {
889 fn from(resp: ControlResponse) -> Self {
890 ControlResponseMessage {
891 message_type: "control_response".to_string(),
892 response: resp.response,
893 }
894 }
895}
896
897#[derive(Debug, Clone, Serialize, Deserialize)]
915pub struct SDKControlInterruptRequest {
916 subtype: SDKControlInterruptSubtype,
917}
918
919#[derive(Debug, Clone, Serialize, Deserialize)]
920enum SDKControlInterruptSubtype {
921 #[serde(rename = "interrupt")]
922 Interrupt,
923}
924
925impl SDKControlInterruptRequest {
926 pub fn new() -> Self {
928 SDKControlInterruptRequest {
929 subtype: SDKControlInterruptSubtype::Interrupt,
930 }
931 }
932}
933
934impl Default for SDKControlInterruptRequest {
935 fn default() -> Self {
936 Self::new()
937 }
938}
939
940#[derive(Debug, Clone, Serialize, Deserialize)]
942pub struct ControlRequestMessage {
943 #[serde(rename = "type")]
944 pub message_type: String,
945 pub request_id: String,
946 pub request: ControlRequestPayload,
947}
948
949impl ControlRequestMessage {
950 pub fn initialize(request_id: impl Into<String>) -> Self {
952 ControlRequestMessage {
953 message_type: "control_request".to_string(),
954 request_id: request_id.into(),
955 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
956 }
957 }
958
959 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
961 ControlRequestMessage {
962 message_type: "control_request".to_string(),
963 request_id: request_id.into(),
964 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
965 }
966 }
967}
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972 use crate::io::ClaudeOutput;
973
974 #[test]
975 fn test_deserialize_control_request_can_use_tool() {
976 let json = r#"{
977 "type": "control_request",
978 "request_id": "perm-abc123",
979 "request": {
980 "subtype": "can_use_tool",
981 "tool_name": "Write",
982 "input": {
983 "file_path": "/home/user/hello.py",
984 "content": "print('hello')"
985 },
986 "permission_suggestions": [],
987 "blocked_path": null
988 }
989 }"#;
990
991 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
992 assert!(output.is_control_request());
993
994 if let ClaudeOutput::ControlRequest(req) = output {
995 assert_eq!(req.request_id, "perm-abc123");
996 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
997 assert_eq!(perm_req.tool_name, "Write");
998 assert_eq!(
999 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1000 "/home/user/hello.py"
1001 );
1002 } else {
1003 panic!("Expected CanUseTool payload");
1004 }
1005 } else {
1006 panic!("Expected ControlRequest");
1007 }
1008 }
1009
1010 #[test]
1011 fn test_deserialize_control_request_edit_tool_real() {
1012 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"}}"#;
1014
1015 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1016 assert!(output.is_control_request());
1017 assert_eq!(output.message_type(), "control_request");
1018
1019 if let ClaudeOutput::ControlRequest(req) = output {
1020 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1021 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1022 assert_eq!(perm_req.tool_name, "Edit");
1023 assert_eq!(
1024 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1025 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1026 );
1027 assert!(perm_req.input.get("old_string").is_some());
1028 assert!(perm_req.input.get("new_string").is_some());
1029 assert!(!perm_req
1030 .input
1031 .get("replace_all")
1032 .unwrap()
1033 .as_bool()
1034 .unwrap());
1035 } else {
1036 panic!("Expected CanUseTool payload");
1037 }
1038 } else {
1039 panic!("Expected ControlRequest");
1040 }
1041 }
1042
1043 #[test]
1044 fn test_tool_permission_request_allow() {
1045 let req = ToolPermissionRequest {
1046 tool_name: "Read".to_string(),
1047 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1048 permission_suggestions: vec![],
1049 blocked_path: None,
1050 decision_reason: None,
1051 tool_use_id: None,
1052 };
1053
1054 let response = req.allow("req-123");
1055 let message: ControlResponseMessage = response.into();
1056
1057 let json = serde_json::to_string(&message).unwrap();
1058 assert!(json.contains("\"type\":\"control_response\""));
1059 assert!(json.contains("\"subtype\":\"success\""));
1060 assert!(json.contains("\"request_id\":\"req-123\""));
1061 assert!(json.contains("\"behavior\":\"allow\""));
1062 assert!(json.contains("\"updatedInput\""));
1063 }
1064
1065 #[test]
1066 fn test_tool_permission_request_allow_with_modified_input() {
1067 let req = ToolPermissionRequest {
1068 tool_name: "Write".to_string(),
1069 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1070 permission_suggestions: vec![],
1071 blocked_path: None,
1072 decision_reason: None,
1073 tool_use_id: None,
1074 };
1075
1076 let modified_input = serde_json::json!({
1077 "file_path": "/tmp/safe/passwd",
1078 "content": "test"
1079 });
1080 let response = req.allow_with(modified_input, "req-456");
1081 let message: ControlResponseMessage = response.into();
1082
1083 let json = serde_json::to_string(&message).unwrap();
1084 assert!(json.contains("/tmp/safe/passwd"));
1085 assert!(!json.contains("/etc/passwd"));
1086 }
1087
1088 #[test]
1089 fn test_tool_permission_request_deny() {
1090 let req = ToolPermissionRequest {
1091 tool_name: "Bash".to_string(),
1092 input: serde_json::json!({"command": "sudo rm -rf /"}),
1093 permission_suggestions: vec![],
1094 blocked_path: None,
1095 decision_reason: None,
1096 tool_use_id: None,
1097 };
1098
1099 let response = req.deny("Dangerous command blocked", "req-789");
1100 let message: ControlResponseMessage = response.into();
1101
1102 let json = serde_json::to_string(&message).unwrap();
1103 assert!(json.contains("\"behavior\":\"deny\""));
1104 assert!(json.contains("Dangerous command blocked"));
1105 assert!(!json.contains("\"interrupt\":true"));
1106 }
1107
1108 #[test]
1109 fn test_tool_permission_request_deny_and_stop() {
1110 let req = ToolPermissionRequest {
1111 tool_name: "Bash".to_string(),
1112 input: serde_json::json!({"command": "rm -rf /"}),
1113 permission_suggestions: vec![],
1114 blocked_path: None,
1115 decision_reason: None,
1116 tool_use_id: None,
1117 };
1118
1119 let response = req.deny_and_stop("Security violation", "req-000");
1120 let message: ControlResponseMessage = response.into();
1121
1122 let json = serde_json::to_string(&message).unwrap();
1123 assert!(json.contains("\"behavior\":\"deny\""));
1124 assert!(json.contains("\"interrupt\":true"));
1125 }
1126
1127 #[test]
1128 fn test_permission_result_serialization() {
1129 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1131 let json = serde_json::to_string(&allow).unwrap();
1132 assert!(json.contains("\"behavior\":\"allow\""));
1133 assert!(json.contains("\"updatedInput\""));
1134
1135 let deny = PermissionResult::deny("Not allowed");
1137 let json = serde_json::to_string(&deny).unwrap();
1138 assert!(json.contains("\"behavior\":\"deny\""));
1139 assert!(json.contains("\"message\":\"Not allowed\""));
1140 assert!(!json.contains("\"interrupt\""));
1141
1142 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1144 let json = serde_json::to_string(&deny_stop).unwrap();
1145 assert!(json.contains("\"interrupt\":true"));
1146 }
1147
1148 #[test]
1149 fn test_control_request_message_initialize() {
1150 let init = ControlRequestMessage::initialize("init-1");
1151
1152 let json = serde_json::to_string(&init).unwrap();
1153 assert!(json.contains("\"type\":\"control_request\""));
1154 assert!(json.contains("\"request_id\":\"init-1\""));
1155 assert!(json.contains("\"subtype\":\"initialize\""));
1156 }
1157
1158 #[test]
1159 fn test_control_response_error() {
1160 let response = ControlResponse::error("req-err", "Something went wrong");
1161 let message: ControlResponseMessage = response.into();
1162
1163 let json = serde_json::to_string(&message).unwrap();
1164 assert!(json.contains("\"subtype\":\"error\""));
1165 assert!(json.contains("\"error\":\"Something went wrong\""));
1166 }
1167
1168 #[test]
1169 fn test_roundtrip_control_request() {
1170 let original_json = r#"{
1171 "type": "control_request",
1172 "request_id": "test-123",
1173 "request": {
1174 "subtype": "can_use_tool",
1175 "tool_name": "Bash",
1176 "input": {"command": "ls -la"},
1177 "permission_suggestions": []
1178 }
1179 }"#;
1180
1181 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1182
1183 let reserialized = serde_json::to_string(&output).unwrap();
1184 assert!(reserialized.contains("control_request"));
1185 assert!(reserialized.contains("test-123"));
1186 assert!(reserialized.contains("Bash"));
1187 }
1188
1189 #[test]
1190 fn test_permission_suggestions_parsing() {
1191 let json = r#"{
1192 "type": "control_request",
1193 "request_id": "perm-456",
1194 "request": {
1195 "subtype": "can_use_tool",
1196 "tool_name": "Bash",
1197 "input": {"command": "npm test"},
1198 "permission_suggestions": [
1199 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1200 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1201 ]
1202 }
1203 }"#;
1204
1205 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1206 if let ClaudeOutput::ControlRequest(req) = output {
1207 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1208 assert_eq!(perm_req.permission_suggestions.len(), 2);
1209 assert_eq!(
1210 perm_req.permission_suggestions[0].suggestion_type,
1211 PermissionType::SetMode
1212 );
1213 assert_eq!(
1214 perm_req.permission_suggestions[0].mode,
1215 Some(PermissionModeName::AcceptEdits)
1216 );
1217 assert_eq!(
1218 perm_req.permission_suggestions[0].destination,
1219 PermissionDestination::Session
1220 );
1221 assert_eq!(
1222 perm_req.permission_suggestions[1].suggestion_type,
1223 PermissionType::SetMode
1224 );
1225 assert_eq!(
1226 perm_req.permission_suggestions[1].mode,
1227 Some(PermissionModeName::BypassPermissions)
1228 );
1229 assert_eq!(
1230 perm_req.permission_suggestions[1].destination,
1231 PermissionDestination::Project
1232 );
1233 } else {
1234 panic!("Expected CanUseTool payload");
1235 }
1236 } else {
1237 panic!("Expected ControlRequest");
1238 }
1239 }
1240
1241 #[test]
1242 fn test_permission_suggestion_set_mode_roundtrip() {
1243 let suggestion = PermissionSuggestion {
1244 suggestion_type: PermissionType::SetMode,
1245 destination: PermissionDestination::Session,
1246 mode: Some(PermissionModeName::AcceptEdits),
1247 behavior: None,
1248 rules: None,
1249 };
1250
1251 let json = serde_json::to_string(&suggestion).unwrap();
1252 assert!(json.contains("\"type\":\"setMode\""));
1253 assert!(json.contains("\"mode\":\"acceptEdits\""));
1254 assert!(json.contains("\"destination\":\"session\""));
1255 assert!(!json.contains("\"behavior\""));
1256 assert!(!json.contains("\"rules\""));
1257
1258 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1259 assert_eq!(parsed, suggestion);
1260 }
1261
1262 #[test]
1263 fn test_permission_suggestion_add_rules_roundtrip() {
1264 let suggestion = PermissionSuggestion {
1265 suggestion_type: PermissionType::AddRules,
1266 destination: PermissionDestination::Session,
1267 mode: None,
1268 behavior: Some(PermissionBehavior::Allow),
1269 rules: Some(vec![serde_json::json!({
1270 "toolName": "Read",
1271 "ruleContent": "//tmp/**"
1272 })]),
1273 };
1274
1275 let json = serde_json::to_string(&suggestion).unwrap();
1276 assert!(json.contains("\"type\":\"addRules\""));
1277 assert!(json.contains("\"behavior\":\"allow\""));
1278 assert!(json.contains("\"destination\":\"session\""));
1279 assert!(json.contains("\"rules\""));
1280 assert!(json.contains("\"toolName\":\"Read\""));
1281 assert!(!json.contains("\"mode\""));
1282
1283 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1284 assert_eq!(parsed, suggestion);
1285 }
1286
1287 #[test]
1288 fn test_permission_suggestion_add_rules_from_real_json() {
1289 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1290
1291 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1292 assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1293 assert_eq!(parsed.destination, PermissionDestination::Session);
1294 assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1295 assert!(parsed.rules.is_some());
1296 assert!(parsed.mode.is_none());
1297 }
1298
1299 #[test]
1300 fn test_permission_allow_tool() {
1301 let perm = Permission::allow_tool("Bash", "npm test");
1302
1303 assert_eq!(perm.permission_type, PermissionType::AddRules);
1304 assert_eq!(perm.destination, PermissionDestination::Session);
1305 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1306 assert!(perm.mode.is_none());
1307
1308 let rules = perm.rules.unwrap();
1309 assert_eq!(rules.len(), 1);
1310 assert_eq!(rules[0].tool_name, "Bash");
1311 assert_eq!(rules[0].rule_content, "npm test");
1312 }
1313
1314 #[test]
1315 fn test_permission_allow_tool_with_destination() {
1316 let perm = Permission::allow_tool_with_destination(
1317 "Read",
1318 "/tmp/**",
1319 PermissionDestination::Project,
1320 );
1321
1322 assert_eq!(perm.permission_type, PermissionType::AddRules);
1323 assert_eq!(perm.destination, PermissionDestination::Project);
1324 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1325
1326 let rules = perm.rules.unwrap();
1327 assert_eq!(rules[0].tool_name, "Read");
1328 assert_eq!(rules[0].rule_content, "/tmp/**");
1329 }
1330
1331 #[test]
1332 fn test_permission_set_mode() {
1333 let perm = Permission::set_mode(
1334 PermissionModeName::AcceptEdits,
1335 PermissionDestination::Session,
1336 );
1337
1338 assert_eq!(perm.permission_type, PermissionType::SetMode);
1339 assert_eq!(perm.destination, PermissionDestination::Session);
1340 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1341 assert!(perm.behavior.is_none());
1342 assert!(perm.rules.is_none());
1343 }
1344
1345 #[test]
1346 fn test_permission_serialization() {
1347 let perm = Permission::allow_tool("Bash", "npm test");
1348 let json = serde_json::to_string(&perm).unwrap();
1349
1350 assert!(json.contains("\"type\":\"addRules\""));
1351 assert!(json.contains("\"destination\":\"session\""));
1352 assert!(json.contains("\"behavior\":\"allow\""));
1353 assert!(json.contains("\"toolName\":\"Bash\""));
1354 assert!(json.contains("\"ruleContent\":\"npm test\""));
1355 }
1356
1357 #[test]
1358 fn test_permission_from_suggestion_set_mode() {
1359 let suggestion = PermissionSuggestion {
1360 suggestion_type: PermissionType::SetMode,
1361 destination: PermissionDestination::Session,
1362 mode: Some(PermissionModeName::AcceptEdits),
1363 behavior: None,
1364 rules: None,
1365 };
1366
1367 let perm = Permission::from_suggestion(&suggestion);
1368
1369 assert_eq!(perm.permission_type, PermissionType::SetMode);
1370 assert_eq!(perm.destination, PermissionDestination::Session);
1371 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1372 }
1373
1374 #[test]
1375 fn test_permission_from_suggestion_add_rules() {
1376 let suggestion = PermissionSuggestion {
1377 suggestion_type: PermissionType::AddRules,
1378 destination: PermissionDestination::Session,
1379 mode: None,
1380 behavior: Some(PermissionBehavior::Allow),
1381 rules: Some(vec![serde_json::json!({
1382 "toolName": "Read",
1383 "ruleContent": "/tmp/**"
1384 })]),
1385 };
1386
1387 let perm = Permission::from_suggestion(&suggestion);
1388
1389 assert_eq!(perm.permission_type, PermissionType::AddRules);
1390 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1391
1392 let rules = perm.rules.unwrap();
1393 assert_eq!(rules.len(), 1);
1394 assert_eq!(rules[0].tool_name, "Read");
1395 assert_eq!(rules[0].rule_content, "/tmp/**");
1396 }
1397
1398 #[test]
1399 fn test_permission_result_allow_with_typed_permissions() {
1400 let result = PermissionResult::allow_with_typed_permissions(
1401 serde_json::json!({"command": "npm test"}),
1402 vec![Permission::allow_tool("Bash", "npm test")],
1403 );
1404
1405 let json = serde_json::to_string(&result).unwrap();
1406 assert!(json.contains("\"behavior\":\"allow\""));
1407 assert!(json.contains("\"updatedPermissions\""));
1408 assert!(json.contains("\"toolName\":\"Bash\""));
1409 }
1410
1411 #[test]
1412 fn test_tool_permission_request_allow_and_remember() {
1413 let req = ToolPermissionRequest {
1414 tool_name: "Bash".to_string(),
1415 input: serde_json::json!({"command": "npm test"}),
1416 permission_suggestions: vec![],
1417 blocked_path: None,
1418 decision_reason: None,
1419 tool_use_id: None,
1420 };
1421
1422 let response =
1423 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1424 let message: ControlResponseMessage = response.into();
1425 let json = serde_json::to_string(&message).unwrap();
1426
1427 assert!(json.contains("\"type\":\"control_response\""));
1428 assert!(json.contains("\"behavior\":\"allow\""));
1429 assert!(json.contains("\"updatedPermissions\""));
1430 assert!(json.contains("\"toolName\":\"Bash\""));
1431 }
1432
1433 #[test]
1434 fn test_tool_permission_request_allow_and_remember_suggestion() {
1435 let req = ToolPermissionRequest {
1436 tool_name: "Bash".to_string(),
1437 input: serde_json::json!({"command": "npm test"}),
1438 permission_suggestions: vec![PermissionSuggestion {
1439 suggestion_type: PermissionType::SetMode,
1440 destination: PermissionDestination::Session,
1441 mode: Some(PermissionModeName::AcceptEdits),
1442 behavior: None,
1443 rules: None,
1444 }],
1445 blocked_path: None,
1446 decision_reason: None,
1447 tool_use_id: None,
1448 };
1449
1450 let response = req.allow_and_remember_suggestion("req-123");
1451 assert!(response.is_some());
1452
1453 let message: ControlResponseMessage = response.unwrap().into();
1454 let json = serde_json::to_string(&message).unwrap();
1455
1456 assert!(json.contains("\"type\":\"setMode\""));
1457 assert!(json.contains("\"mode\":\"acceptEdits\""));
1458 }
1459
1460 #[test]
1461 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1462 let req = ToolPermissionRequest {
1463 tool_name: "Bash".to_string(),
1464 input: serde_json::json!({"command": "npm test"}),
1465 permission_suggestions: vec![], blocked_path: None,
1467 decision_reason: None,
1468 tool_use_id: None,
1469 };
1470
1471 let response = req.allow_and_remember_suggestion("req-123");
1472 assert!(response.is_none());
1473 }
1474
1475 #[test]
1488 fn test_ask_user_question_answer_preserves_questions_in_updated_input() {
1489 let req = ToolPermissionRequest {
1490 tool_name: "AskUserQuestion".to_string(),
1491 input: serde_json::json!({
1492 "questions": [{
1493 "question": "Which color do you prefer?",
1494 "header": "Color",
1495 "options": [
1496 {"label": "Red", "description": "A warm color"},
1497 {"label": "Blue", "description": "A cool color"},
1498 ],
1499 "multiSelect": false,
1500 }],
1501 }),
1502 permission_suggestions: vec![],
1503 blocked_path: None,
1504 decision_reason: None,
1505 tool_use_id: None,
1506 };
1507
1508 let mut answers = std::collections::HashMap::new();
1509 answers.insert("Color".to_string(), "Blue".to_string());
1510
1511 let response = req
1512 .answer_questions(answers, "req-q1")
1513 .expect("AskUserQuestion input round-trips through the typed helper");
1514 let wire = serde_json::to_value(&response).expect("ControlResponse serializes");
1515
1516 let updated_input = &wire["response"]["response"]["updatedInput"];
1520 assert_eq!(
1521 updated_input["questions"][0]["header"], "Color",
1522 "questions array must round-trip in updatedInput so the \
1523 frontend's `questions.map(...)` doesn't fault on undefined"
1524 );
1525 assert_eq!(updated_input["answers"]["Color"], "Blue");
1526 }
1527}