1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::fmt;
5
6use crate::tool_inputs::AskUserQuestionInput;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub enum PermissionType {
18 AddRules,
20 SetMode,
22 Unknown(String),
24}
25
26impl PermissionType {
27 pub fn as_str(&self) -> &str {
28 match self {
29 Self::AddRules => "addRules",
30 Self::SetMode => "setMode",
31 Self::Unknown(s) => s.as_str(),
32 }
33 }
34}
35
36impl fmt::Display for PermissionType {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 f.write_str(self.as_str())
39 }
40}
41
42impl From<&str> for PermissionType {
43 fn from(s: &str) -> Self {
44 match s {
45 "addRules" => Self::AddRules,
46 "setMode" => Self::SetMode,
47 other => Self::Unknown(other.to_string()),
48 }
49 }
50}
51
52impl Serialize for PermissionType {
53 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
54 serializer.serialize_str(self.as_str())
55 }
56}
57
58impl<'de> Deserialize<'de> for PermissionType {
59 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
60 let s = String::deserialize(deserializer)?;
61 Ok(Self::from(s.as_str()))
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub enum PermissionDestination {
68 Session,
70 Project,
72 Unknown(String),
74}
75
76impl PermissionDestination {
77 pub fn as_str(&self) -> &str {
78 match self {
79 Self::Session => "session",
80 Self::Project => "project",
81 Self::Unknown(s) => s.as_str(),
82 }
83 }
84}
85
86impl fmt::Display for PermissionDestination {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.write_str(self.as_str())
89 }
90}
91
92impl From<&str> for PermissionDestination {
93 fn from(s: &str) -> Self {
94 match s {
95 "session" => Self::Session,
96 "project" => Self::Project,
97 other => Self::Unknown(other.to_string()),
98 }
99 }
100}
101
102impl Serialize for PermissionDestination {
103 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
104 serializer.serialize_str(self.as_str())
105 }
106}
107
108impl<'de> Deserialize<'de> for PermissionDestination {
109 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
110 let s = String::deserialize(deserializer)?;
111 Ok(Self::from(s.as_str()))
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash)]
117pub enum PermissionBehavior {
118 Allow,
120 Deny,
122 Unknown(String),
124}
125
126impl PermissionBehavior {
127 pub fn as_str(&self) -> &str {
128 match self {
129 Self::Allow => "allow",
130 Self::Deny => "deny",
131 Self::Unknown(s) => s.as_str(),
132 }
133 }
134}
135
136impl fmt::Display for PermissionBehavior {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 f.write_str(self.as_str())
139 }
140}
141
142impl From<&str> for PermissionBehavior {
143 fn from(s: &str) -> Self {
144 match s {
145 "allow" => Self::Allow,
146 "deny" => Self::Deny,
147 other => Self::Unknown(other.to_string()),
148 }
149 }
150}
151
152impl Serialize for PermissionBehavior {
153 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
154 serializer.serialize_str(self.as_str())
155 }
156}
157
158impl<'de> Deserialize<'de> for PermissionBehavior {
159 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
160 let s = String::deserialize(deserializer)?;
161 Ok(Self::from(s.as_str()))
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Hash)]
167pub enum PermissionModeName {
168 AcceptEdits,
170 BypassPermissions,
172 Unknown(String),
174}
175
176impl PermissionModeName {
177 pub fn as_str(&self) -> &str {
178 match self {
179 Self::AcceptEdits => "acceptEdits",
180 Self::BypassPermissions => "bypassPermissions",
181 Self::Unknown(s) => s.as_str(),
182 }
183 }
184}
185
186impl fmt::Display for PermissionModeName {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.write_str(self.as_str())
189 }
190}
191
192impl From<&str> for PermissionModeName {
193 fn from(s: &str) -> Self {
194 match s {
195 "acceptEdits" => Self::AcceptEdits,
196 "bypassPermissions" => Self::BypassPermissions,
197 other => Self::Unknown(other.to_string()),
198 }
199 }
200}
201
202impl Serialize for PermissionModeName {
203 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
204 serializer.serialize_str(self.as_str())
205 }
206}
207
208impl<'de> Deserialize<'de> for PermissionModeName {
209 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
210 let s = String::deserialize(deserializer)?;
211 Ok(Self::from(s.as_str()))
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct ControlRequest {
226 pub request_id: String,
228 pub request: ControlRequestPayload,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(tag = "subtype", rename_all = "snake_case")]
235pub enum ControlRequestPayload {
236 CanUseTool(ToolPermissionRequest),
238 HookCallback(HookCallbackRequest),
240 McpMessage(McpMessageRequest),
242 Initialize(InitializeRequest),
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
263pub struct Permission {
264 #[serde(rename = "type")]
266 pub permission_type: PermissionType,
267 pub destination: PermissionDestination,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub mode: Option<PermissionModeName>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub behavior: Option<PermissionBehavior>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub rules: Option<Vec<PermissionRule>>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
282pub struct PermissionRule {
283 #[serde(rename = "toolName")]
285 pub tool_name: String,
286 #[serde(rename = "ruleContent")]
288 pub rule_content: String,
289}
290
291impl Permission {
292 pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
305 Permission {
306 permission_type: PermissionType::AddRules,
307 destination: PermissionDestination::Session,
308 mode: None,
309 behavior: Some(PermissionBehavior::Allow),
310 rules: Some(vec![PermissionRule {
311 tool_name: tool_name.into(),
312 rule_content: rule_content.into(),
313 }]),
314 }
315 }
316
317 pub fn allow_tool_with_destination(
327 tool_name: impl Into<String>,
328 rule_content: impl Into<String>,
329 destination: PermissionDestination,
330 ) -> Self {
331 Permission {
332 permission_type: PermissionType::AddRules,
333 destination,
334 mode: None,
335 behavior: Some(PermissionBehavior::Allow),
336 rules: Some(vec![PermissionRule {
337 tool_name: tool_name.into(),
338 rule_content: rule_content.into(),
339 }]),
340 }
341 }
342
343 pub fn set_mode(mode: PermissionModeName, destination: PermissionDestination) -> Self {
353 Permission {
354 permission_type: PermissionType::SetMode,
355 destination,
356 mode: Some(mode),
357 behavior: None,
358 rules: None,
359 }
360 }
361
362 pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
381 Permission {
382 permission_type: suggestion.suggestion_type.clone(),
383 destination: suggestion.destination.clone(),
384 mode: suggestion.mode.clone(),
385 behavior: suggestion.behavior.clone(),
386 rules: suggestion.rules.as_ref().map(|rules| {
387 rules
388 .iter()
389 .filter_map(|v| {
390 Some(PermissionRule {
391 tool_name: v.get("toolName")?.as_str()?.to_string(),
392 rule_content: v.get("ruleContent")?.as_str()?.to_string(),
393 })
394 })
395 .collect()
396 }),
397 }
398 }
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
412pub struct PermissionSuggestion {
413 #[serde(rename = "type")]
415 pub suggestion_type: PermissionType,
416 pub destination: PermissionDestination,
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub mode: Option<PermissionModeName>,
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub behavior: Option<PermissionBehavior>,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub rules: Option<Vec<Value>>,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct ToolPermissionRequest {
456 pub tool_name: String,
458 pub input: Value,
460 #[serde(default)]
462 pub permission_suggestions: Vec<PermissionSuggestion>,
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub blocked_path: Option<String>,
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub decision_reason: Option<String>,
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub tool_use_id: Option<String>,
472}
473
474impl ToolPermissionRequest {
475 pub fn allow(&self, request_id: &str) -> ControlResponse {
492 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
493 }
494
495 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
517 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
518 }
519
520 pub fn allow_with_permissions(
524 &self,
525 modified_input: Value,
526 permissions: Vec<Value>,
527 request_id: &str,
528 ) -> ControlResponse {
529 ControlResponse::from_result(
530 request_id,
531 PermissionResult::allow_with_permissions(modified_input, permissions),
532 )
533 }
534
535 pub fn allow_and_remember(
561 &self,
562 permissions: Vec<Permission>,
563 request_id: &str,
564 ) -> ControlResponse {
565 ControlResponse::from_result(
566 request_id,
567 PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
568 )
569 }
570
571 pub fn allow_with_and_remember(
575 &self,
576 modified_input: Value,
577 permissions: Vec<Permission>,
578 request_id: &str,
579 ) -> ControlResponse {
580 ControlResponse::from_result(
581 request_id,
582 PermissionResult::allow_with_typed_permissions(modified_input, permissions),
583 )
584 }
585
586 pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
612 self.permission_suggestions.first().map(|suggestion| {
613 let perm = Permission::from_suggestion(suggestion);
614 self.allow_and_remember(vec![perm], request_id)
615 })
616 }
617
618 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
637 ControlResponse::from_result(request_id, PermissionResult::deny(message))
638 }
639
640 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
644 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
645 }
646
647 pub fn answer_questions(
701 &self,
702 answers_by_index: &HashMap<usize, String>,
703 request_id: &str,
704 ) -> Result<ControlResponse, AskUserQuestionResponseError> {
705 if self.tool_name != "AskUserQuestion" {
706 return Err(AskUserQuestionResponseError::WrongTool(
707 self.tool_name.clone(),
708 ));
709 }
710 let parsed: AskUserQuestionInput = serde_json::from_value(self.input.clone())
711 .map_err(AskUserQuestionResponseError::ParseInput)?;
712 let total = parsed.questions.len();
713
714 let mut answers_map = serde_json::Map::new();
715 for (idx, answer) in answers_by_index {
716 let q = parsed.questions.get(*idx).ok_or(
717 AskUserQuestionResponseError::QuestionIndexOutOfRange { index: *idx, total },
718 )?;
719 answers_map.insert(q.question.clone(), Value::String(answer.clone()));
720 }
721
722 let mut updated_input = self.input.clone();
723 updated_input
725 .as_object_mut()
726 .expect("AskUserQuestion input is a JSON object")
727 .insert("answers".to_string(), Value::Object(answers_map));
728
729 Ok(ControlResponse::from_result(
730 request_id,
731 PermissionResult::allow(updated_input),
732 ))
733 }
734}
735
736#[derive(Debug, thiserror::Error)]
739pub enum AskUserQuestionResponseError {
740 #[error("expected tool_name=AskUserQuestion, got `{0}`")]
742 WrongTool(String),
743 #[error("failed to parse AskUserQuestion input: {0}")]
745 ParseInput(#[source] serde_json::Error),
746 #[error("answer references question index {index}, but only {total} question(s) were asked")]
748 QuestionIndexOutOfRange {
749 index: usize,
751 total: usize,
753 },
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
761#[serde(tag = "behavior", rename_all = "snake_case")]
762pub enum PermissionResult {
763 Allow {
765 #[serde(rename = "updatedInput")]
767 updated_input: Value,
768 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
770 updated_permissions: Option<Vec<Value>>,
771 },
772 Deny {
774 message: String,
776 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
778 interrupt: bool,
779 },
780}
781
782impl PermissionResult {
783 pub fn allow(input: Value) -> Self {
785 PermissionResult::Allow {
786 updated_input: input,
787 updated_permissions: None,
788 }
789 }
790
791 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
795 PermissionResult::Allow {
796 updated_input: input,
797 updated_permissions: Some(permissions),
798 }
799 }
800
801 pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
817 let permission_values: Vec<Value> = permissions
818 .into_iter()
819 .filter_map(|p| serde_json::to_value(p).ok())
820 .collect();
821 PermissionResult::Allow {
822 updated_input: input,
823 updated_permissions: Some(permission_values),
824 }
825 }
826
827 pub fn deny(message: impl Into<String>) -> Self {
829 PermissionResult::Deny {
830 message: message.into(),
831 interrupt: false,
832 }
833 }
834
835 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
837 PermissionResult::Deny {
838 message: message.into(),
839 interrupt: true,
840 }
841 }
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize)]
846pub struct HookCallbackRequest {
847 pub callback_id: String,
848 pub input: Value,
849 #[serde(skip_serializing_if = "Option::is_none")]
850 pub tool_use_id: Option<String>,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize)]
855pub struct McpMessageRequest {
856 pub server_name: String,
857 pub message: Value,
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize)]
862pub struct InitializeRequest {
863 #[serde(skip_serializing_if = "Option::is_none")]
864 pub hooks: Option<Value>,
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct ControlResponse {
873 pub response: ControlResponsePayload,
875}
876
877impl ControlResponse {
878 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
882 let response_value = serde_json::to_value(&result)
884 .expect("PermissionResult serialization should never fail");
885 ControlResponse {
886 response: ControlResponsePayload::Success {
887 request_id: request_id.to_string(),
888 response: Some(response_value),
889 },
890 }
891 }
892
893 pub fn success(request_id: &str, response_data: Value) -> Self {
895 ControlResponse {
896 response: ControlResponsePayload::Success {
897 request_id: request_id.to_string(),
898 response: Some(response_data),
899 },
900 }
901 }
902
903 pub fn success_empty(request_id: &str) -> Self {
905 ControlResponse {
906 response: ControlResponsePayload::Success {
907 request_id: request_id.to_string(),
908 response: None,
909 },
910 }
911 }
912
913 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
915 ControlResponse {
916 response: ControlResponsePayload::Error {
917 request_id: request_id.to_string(),
918 error: error_message.into(),
919 },
920 }
921 }
922}
923
924#[derive(Debug, Clone, Serialize, Deserialize)]
926#[serde(tag = "subtype", rename_all = "snake_case")]
927pub enum ControlResponsePayload {
928 Success {
929 request_id: String,
930 #[serde(skip_serializing_if = "Option::is_none")]
931 response: Option<Value>,
932 },
933 Error {
934 request_id: String,
935 error: String,
936 },
937}
938
939#[derive(Debug, Clone, Serialize, Deserialize)]
941pub struct ControlResponseMessage {
942 #[serde(rename = "type")]
943 pub message_type: String,
944 pub response: ControlResponsePayload,
945}
946
947impl From<ControlResponse> for ControlResponseMessage {
948 fn from(resp: ControlResponse) -> Self {
949 ControlResponseMessage {
950 message_type: "control_response".to_string(),
951 response: resp.response,
952 }
953 }
954}
955
956#[derive(Debug, Clone, Serialize, Deserialize)]
974pub struct SDKControlInterruptRequest {
975 subtype: SDKControlInterruptSubtype,
976}
977
978#[derive(Debug, Clone, Serialize, Deserialize)]
979enum SDKControlInterruptSubtype {
980 #[serde(rename = "interrupt")]
981 Interrupt,
982}
983
984impl SDKControlInterruptRequest {
985 pub fn new() -> Self {
987 SDKControlInterruptRequest {
988 subtype: SDKControlInterruptSubtype::Interrupt,
989 }
990 }
991}
992
993impl Default for SDKControlInterruptRequest {
994 fn default() -> Self {
995 Self::new()
996 }
997}
998
999#[derive(Debug, Clone, Serialize, Deserialize)]
1001pub struct ControlRequestMessage {
1002 #[serde(rename = "type")]
1003 pub message_type: String,
1004 pub request_id: String,
1005 pub request: ControlRequestPayload,
1006}
1007
1008impl ControlRequestMessage {
1009 pub fn initialize(request_id: impl Into<String>) -> Self {
1011 ControlRequestMessage {
1012 message_type: "control_request".to_string(),
1013 request_id: request_id.into(),
1014 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
1015 }
1016 }
1017
1018 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
1020 ControlRequestMessage {
1021 message_type: "control_request".to_string(),
1022 request_id: request_id.into(),
1023 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
1024 }
1025 }
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030 use super::*;
1031 use crate::io::ClaudeOutput;
1032
1033 #[test]
1034 fn test_deserialize_control_request_can_use_tool() {
1035 let json = r#"{
1036 "type": "control_request",
1037 "request_id": "perm-abc123",
1038 "request": {
1039 "subtype": "can_use_tool",
1040 "tool_name": "Write",
1041 "input": {
1042 "file_path": "/home/user/hello.py",
1043 "content": "print('hello')"
1044 },
1045 "permission_suggestions": [],
1046 "blocked_path": null
1047 }
1048 }"#;
1049
1050 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1051 assert!(output.is_control_request());
1052
1053 if let ClaudeOutput::ControlRequest(req) = output {
1054 assert_eq!(req.request_id, "perm-abc123");
1055 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1056 assert_eq!(perm_req.tool_name, "Write");
1057 assert_eq!(
1058 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1059 "/home/user/hello.py"
1060 );
1061 } else {
1062 panic!("Expected CanUseTool payload");
1063 }
1064 } else {
1065 panic!("Expected ControlRequest");
1066 }
1067 }
1068
1069 #[test]
1070 fn test_deserialize_control_request_edit_tool_real() {
1071 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"}}"#;
1073
1074 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1075 assert!(output.is_control_request());
1076 assert_eq!(output.message_type(), "control_request");
1077
1078 if let ClaudeOutput::ControlRequest(req) = output {
1079 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1080 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1081 assert_eq!(perm_req.tool_name, "Edit");
1082 assert_eq!(
1083 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1084 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1085 );
1086 assert!(perm_req.input.get("old_string").is_some());
1087 assert!(perm_req.input.get("new_string").is_some());
1088 assert!(!perm_req
1089 .input
1090 .get("replace_all")
1091 .unwrap()
1092 .as_bool()
1093 .unwrap());
1094 } else {
1095 panic!("Expected CanUseTool payload");
1096 }
1097 } else {
1098 panic!("Expected ControlRequest");
1099 }
1100 }
1101
1102 #[test]
1103 fn test_tool_permission_request_allow() {
1104 let req = ToolPermissionRequest {
1105 tool_name: "Read".to_string(),
1106 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1107 permission_suggestions: vec![],
1108 blocked_path: None,
1109 decision_reason: None,
1110 tool_use_id: None,
1111 };
1112
1113 let response = req.allow("req-123");
1114 let message: ControlResponseMessage = response.into();
1115
1116 let json = serde_json::to_string(&message).unwrap();
1117 assert!(json.contains("\"type\":\"control_response\""));
1118 assert!(json.contains("\"subtype\":\"success\""));
1119 assert!(json.contains("\"request_id\":\"req-123\""));
1120 assert!(json.contains("\"behavior\":\"allow\""));
1121 assert!(json.contains("\"updatedInput\""));
1122 }
1123
1124 #[test]
1125 fn test_tool_permission_request_allow_with_modified_input() {
1126 let req = ToolPermissionRequest {
1127 tool_name: "Write".to_string(),
1128 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1129 permission_suggestions: vec![],
1130 blocked_path: None,
1131 decision_reason: None,
1132 tool_use_id: None,
1133 };
1134
1135 let modified_input = serde_json::json!({
1136 "file_path": "/tmp/safe/passwd",
1137 "content": "test"
1138 });
1139 let response = req.allow_with(modified_input, "req-456");
1140 let message: ControlResponseMessage = response.into();
1141
1142 let json = serde_json::to_string(&message).unwrap();
1143 assert!(json.contains("/tmp/safe/passwd"));
1144 assert!(!json.contains("/etc/passwd"));
1145 }
1146
1147 #[test]
1148 fn test_tool_permission_request_deny() {
1149 let req = ToolPermissionRequest {
1150 tool_name: "Bash".to_string(),
1151 input: serde_json::json!({"command": "sudo rm -rf /"}),
1152 permission_suggestions: vec![],
1153 blocked_path: None,
1154 decision_reason: None,
1155 tool_use_id: None,
1156 };
1157
1158 let response = req.deny("Dangerous command blocked", "req-789");
1159 let message: ControlResponseMessage = response.into();
1160
1161 let json = serde_json::to_string(&message).unwrap();
1162 assert!(json.contains("\"behavior\":\"deny\""));
1163 assert!(json.contains("Dangerous command blocked"));
1164 assert!(!json.contains("\"interrupt\":true"));
1165 }
1166
1167 #[test]
1168 fn test_tool_permission_request_deny_and_stop() {
1169 let req = ToolPermissionRequest {
1170 tool_name: "Bash".to_string(),
1171 input: serde_json::json!({"command": "rm -rf /"}),
1172 permission_suggestions: vec![],
1173 blocked_path: None,
1174 decision_reason: None,
1175 tool_use_id: None,
1176 };
1177
1178 let response = req.deny_and_stop("Security violation", "req-000");
1179 let message: ControlResponseMessage = response.into();
1180
1181 let json = serde_json::to_string(&message).unwrap();
1182 assert!(json.contains("\"behavior\":\"deny\""));
1183 assert!(json.contains("\"interrupt\":true"));
1184 }
1185
1186 #[test]
1187 fn test_permission_result_serialization() {
1188 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1190 let json = serde_json::to_string(&allow).unwrap();
1191 assert!(json.contains("\"behavior\":\"allow\""));
1192 assert!(json.contains("\"updatedInput\""));
1193
1194 let deny = PermissionResult::deny("Not allowed");
1196 let json = serde_json::to_string(&deny).unwrap();
1197 assert!(json.contains("\"behavior\":\"deny\""));
1198 assert!(json.contains("\"message\":\"Not allowed\""));
1199 assert!(!json.contains("\"interrupt\""));
1200
1201 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1203 let json = serde_json::to_string(&deny_stop).unwrap();
1204 assert!(json.contains("\"interrupt\":true"));
1205 }
1206
1207 #[test]
1208 fn test_control_request_message_initialize() {
1209 let init = ControlRequestMessage::initialize("init-1");
1210
1211 let json = serde_json::to_string(&init).unwrap();
1212 assert!(json.contains("\"type\":\"control_request\""));
1213 assert!(json.contains("\"request_id\":\"init-1\""));
1214 assert!(json.contains("\"subtype\":\"initialize\""));
1215 }
1216
1217 #[test]
1218 fn test_control_response_error() {
1219 let response = ControlResponse::error("req-err", "Something went wrong");
1220 let message: ControlResponseMessage = response.into();
1221
1222 let json = serde_json::to_string(&message).unwrap();
1223 assert!(json.contains("\"subtype\":\"error\""));
1224 assert!(json.contains("\"error\":\"Something went wrong\""));
1225 }
1226
1227 #[test]
1228 fn test_roundtrip_control_request() {
1229 let original_json = r#"{
1230 "type": "control_request",
1231 "request_id": "test-123",
1232 "request": {
1233 "subtype": "can_use_tool",
1234 "tool_name": "Bash",
1235 "input": {"command": "ls -la"},
1236 "permission_suggestions": []
1237 }
1238 }"#;
1239
1240 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1241
1242 let reserialized = serde_json::to_string(&output).unwrap();
1243 assert!(reserialized.contains("control_request"));
1244 assert!(reserialized.contains("test-123"));
1245 assert!(reserialized.contains("Bash"));
1246 }
1247
1248 #[test]
1249 fn test_permission_suggestions_parsing() {
1250 let json = r#"{
1251 "type": "control_request",
1252 "request_id": "perm-456",
1253 "request": {
1254 "subtype": "can_use_tool",
1255 "tool_name": "Bash",
1256 "input": {"command": "npm test"},
1257 "permission_suggestions": [
1258 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1259 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1260 ]
1261 }
1262 }"#;
1263
1264 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1265 if let ClaudeOutput::ControlRequest(req) = output {
1266 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1267 assert_eq!(perm_req.permission_suggestions.len(), 2);
1268 assert_eq!(
1269 perm_req.permission_suggestions[0].suggestion_type,
1270 PermissionType::SetMode
1271 );
1272 assert_eq!(
1273 perm_req.permission_suggestions[0].mode,
1274 Some(PermissionModeName::AcceptEdits)
1275 );
1276 assert_eq!(
1277 perm_req.permission_suggestions[0].destination,
1278 PermissionDestination::Session
1279 );
1280 assert_eq!(
1281 perm_req.permission_suggestions[1].suggestion_type,
1282 PermissionType::SetMode
1283 );
1284 assert_eq!(
1285 perm_req.permission_suggestions[1].mode,
1286 Some(PermissionModeName::BypassPermissions)
1287 );
1288 assert_eq!(
1289 perm_req.permission_suggestions[1].destination,
1290 PermissionDestination::Project
1291 );
1292 } else {
1293 panic!("Expected CanUseTool payload");
1294 }
1295 } else {
1296 panic!("Expected ControlRequest");
1297 }
1298 }
1299
1300 #[test]
1301 fn test_permission_suggestion_set_mode_roundtrip() {
1302 let suggestion = PermissionSuggestion {
1303 suggestion_type: PermissionType::SetMode,
1304 destination: PermissionDestination::Session,
1305 mode: Some(PermissionModeName::AcceptEdits),
1306 behavior: None,
1307 rules: None,
1308 };
1309
1310 let json = serde_json::to_string(&suggestion).unwrap();
1311 assert!(json.contains("\"type\":\"setMode\""));
1312 assert!(json.contains("\"mode\":\"acceptEdits\""));
1313 assert!(json.contains("\"destination\":\"session\""));
1314 assert!(!json.contains("\"behavior\""));
1315 assert!(!json.contains("\"rules\""));
1316
1317 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1318 assert_eq!(parsed, suggestion);
1319 }
1320
1321 #[test]
1322 fn test_permission_suggestion_add_rules_roundtrip() {
1323 let suggestion = PermissionSuggestion {
1324 suggestion_type: PermissionType::AddRules,
1325 destination: PermissionDestination::Session,
1326 mode: None,
1327 behavior: Some(PermissionBehavior::Allow),
1328 rules: Some(vec![serde_json::json!({
1329 "toolName": "Read",
1330 "ruleContent": "//tmp/**"
1331 })]),
1332 };
1333
1334 let json = serde_json::to_string(&suggestion).unwrap();
1335 assert!(json.contains("\"type\":\"addRules\""));
1336 assert!(json.contains("\"behavior\":\"allow\""));
1337 assert!(json.contains("\"destination\":\"session\""));
1338 assert!(json.contains("\"rules\""));
1339 assert!(json.contains("\"toolName\":\"Read\""));
1340 assert!(!json.contains("\"mode\""));
1341
1342 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1343 assert_eq!(parsed, suggestion);
1344 }
1345
1346 #[test]
1347 fn test_permission_suggestion_add_rules_from_real_json() {
1348 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1349
1350 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1351 assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1352 assert_eq!(parsed.destination, PermissionDestination::Session);
1353 assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1354 assert!(parsed.rules.is_some());
1355 assert!(parsed.mode.is_none());
1356 }
1357
1358 #[test]
1359 fn test_permission_allow_tool() {
1360 let perm = Permission::allow_tool("Bash", "npm test");
1361
1362 assert_eq!(perm.permission_type, PermissionType::AddRules);
1363 assert_eq!(perm.destination, PermissionDestination::Session);
1364 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1365 assert!(perm.mode.is_none());
1366
1367 let rules = perm.rules.unwrap();
1368 assert_eq!(rules.len(), 1);
1369 assert_eq!(rules[0].tool_name, "Bash");
1370 assert_eq!(rules[0].rule_content, "npm test");
1371 }
1372
1373 #[test]
1374 fn test_permission_allow_tool_with_destination() {
1375 let perm = Permission::allow_tool_with_destination(
1376 "Read",
1377 "/tmp/**",
1378 PermissionDestination::Project,
1379 );
1380
1381 assert_eq!(perm.permission_type, PermissionType::AddRules);
1382 assert_eq!(perm.destination, PermissionDestination::Project);
1383 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1384
1385 let rules = perm.rules.unwrap();
1386 assert_eq!(rules[0].tool_name, "Read");
1387 assert_eq!(rules[0].rule_content, "/tmp/**");
1388 }
1389
1390 #[test]
1391 fn test_permission_set_mode() {
1392 let perm = Permission::set_mode(
1393 PermissionModeName::AcceptEdits,
1394 PermissionDestination::Session,
1395 );
1396
1397 assert_eq!(perm.permission_type, PermissionType::SetMode);
1398 assert_eq!(perm.destination, PermissionDestination::Session);
1399 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1400 assert!(perm.behavior.is_none());
1401 assert!(perm.rules.is_none());
1402 }
1403
1404 #[test]
1405 fn test_permission_serialization() {
1406 let perm = Permission::allow_tool("Bash", "npm test");
1407 let json = serde_json::to_string(&perm).unwrap();
1408
1409 assert!(json.contains("\"type\":\"addRules\""));
1410 assert!(json.contains("\"destination\":\"session\""));
1411 assert!(json.contains("\"behavior\":\"allow\""));
1412 assert!(json.contains("\"toolName\":\"Bash\""));
1413 assert!(json.contains("\"ruleContent\":\"npm test\""));
1414 }
1415
1416 #[test]
1417 fn test_permission_from_suggestion_set_mode() {
1418 let suggestion = PermissionSuggestion {
1419 suggestion_type: PermissionType::SetMode,
1420 destination: PermissionDestination::Session,
1421 mode: Some(PermissionModeName::AcceptEdits),
1422 behavior: None,
1423 rules: None,
1424 };
1425
1426 let perm = Permission::from_suggestion(&suggestion);
1427
1428 assert_eq!(perm.permission_type, PermissionType::SetMode);
1429 assert_eq!(perm.destination, PermissionDestination::Session);
1430 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1431 }
1432
1433 #[test]
1434 fn test_permission_from_suggestion_add_rules() {
1435 let suggestion = PermissionSuggestion {
1436 suggestion_type: PermissionType::AddRules,
1437 destination: PermissionDestination::Session,
1438 mode: None,
1439 behavior: Some(PermissionBehavior::Allow),
1440 rules: Some(vec![serde_json::json!({
1441 "toolName": "Read",
1442 "ruleContent": "/tmp/**"
1443 })]),
1444 };
1445
1446 let perm = Permission::from_suggestion(&suggestion);
1447
1448 assert_eq!(perm.permission_type, PermissionType::AddRules);
1449 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1450
1451 let rules = perm.rules.unwrap();
1452 assert_eq!(rules.len(), 1);
1453 assert_eq!(rules[0].tool_name, "Read");
1454 assert_eq!(rules[0].rule_content, "/tmp/**");
1455 }
1456
1457 #[test]
1458 fn test_permission_result_allow_with_typed_permissions() {
1459 let result = PermissionResult::allow_with_typed_permissions(
1460 serde_json::json!({"command": "npm test"}),
1461 vec![Permission::allow_tool("Bash", "npm test")],
1462 );
1463
1464 let json = serde_json::to_string(&result).unwrap();
1465 assert!(json.contains("\"behavior\":\"allow\""));
1466 assert!(json.contains("\"updatedPermissions\""));
1467 assert!(json.contains("\"toolName\":\"Bash\""));
1468 }
1469
1470 #[test]
1471 fn test_tool_permission_request_allow_and_remember() {
1472 let req = ToolPermissionRequest {
1473 tool_name: "Bash".to_string(),
1474 input: serde_json::json!({"command": "npm test"}),
1475 permission_suggestions: vec![],
1476 blocked_path: None,
1477 decision_reason: None,
1478 tool_use_id: None,
1479 };
1480
1481 let response =
1482 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1483 let message: ControlResponseMessage = response.into();
1484 let json = serde_json::to_string(&message).unwrap();
1485
1486 assert!(json.contains("\"type\":\"control_response\""));
1487 assert!(json.contains("\"behavior\":\"allow\""));
1488 assert!(json.contains("\"updatedPermissions\""));
1489 assert!(json.contains("\"toolName\":\"Bash\""));
1490 }
1491
1492 #[test]
1493 fn test_tool_permission_request_allow_and_remember_suggestion() {
1494 let req = ToolPermissionRequest {
1495 tool_name: "Bash".to_string(),
1496 input: serde_json::json!({"command": "npm test"}),
1497 permission_suggestions: vec![PermissionSuggestion {
1498 suggestion_type: PermissionType::SetMode,
1499 destination: PermissionDestination::Session,
1500 mode: Some(PermissionModeName::AcceptEdits),
1501 behavior: None,
1502 rules: None,
1503 }],
1504 blocked_path: None,
1505 decision_reason: None,
1506 tool_use_id: None,
1507 };
1508
1509 let response = req.allow_and_remember_suggestion("req-123");
1510 assert!(response.is_some());
1511
1512 let message: ControlResponseMessage = response.unwrap().into();
1513 let json = serde_json::to_string(&message).unwrap();
1514
1515 assert!(json.contains("\"type\":\"setMode\""));
1516 assert!(json.contains("\"mode\":\"acceptEdits\""));
1517 }
1518
1519 #[test]
1520 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1521 let req = ToolPermissionRequest {
1522 tool_name: "Bash".to_string(),
1523 input: serde_json::json!({"command": "npm test"}),
1524 permission_suggestions: vec![], blocked_path: None,
1526 decision_reason: None,
1527 tool_use_id: None,
1528 };
1529
1530 let response = req.allow_and_remember_suggestion("req-123");
1531 assert!(response.is_none());
1532 }
1533
1534 fn ask_user_question_request() -> ToolPermissionRequest {
1535 ToolPermissionRequest {
1536 tool_name: "AskUserQuestion".to_string(),
1537 input: serde_json::json!({
1538 "questions": [
1539 {
1540 "question": "Which color do you prefer?",
1541 "header": "Color",
1542 "options": [
1543 {"label": "Red", "description": "warm"},
1544 {"label": "Blue", "description": "cool"}
1545 ],
1546 "multiSelect": false
1547 },
1548 {
1549 "question": "Pick a size",
1550 "header": "Size",
1551 "options": [
1552 {"label": "Small"},
1553 {"label": "Large"}
1554 ],
1555 "multiSelect": false
1556 }
1557 ]
1558 }),
1559 permission_suggestions: vec![],
1560 blocked_path: None,
1561 decision_reason: None,
1562 tool_use_id: None,
1563 }
1564 }
1565
1566 fn extract_updated_input(resp: &ControlResponse) -> Value {
1567 let ControlResponsePayload::Success { response, .. } = &resp.response else {
1568 panic!("expected Success payload");
1569 };
1570 let inner = response.as_ref().expect("response body present");
1571 inner
1572 .get("updatedInput")
1573 .cloned()
1574 .expect("updatedInput field present")
1575 }
1576
1577 #[test]
1578 fn answer_questions_keys_by_question_text_and_preserves_questions() {
1579 let req = ask_user_question_request();
1580 let mut answers = HashMap::new();
1581 answers.insert(0, "Blue".to_string());
1582 answers.insert(1, "Large".to_string());
1583
1584 let resp = req.answer_questions(&answers, "rid-1").unwrap();
1585 let updated = extract_updated_input(&resp);
1586
1587 assert_eq!(
1589 updated["questions"], req.input["questions"],
1590 "questions array must round-trip unchanged"
1591 );
1592 assert_eq!(
1594 updated["answers"]["Which color do you prefer?"],
1595 Value::String("Blue".into())
1596 );
1597 assert_eq!(
1598 updated["answers"]["Pick a size"],
1599 Value::String("Large".into())
1600 );
1601 assert!(updated["answers"].get("Color").is_none());
1603 assert!(updated["answers"].get("Size").is_none());
1604 }
1605
1606 #[test]
1607 fn answer_questions_partial_answers_omits_unanswered() {
1608 let req = ask_user_question_request();
1609 let mut answers = HashMap::new();
1610 answers.insert(1, "Small".to_string());
1611
1612 let resp = req.answer_questions(&answers, "rid-2").unwrap();
1613 let updated = extract_updated_input(&resp);
1614
1615 assert_eq!(
1616 updated["answers"]["Pick a size"],
1617 Value::String("Small".into())
1618 );
1619 assert!(updated["answers"]
1620 .get("Which color do you prefer?")
1621 .is_none());
1622 }
1623
1624 #[test]
1625 fn answer_questions_rejects_wrong_tool() {
1626 let mut req = ask_user_question_request();
1627 req.tool_name = "Bash".to_string();
1628
1629 let mut answers = HashMap::new();
1630 answers.insert(0, "Blue".to_string());
1631 match req.answer_questions(&answers, "rid-3").unwrap_err() {
1632 AskUserQuestionResponseError::WrongTool(name) => assert_eq!(name, "Bash"),
1633 other => panic!("expected WrongTool, got {other:?}"),
1634 }
1635 }
1636
1637 #[test]
1638 fn answer_questions_rejects_unparseable_input() {
1639 let req = ToolPermissionRequest {
1640 tool_name: "AskUserQuestion".to_string(),
1641 input: serde_json::json!({"not_questions": "garbage"}),
1642 permission_suggestions: vec![],
1643 blocked_path: None,
1644 decision_reason: None,
1645 tool_use_id: None,
1646 };
1647
1648 let answers = HashMap::new();
1649 match req.answer_questions(&answers, "rid-4").unwrap_err() {
1650 AskUserQuestionResponseError::ParseInput(_) => {}
1651 other => panic!("expected ParseInput, got {other:?}"),
1652 }
1653 }
1654
1655 #[test]
1656 fn answer_questions_rejects_out_of_range_index() {
1657 let req = ask_user_question_request();
1658 let mut answers = HashMap::new();
1659 answers.insert(7, "ghost".to_string());
1660
1661 match req.answer_questions(&answers, "rid-5").unwrap_err() {
1662 AskUserQuestionResponseError::QuestionIndexOutOfRange { index, total } => {
1663 assert_eq!(index, 7);
1664 assert_eq!(total, 2);
1665 }
1666 other => panic!("expected QuestionIndexOutOfRange, got {other:?}"),
1667 }
1668 }
1669}