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)]
241pub struct ControlCancelRequest {
242 pub request_id: String,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub session_id: Option<String>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(tag = "subtype", rename_all = "snake_case")]
252pub enum ControlRequestPayload {
253 CanUseTool(ToolPermissionRequest),
255 HookCallback(HookCallbackRequest),
257 McpMessage(McpMessageRequest),
259 Initialize(InitializeRequest),
261 Interrupt,
263 ApplyFlagSettings(ApplyFlagSettingsRequest),
266}
267
268#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
297pub struct ApplyFlagSettingsRequest {
298 #[serde(default)]
300 pub settings: serde_json::Map<String, Value>,
301}
302
303impl ApplyFlagSettingsRequest {
304 pub fn new() -> Self {
306 Self::default()
307 }
308
309 pub fn effort_level(self, level: crate::EffortLevel) -> Self {
311 self.set("effortLevel", Value::String(level.as_str().to_string()))
312 }
313
314 pub fn clear_effort_level(self) -> Self {
317 self.set("effortLevel", Value::Null)
318 }
319
320 pub fn ultracode(self, on: bool) -> Self {
323 self.set("ultracode", Value::Bool(on))
324 }
325
326 pub fn set(mut self, key: impl Into<String>, value: Value) -> Self {
328 self.settings.insert(key.into(), value);
329 self
330 }
331
332 pub fn get_effort_level(&self) -> Option<crate::EffortLevel> {
334 self.settings
335 .get("effortLevel")?
336 .as_str()
337 .map(crate::EffortLevel::from)
338 }
339
340 pub fn get_ultracode(&self) -> Option<bool> {
342 self.settings.get("ultracode")?.as_bool()
343 }
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
363pub struct Permission {
364 #[serde(rename = "type")]
366 pub permission_type: PermissionType,
367 pub destination: PermissionDestination,
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub mode: Option<PermissionModeName>,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub behavior: Option<PermissionBehavior>,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub rules: Option<Vec<PermissionRule>>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
382pub struct PermissionRule {
383 #[serde(rename = "toolName")]
385 pub tool_name: String,
386 #[serde(rename = "ruleContent")]
388 pub rule_content: String,
389}
390
391impl Permission {
392 pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
405 Permission {
406 permission_type: PermissionType::AddRules,
407 destination: PermissionDestination::Session,
408 mode: None,
409 behavior: Some(PermissionBehavior::Allow),
410 rules: Some(vec![PermissionRule {
411 tool_name: tool_name.into(),
412 rule_content: rule_content.into(),
413 }]),
414 }
415 }
416
417 pub fn allow_tool_with_destination(
427 tool_name: impl Into<String>,
428 rule_content: impl Into<String>,
429 destination: PermissionDestination,
430 ) -> Self {
431 Permission {
432 permission_type: PermissionType::AddRules,
433 destination,
434 mode: None,
435 behavior: Some(PermissionBehavior::Allow),
436 rules: Some(vec![PermissionRule {
437 tool_name: tool_name.into(),
438 rule_content: rule_content.into(),
439 }]),
440 }
441 }
442
443 pub fn set_mode(mode: PermissionModeName, destination: PermissionDestination) -> Self {
453 Permission {
454 permission_type: PermissionType::SetMode,
455 destination,
456 mode: Some(mode),
457 behavior: None,
458 rules: None,
459 }
460 }
461
462 pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
481 Permission {
482 permission_type: suggestion.suggestion_type.clone(),
483 destination: suggestion.destination.clone(),
484 mode: suggestion.mode.clone(),
485 behavior: suggestion.behavior.clone(),
486 rules: suggestion.rules.as_ref().map(|rules| {
487 rules
488 .iter()
489 .filter_map(|v| {
490 Some(PermissionRule {
491 tool_name: v.get("toolName")?.as_str()?.to_string(),
492 rule_content: v.get("ruleContent")?.as_str()?.to_string(),
493 })
494 })
495 .collect()
496 }),
497 }
498 }
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
512pub struct PermissionSuggestion {
513 #[serde(rename = "type")]
515 pub suggestion_type: PermissionType,
516 pub destination: PermissionDestination,
518 #[serde(skip_serializing_if = "Option::is_none")]
520 pub mode: Option<PermissionModeName>,
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub behavior: Option<PermissionBehavior>,
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub rules: Option<Vec<Value>>,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct ToolPermissionRequest {
556 pub tool_name: String,
558 pub input: Value,
560 #[serde(default)]
562 pub permission_suggestions: Vec<PermissionSuggestion>,
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub blocked_path: Option<String>,
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub decision_reason: Option<String>,
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub tool_use_id: Option<String>,
572}
573
574impl ToolPermissionRequest {
575 pub fn allow(&self, request_id: &str) -> ControlResponse {
592 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
593 }
594
595 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
617 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
618 }
619
620 pub fn allow_with_permissions(
624 &self,
625 modified_input: Value,
626 permissions: Vec<Value>,
627 request_id: &str,
628 ) -> ControlResponse {
629 ControlResponse::from_result(
630 request_id,
631 PermissionResult::allow_with_permissions(modified_input, permissions),
632 )
633 }
634
635 pub fn allow_and_remember(
661 &self,
662 permissions: Vec<Permission>,
663 request_id: &str,
664 ) -> ControlResponse {
665 ControlResponse::from_result(
666 request_id,
667 PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
668 )
669 }
670
671 pub fn allow_with_and_remember(
675 &self,
676 modified_input: Value,
677 permissions: Vec<Permission>,
678 request_id: &str,
679 ) -> ControlResponse {
680 ControlResponse::from_result(
681 request_id,
682 PermissionResult::allow_with_typed_permissions(modified_input, permissions),
683 )
684 }
685
686 pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
712 self.permission_suggestions.first().map(|suggestion| {
713 let perm = Permission::from_suggestion(suggestion);
714 self.allow_and_remember(vec![perm], request_id)
715 })
716 }
717
718 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
737 ControlResponse::from_result(request_id, PermissionResult::deny(message))
738 }
739
740 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
744 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
745 }
746
747 pub fn answer_questions(
801 &self,
802 answers_by_index: &HashMap<usize, String>,
803 request_id: &str,
804 ) -> Result<ControlResponse, AskUserQuestionResponseError> {
805 if self.tool_name != "AskUserQuestion" {
806 return Err(AskUserQuestionResponseError::WrongTool(
807 self.tool_name.clone(),
808 ));
809 }
810 let parsed: AskUserQuestionInput = serde_json::from_value(self.input.clone())
811 .map_err(AskUserQuestionResponseError::ParseInput)?;
812 let total = parsed.questions.len();
813
814 let mut answers_map = serde_json::Map::new();
815 for (idx, answer) in answers_by_index {
816 let q = parsed.questions.get(*idx).ok_or(
817 AskUserQuestionResponseError::QuestionIndexOutOfRange { index: *idx, total },
818 )?;
819 answers_map.insert(q.question.clone(), Value::String(answer.clone()));
820 }
821
822 let mut updated_input = self.input.clone();
823 updated_input
825 .as_object_mut()
826 .expect("AskUserQuestion input is a JSON object")
827 .insert("answers".to_string(), Value::Object(answers_map));
828
829 Ok(ControlResponse::from_result(
830 request_id,
831 PermissionResult::allow(updated_input),
832 ))
833 }
834}
835
836#[derive(Debug, thiserror::Error)]
839pub enum AskUserQuestionResponseError {
840 #[error("expected tool_name=AskUserQuestion, got `{0}`")]
842 WrongTool(String),
843 #[error("failed to parse AskUserQuestion input: {0}")]
845 ParseInput(#[source] serde_json::Error),
846 #[error("answer references question index {index}, but only {total} question(s) were asked")]
848 QuestionIndexOutOfRange {
849 index: usize,
851 total: usize,
853 },
854}
855
856#[derive(Debug, Clone, Serialize, Deserialize)]
861#[serde(tag = "behavior", rename_all = "snake_case")]
862pub enum PermissionResult {
863 Allow {
865 #[serde(rename = "updatedInput")]
867 updated_input: Value,
868 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
870 updated_permissions: Option<Vec<Value>>,
871 },
872 Deny {
874 message: String,
876 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
878 interrupt: bool,
879 },
880}
881
882impl PermissionResult {
883 pub fn allow(input: Value) -> Self {
885 PermissionResult::Allow {
886 updated_input: input,
887 updated_permissions: None,
888 }
889 }
890
891 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
895 PermissionResult::Allow {
896 updated_input: input,
897 updated_permissions: Some(permissions),
898 }
899 }
900
901 pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
917 let permission_values: Vec<Value> = permissions
918 .into_iter()
919 .filter_map(|p| serde_json::to_value(p).ok())
920 .collect();
921 PermissionResult::Allow {
922 updated_input: input,
923 updated_permissions: Some(permission_values),
924 }
925 }
926
927 pub fn deny(message: impl Into<String>) -> Self {
929 PermissionResult::Deny {
930 message: message.into(),
931 interrupt: false,
932 }
933 }
934
935 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
937 PermissionResult::Deny {
938 message: message.into(),
939 interrupt: true,
940 }
941 }
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize)]
946pub struct HookCallbackRequest {
947 pub callback_id: String,
948 pub input: Value,
949 #[serde(skip_serializing_if = "Option::is_none")]
950 pub tool_use_id: Option<String>,
951}
952
953#[derive(Debug, Clone, Serialize, Deserialize)]
955pub struct McpMessageRequest {
956 pub server_name: String,
957 pub message: Value,
958}
959
960#[derive(Debug, Clone, Serialize, Deserialize)]
962pub struct InitializeRequest {
963 #[serde(skip_serializing_if = "Option::is_none")]
964 pub hooks: Option<Value>,
965}
966
967#[derive(Debug, Clone, Serialize, Deserialize)]
972pub struct ControlResponse {
973 pub response: ControlResponsePayload,
975}
976
977impl ControlResponse {
978 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
982 let response_value = serde_json::to_value(&result)
984 .expect("PermissionResult serialization should never fail");
985 ControlResponse {
986 response: ControlResponsePayload::Success {
987 request_id: request_id.to_string(),
988 response: Some(response_value),
989 },
990 }
991 }
992
993 pub fn success(request_id: &str, response_data: Value) -> Self {
995 ControlResponse {
996 response: ControlResponsePayload::Success {
997 request_id: request_id.to_string(),
998 response: Some(response_data),
999 },
1000 }
1001 }
1002
1003 pub fn success_empty(request_id: &str) -> Self {
1005 ControlResponse {
1006 response: ControlResponsePayload::Success {
1007 request_id: request_id.to_string(),
1008 response: None,
1009 },
1010 }
1011 }
1012
1013 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
1015 ControlResponse {
1016 response: ControlResponsePayload::Error {
1017 request_id: request_id.to_string(),
1018 error: error_message.into(),
1019 },
1020 }
1021 }
1022
1023 pub fn get_usage_response(&self) -> Option<Result<GetUsageResponse, serde_json::Error>> {
1025 match &self.response {
1026 ControlResponsePayload::Success {
1027 response: Some(value),
1028 ..
1029 } => Some(serde_json::from_value(value.clone())),
1030 _ => None,
1031 }
1032 }
1033}
1034
1035#[derive(Debug, Clone, Serialize, Deserialize)]
1037#[serde(tag = "subtype", rename_all = "snake_case")]
1038pub enum ControlResponsePayload {
1039 Success {
1040 request_id: String,
1041 #[serde(skip_serializing_if = "Option::is_none")]
1042 response: Option<Value>,
1043 },
1044 Error {
1045 request_id: String,
1046 error: String,
1047 },
1048}
1049
1050#[derive(Debug, Clone, Serialize, Deserialize)]
1052pub struct GetUsageResponse {
1053 pub session: UsageSession,
1054 pub subscription_type: Option<String>,
1055 pub rate_limits_available: bool,
1056 pub rate_limits: Option<UsageRateLimits>,
1057 pub behaviors: UsageBehaviors,
1058 #[serde(flatten)]
1059 pub extra: serde_json::Map<String, Value>,
1060}
1061
1062#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1063pub struct UsageSession {
1064 pub total_cost_usd: f64,
1065 pub total_api_duration_ms: u64,
1066 pub total_duration_ms: u64,
1067 pub total_lines_added: u64,
1068 pub total_lines_removed: u64,
1069 pub model_usage: HashMap<String, UsageModelUsage>,
1070 #[serde(flatten)]
1071 pub extra: serde_json::Map<String, Value>,
1072}
1073
1074#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1075#[serde(rename_all = "camelCase")]
1076pub struct UsageModelUsage {
1077 #[serde(default)]
1078 pub input_tokens: u64,
1079 #[serde(default)]
1080 pub output_tokens: u64,
1081 #[serde(default)]
1082 pub cache_read_input_tokens: u64,
1083 #[serde(default)]
1084 pub cache_creation_input_tokens: u64,
1085 #[serde(default)]
1086 pub web_search_requests: u64,
1087 #[serde(default, rename = "costUSD")]
1088 pub cost_usd: f64,
1089 #[serde(default)]
1090 pub context_window: u64,
1091 #[serde(default)]
1092 pub max_output_tokens: u64,
1093 #[serde(flatten)]
1094 pub extra: serde_json::Map<String, Value>,
1095}
1096
1097#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1098pub struct UsageRateLimits {
1099 #[serde(default, skip_serializing_if = "Option::is_none")]
1100 pub five_hour: Option<UsageRateLimitWindow>,
1101 #[serde(default, skip_serializing_if = "Option::is_none")]
1102 pub seven_day: Option<UsageRateLimitWindow>,
1103 #[serde(default, skip_serializing_if = "Option::is_none")]
1104 pub seven_day_oauth_apps: Option<UsageRateLimitWindow>,
1105 #[serde(default, skip_serializing_if = "Option::is_none")]
1106 pub seven_day_opus: Option<UsageRateLimitWindow>,
1107 #[serde(default, skip_serializing_if = "Option::is_none")]
1108 pub seven_day_sonnet: Option<UsageRateLimitWindow>,
1109 #[serde(default, skip_serializing_if = "Option::is_none")]
1110 pub model_scoped: Option<Vec<ModelScopedRateLimit>>,
1111 #[serde(flatten)]
1112 pub extra: serde_json::Map<String, Value>,
1113}
1114
1115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1116pub struct UsageRateLimitWindow {
1117 pub utilization: Option<f64>,
1118 pub resets_at: Option<String>,
1119 #[serde(flatten)]
1120 pub extra: serde_json::Map<String, Value>,
1121}
1122
1123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1124pub struct ModelScopedRateLimit {
1125 pub display_name: String,
1126 pub utilization: Option<f64>,
1127 pub resets_at: Option<String>,
1128 #[serde(flatten)]
1129 pub extra: serde_json::Map<String, Value>,
1130}
1131
1132#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1133pub struct UsageBehaviors {
1134 pub request_count: u64,
1135 pub session_count: u64,
1136 pub behaviors: Vec<UsageBehavior>,
1137 pub agents: Value,
1138 pub skills: Value,
1139 pub plugins: Value,
1140 pub mcp_servers: Value,
1141 #[serde(flatten)]
1142 pub extra: serde_json::Map<String, Value>,
1143}
1144
1145#[derive(Debug, Clone, Serialize, Deserialize)]
1146pub struct UsageBehavior {
1147 pub key: String,
1148 pub pct: f64,
1149 pub count: u64,
1150 #[serde(flatten)]
1151 pub extra: serde_json::Map<String, Value>,
1152}
1153
1154#[derive(Debug, Clone, Serialize, Deserialize)]
1156pub struct ControlResponseMessage {
1157 #[serde(rename = "type")]
1158 pub message_type: String,
1159 pub response: ControlResponsePayload,
1160}
1161
1162impl From<ControlResponse> for ControlResponseMessage {
1163 fn from(resp: ControlResponse) -> Self {
1164 ControlResponseMessage {
1165 message_type: "control_response".to_string(),
1166 response: resp.response,
1167 }
1168 }
1169}
1170
1171#[derive(Debug, Clone, Serialize, Deserialize)]
1173pub struct ControlRequestMessage {
1174 #[serde(rename = "type")]
1175 pub message_type: String,
1176 pub request_id: String,
1177 pub request: ControlRequestPayload,
1178}
1179
1180impl ControlRequestMessage {
1181 pub fn initialize(request_id: impl Into<String>) -> Self {
1183 ControlRequestMessage {
1184 message_type: "control_request".to_string(),
1185 request_id: request_id.into(),
1186 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
1187 }
1188 }
1189
1190 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
1192 ControlRequestMessage {
1193 message_type: "control_request".to_string(),
1194 request_id: request_id.into(),
1195 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
1196 }
1197 }
1198
1199 pub fn interrupt(request_id: impl Into<String>) -> Self {
1206 ControlRequestMessage {
1207 message_type: "control_request".to_string(),
1208 request_id: request_id.into(),
1209 request: ControlRequestPayload::Interrupt,
1210 }
1211 }
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216 use super::*;
1217 use crate::io::ClaudeOutput;
1218
1219 #[test]
1220 fn test_deserialize_control_request_can_use_tool() {
1221 let json = r#"{
1222 "type": "control_request",
1223 "request_id": "perm-abc123",
1224 "request": {
1225 "subtype": "can_use_tool",
1226 "tool_name": "Write",
1227 "input": {
1228 "file_path": "/home/user/hello.py",
1229 "content": "print('hello')"
1230 },
1231 "permission_suggestions": [],
1232 "blocked_path": null
1233 }
1234 }"#;
1235
1236 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1237 assert!(output.is_control_request());
1238
1239 if let ClaudeOutput::ControlRequest(req) = output {
1240 assert_eq!(req.request_id, "perm-abc123");
1241 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1242 assert_eq!(perm_req.tool_name, "Write");
1243 assert_eq!(
1244 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1245 "/home/user/hello.py"
1246 );
1247 } else {
1248 panic!("Expected CanUseTool payload");
1249 }
1250 } else {
1251 panic!("Expected ControlRequest");
1252 }
1253 }
1254
1255 #[test]
1256 fn test_deserialize_control_request_edit_tool_real() {
1257 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"}}"#;
1259
1260 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1261 assert!(output.is_control_request());
1262 assert_eq!(output.message_type(), "control_request");
1263
1264 if let ClaudeOutput::ControlRequest(req) = output {
1265 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1266 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1267 assert_eq!(perm_req.tool_name, "Edit");
1268 assert_eq!(
1269 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1270 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1271 );
1272 assert!(perm_req.input.get("old_string").is_some());
1273 assert!(perm_req.input.get("new_string").is_some());
1274 assert!(!perm_req
1275 .input
1276 .get("replace_all")
1277 .unwrap()
1278 .as_bool()
1279 .unwrap());
1280 } else {
1281 panic!("Expected CanUseTool payload");
1282 }
1283 } else {
1284 panic!("Expected ControlRequest");
1285 }
1286 }
1287
1288 #[test]
1289 fn test_tool_permission_request_allow() {
1290 let req = ToolPermissionRequest {
1291 tool_name: "Read".to_string(),
1292 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1293 permission_suggestions: vec![],
1294 blocked_path: None,
1295 decision_reason: None,
1296 tool_use_id: None,
1297 };
1298
1299 let response = req.allow("req-123");
1300 let message: ControlResponseMessage = response.into();
1301
1302 let json = serde_json::to_string(&message).unwrap();
1303 assert!(json.contains("\"type\":\"control_response\""));
1304 assert!(json.contains("\"subtype\":\"success\""));
1305 assert!(json.contains("\"request_id\":\"req-123\""));
1306 assert!(json.contains("\"behavior\":\"allow\""));
1307 assert!(json.contains("\"updatedInput\""));
1308 }
1309
1310 #[test]
1311 fn test_tool_permission_request_allow_with_modified_input() {
1312 let req = ToolPermissionRequest {
1313 tool_name: "Write".to_string(),
1314 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1315 permission_suggestions: vec![],
1316 blocked_path: None,
1317 decision_reason: None,
1318 tool_use_id: None,
1319 };
1320
1321 let modified_input = serde_json::json!({
1322 "file_path": "/tmp/safe/passwd",
1323 "content": "test"
1324 });
1325 let response = req.allow_with(modified_input, "req-456");
1326 let message: ControlResponseMessage = response.into();
1327
1328 let json = serde_json::to_string(&message).unwrap();
1329 assert!(json.contains("/tmp/safe/passwd"));
1330 assert!(!json.contains("/etc/passwd"));
1331 }
1332
1333 #[test]
1334 fn test_tool_permission_request_deny() {
1335 let req = ToolPermissionRequest {
1336 tool_name: "Bash".to_string(),
1337 input: serde_json::json!({"command": "sudo rm -rf /"}),
1338 permission_suggestions: vec![],
1339 blocked_path: None,
1340 decision_reason: None,
1341 tool_use_id: None,
1342 };
1343
1344 let response = req.deny("Dangerous command blocked", "req-789");
1345 let message: ControlResponseMessage = response.into();
1346
1347 let json = serde_json::to_string(&message).unwrap();
1348 assert!(json.contains("\"behavior\":\"deny\""));
1349 assert!(json.contains("Dangerous command blocked"));
1350 assert!(!json.contains("\"interrupt\":true"));
1351 }
1352
1353 #[test]
1354 fn test_tool_permission_request_deny_and_stop() {
1355 let req = ToolPermissionRequest {
1356 tool_name: "Bash".to_string(),
1357 input: serde_json::json!({"command": "rm -rf /"}),
1358 permission_suggestions: vec![],
1359 blocked_path: None,
1360 decision_reason: None,
1361 tool_use_id: None,
1362 };
1363
1364 let response = req.deny_and_stop("Security violation", "req-000");
1365 let message: ControlResponseMessage = response.into();
1366
1367 let json = serde_json::to_string(&message).unwrap();
1368 assert!(json.contains("\"behavior\":\"deny\""));
1369 assert!(json.contains("\"interrupt\":true"));
1370 }
1371
1372 #[test]
1373 fn test_permission_result_serialization() {
1374 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1376 let json = serde_json::to_string(&allow).unwrap();
1377 assert!(json.contains("\"behavior\":\"allow\""));
1378 assert!(json.contains("\"updatedInput\""));
1379
1380 let deny = PermissionResult::deny("Not allowed");
1382 let json = serde_json::to_string(&deny).unwrap();
1383 assert!(json.contains("\"behavior\":\"deny\""));
1384 assert!(json.contains("\"message\":\"Not allowed\""));
1385 assert!(!json.contains("\"interrupt\""));
1386
1387 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1389 let json = serde_json::to_string(&deny_stop).unwrap();
1390 assert!(json.contains("\"interrupt\":true"));
1391 }
1392
1393 #[test]
1394 fn test_control_request_message_initialize() {
1395 let init = ControlRequestMessage::initialize("init-1");
1396
1397 let json = serde_json::to_string(&init).unwrap();
1398 assert!(json.contains("\"type\":\"control_request\""));
1399 assert!(json.contains("\"request_id\":\"init-1\""));
1400 assert!(json.contains("\"subtype\":\"initialize\""));
1401 }
1402
1403 #[test]
1404 fn test_control_request_message_interrupt_wire_shape() {
1405 let msg = ControlRequestMessage::interrupt("interrupt-1");
1408 assert_eq!(
1409 serde_json::to_value(&msg).unwrap(),
1410 serde_json::json!({
1411 "type": "control_request",
1412 "request_id": "interrupt-1",
1413 "request": {"subtype": "interrupt"}
1414 })
1415 );
1416 }
1417
1418 #[test]
1419 fn test_interrupt_payload_roundtrip() {
1420 let json = r#"{"subtype":"interrupt"}"#;
1421 let payload: ControlRequestPayload = serde_json::from_str(json).unwrap();
1422 assert!(matches!(payload, ControlRequestPayload::Interrupt));
1423 assert_eq!(serde_json::to_string(&payload).unwrap(), json);
1424 }
1425
1426 #[test]
1427 fn test_apply_flag_settings_wire_shape() {
1428 let input = crate::io::ClaudeInput::apply_flag_settings(
1429 "apply-flag-settings-1",
1430 ApplyFlagSettingsRequest::new()
1431 .effort_level(crate::EffortLevel::Max)
1432 .ultracode(true),
1433 );
1434 assert_eq!(
1435 serde_json::to_value(&input).unwrap(),
1436 serde_json::json!({
1437 "type": "control_request",
1438 "request_id": "apply-flag-settings-1",
1439 "request": {
1440 "subtype": "apply_flag_settings",
1441 "settings": {"effortLevel": "max", "ultracode": true}
1442 }
1443 })
1444 );
1445 }
1446
1447 #[test]
1448 fn test_apply_flag_settings_clear_and_unknown_keys_roundtrip() {
1449 let json = r#"{"subtype":"apply_flag_settings","settings":{"effortLevel":null,"fastMode":true,"someFutureKey":{"a":1}}}"#;
1452 let payload: ControlRequestPayload = serde_json::from_str(json).unwrap();
1453 let ControlRequestPayload::ApplyFlagSettings(req) = &payload else {
1454 panic!("expected ApplyFlagSettings, got {payload:?}");
1455 };
1456 assert_eq!(req.get_effort_level(), None);
1457 assert_eq!(req.settings.get("effortLevel"), Some(&Value::Null));
1458 assert_eq!(req.settings["someFutureKey"], serde_json::json!({"a": 1}));
1459 assert_eq!(
1460 serde_json::to_value(&payload).unwrap(),
1461 serde_json::from_str::<Value>(json).unwrap()
1462 );
1463
1464 let cleared = ApplyFlagSettingsRequest::new().clear_effort_level();
1465 assert_eq!(
1466 serde_json::to_value(&cleared).unwrap(),
1467 serde_json::json!({"settings": {"effortLevel": null}})
1468 );
1469
1470 let future = ApplyFlagSettingsRequest::new().set("effortLevel", Value::from("ultra"));
1471 assert_eq!(
1472 future.get_effort_level(),
1473 Some(crate::EffortLevel::Unknown("ultra".to_string()))
1474 );
1475 assert_eq!(future.get_ultracode(), None);
1476 }
1477
1478 #[test]
1479 fn test_control_response_error() {
1480 let response = ControlResponse::error("req-err", "Something went wrong");
1481 let message: ControlResponseMessage = response.into();
1482
1483 let json = serde_json::to_string(&message).unwrap();
1484 assert!(json.contains("\"subtype\":\"error\""));
1485 assert!(json.contains("\"error\":\"Something went wrong\""));
1486 }
1487
1488 #[test]
1489 fn test_get_usage_response_parses_model_scoped_limits() {
1490 let response = ControlResponse::success(
1491 "usage-1",
1492 serde_json::json!({
1493 "session": {
1494 "total_cost_usd": 0.5,
1495 "total_api_duration_ms": 100,
1496 "total_duration_ms": 200,
1497 "total_lines_added": 3,
1498 "total_lines_removed": 1,
1499 "model_usage": {
1500 "claude-sonnet-4-6": {
1501 "inputTokens": 10,
1502 "outputTokens": 2,
1503 "cacheReadInputTokens": 1,
1504 "cacheCreationInputTokens": 0,
1505 "webSearchRequests": 0,
1506 "costUSD": 0.01,
1507 "contextWindow": 200000,
1508 "maxOutputTokens": 64000
1509 }
1510 }
1511 },
1512 "subscription_type": "pro",
1513 "rate_limits_available": true,
1514 "rate_limits": {
1515 "five_hour": {"utilization": 0.2, "resets_at": "2026-07-09T22:00:00Z"},
1516 "model_scoped": [
1517 {"display_name": "Sonnet", "utilization": 0.4, "resets_at": "2026-07-16T00:00:00Z"}
1518 ]
1519 },
1520 "behaviors": {
1521 "request_count": 1,
1522 "session_count": 1,
1523 "behaviors": [{"key": "cron", "pct": 100.0, "count": 1}],
1524 "agents": [],
1525 "skills": [],
1526 "plugins": [],
1527 "mcp_servers": []
1528 }
1529 }),
1530 );
1531
1532 let usage = response.get_usage_response().unwrap().unwrap();
1533 assert_eq!(usage.subscription_type.as_deref(), Some("pro"));
1534 let model = usage.session.model_usage.get("claude-sonnet-4-6").unwrap();
1535 assert_eq!(model.context_window, 200000);
1536 assert_eq!(model.max_output_tokens, 64000);
1537 let limits = usage.rate_limits.unwrap();
1538 assert_eq!(limits.five_hour.unwrap().utilization, Some(0.2));
1539 assert_eq!(limits.model_scoped.unwrap()[0].display_name, "Sonnet");
1540 }
1541
1542 #[test]
1543 fn test_roundtrip_control_request() {
1544 let original_json = r#"{
1545 "type": "control_request",
1546 "request_id": "test-123",
1547 "request": {
1548 "subtype": "can_use_tool",
1549 "tool_name": "Bash",
1550 "input": {"command": "ls -la"},
1551 "permission_suggestions": []
1552 }
1553 }"#;
1554
1555 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1556
1557 let reserialized = serde_json::to_string(&output).unwrap();
1558 assert!(reserialized.contains("control_request"));
1559 assert!(reserialized.contains("test-123"));
1560 assert!(reserialized.contains("Bash"));
1561 }
1562
1563 #[test]
1564 fn test_permission_suggestions_parsing() {
1565 let json = r#"{
1566 "type": "control_request",
1567 "request_id": "perm-456",
1568 "request": {
1569 "subtype": "can_use_tool",
1570 "tool_name": "Bash",
1571 "input": {"command": "npm test"},
1572 "permission_suggestions": [
1573 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1574 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1575 ]
1576 }
1577 }"#;
1578
1579 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1580 if let ClaudeOutput::ControlRequest(req) = output {
1581 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1582 assert_eq!(perm_req.permission_suggestions.len(), 2);
1583 assert_eq!(
1584 perm_req.permission_suggestions[0].suggestion_type,
1585 PermissionType::SetMode
1586 );
1587 assert_eq!(
1588 perm_req.permission_suggestions[0].mode,
1589 Some(PermissionModeName::AcceptEdits)
1590 );
1591 assert_eq!(
1592 perm_req.permission_suggestions[0].destination,
1593 PermissionDestination::Session
1594 );
1595 assert_eq!(
1596 perm_req.permission_suggestions[1].suggestion_type,
1597 PermissionType::SetMode
1598 );
1599 assert_eq!(
1600 perm_req.permission_suggestions[1].mode,
1601 Some(PermissionModeName::BypassPermissions)
1602 );
1603 assert_eq!(
1604 perm_req.permission_suggestions[1].destination,
1605 PermissionDestination::Project
1606 );
1607 } else {
1608 panic!("Expected CanUseTool payload");
1609 }
1610 } else {
1611 panic!("Expected ControlRequest");
1612 }
1613 }
1614
1615 #[test]
1616 fn test_permission_suggestion_set_mode_roundtrip() {
1617 let suggestion = PermissionSuggestion {
1618 suggestion_type: PermissionType::SetMode,
1619 destination: PermissionDestination::Session,
1620 mode: Some(PermissionModeName::AcceptEdits),
1621 behavior: None,
1622 rules: None,
1623 };
1624
1625 let json = serde_json::to_string(&suggestion).unwrap();
1626 assert!(json.contains("\"type\":\"setMode\""));
1627 assert!(json.contains("\"mode\":\"acceptEdits\""));
1628 assert!(json.contains("\"destination\":\"session\""));
1629 assert!(!json.contains("\"behavior\""));
1630 assert!(!json.contains("\"rules\""));
1631
1632 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1633 assert_eq!(parsed, suggestion);
1634 }
1635
1636 #[test]
1637 fn test_permission_suggestion_add_rules_roundtrip() {
1638 let suggestion = PermissionSuggestion {
1639 suggestion_type: PermissionType::AddRules,
1640 destination: PermissionDestination::Session,
1641 mode: None,
1642 behavior: Some(PermissionBehavior::Allow),
1643 rules: Some(vec![serde_json::json!({
1644 "toolName": "Read",
1645 "ruleContent": "//tmp/**"
1646 })]),
1647 };
1648
1649 let json = serde_json::to_string(&suggestion).unwrap();
1650 assert!(json.contains("\"type\":\"addRules\""));
1651 assert!(json.contains("\"behavior\":\"allow\""));
1652 assert!(json.contains("\"destination\":\"session\""));
1653 assert!(json.contains("\"rules\""));
1654 assert!(json.contains("\"toolName\":\"Read\""));
1655 assert!(!json.contains("\"mode\""));
1656
1657 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1658 assert_eq!(parsed, suggestion);
1659 }
1660
1661 #[test]
1662 fn test_permission_suggestion_add_rules_from_real_json() {
1663 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1664
1665 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1666 assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1667 assert_eq!(parsed.destination, PermissionDestination::Session);
1668 assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1669 assert!(parsed.rules.is_some());
1670 assert!(parsed.mode.is_none());
1671 }
1672
1673 #[test]
1674 fn test_permission_allow_tool() {
1675 let perm = Permission::allow_tool("Bash", "npm test");
1676
1677 assert_eq!(perm.permission_type, PermissionType::AddRules);
1678 assert_eq!(perm.destination, PermissionDestination::Session);
1679 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1680 assert!(perm.mode.is_none());
1681
1682 let rules = perm.rules.unwrap();
1683 assert_eq!(rules.len(), 1);
1684 assert_eq!(rules[0].tool_name, "Bash");
1685 assert_eq!(rules[0].rule_content, "npm test");
1686 }
1687
1688 #[test]
1689 fn test_permission_allow_tool_with_destination() {
1690 let perm = Permission::allow_tool_with_destination(
1691 "Read",
1692 "/tmp/**",
1693 PermissionDestination::Project,
1694 );
1695
1696 assert_eq!(perm.permission_type, PermissionType::AddRules);
1697 assert_eq!(perm.destination, PermissionDestination::Project);
1698 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1699
1700 let rules = perm.rules.unwrap();
1701 assert_eq!(rules[0].tool_name, "Read");
1702 assert_eq!(rules[0].rule_content, "/tmp/**");
1703 }
1704
1705 #[test]
1706 fn test_permission_set_mode() {
1707 let perm = Permission::set_mode(
1708 PermissionModeName::AcceptEdits,
1709 PermissionDestination::Session,
1710 );
1711
1712 assert_eq!(perm.permission_type, PermissionType::SetMode);
1713 assert_eq!(perm.destination, PermissionDestination::Session);
1714 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1715 assert!(perm.behavior.is_none());
1716 assert!(perm.rules.is_none());
1717 }
1718
1719 #[test]
1720 fn test_permission_serialization() {
1721 let perm = Permission::allow_tool("Bash", "npm test");
1722 let json = serde_json::to_string(&perm).unwrap();
1723
1724 assert!(json.contains("\"type\":\"addRules\""));
1725 assert!(json.contains("\"destination\":\"session\""));
1726 assert!(json.contains("\"behavior\":\"allow\""));
1727 assert!(json.contains("\"toolName\":\"Bash\""));
1728 assert!(json.contains("\"ruleContent\":\"npm test\""));
1729 }
1730
1731 #[test]
1732 fn test_permission_from_suggestion_set_mode() {
1733 let suggestion = PermissionSuggestion {
1734 suggestion_type: PermissionType::SetMode,
1735 destination: PermissionDestination::Session,
1736 mode: Some(PermissionModeName::AcceptEdits),
1737 behavior: None,
1738 rules: None,
1739 };
1740
1741 let perm = Permission::from_suggestion(&suggestion);
1742
1743 assert_eq!(perm.permission_type, PermissionType::SetMode);
1744 assert_eq!(perm.destination, PermissionDestination::Session);
1745 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1746 }
1747
1748 #[test]
1749 fn test_permission_from_suggestion_add_rules() {
1750 let suggestion = PermissionSuggestion {
1751 suggestion_type: PermissionType::AddRules,
1752 destination: PermissionDestination::Session,
1753 mode: None,
1754 behavior: Some(PermissionBehavior::Allow),
1755 rules: Some(vec![serde_json::json!({
1756 "toolName": "Read",
1757 "ruleContent": "/tmp/**"
1758 })]),
1759 };
1760
1761 let perm = Permission::from_suggestion(&suggestion);
1762
1763 assert_eq!(perm.permission_type, PermissionType::AddRules);
1764 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1765
1766 let rules = perm.rules.unwrap();
1767 assert_eq!(rules.len(), 1);
1768 assert_eq!(rules[0].tool_name, "Read");
1769 assert_eq!(rules[0].rule_content, "/tmp/**");
1770 }
1771
1772 #[test]
1773 fn test_permission_result_allow_with_typed_permissions() {
1774 let result = PermissionResult::allow_with_typed_permissions(
1775 serde_json::json!({"command": "npm test"}),
1776 vec![Permission::allow_tool("Bash", "npm test")],
1777 );
1778
1779 let json = serde_json::to_string(&result).unwrap();
1780 assert!(json.contains("\"behavior\":\"allow\""));
1781 assert!(json.contains("\"updatedPermissions\""));
1782 assert!(json.contains("\"toolName\":\"Bash\""));
1783 }
1784
1785 #[test]
1786 fn test_tool_permission_request_allow_and_remember() {
1787 let req = ToolPermissionRequest {
1788 tool_name: "Bash".to_string(),
1789 input: serde_json::json!({"command": "npm test"}),
1790 permission_suggestions: vec![],
1791 blocked_path: None,
1792 decision_reason: None,
1793 tool_use_id: None,
1794 };
1795
1796 let response =
1797 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1798 let message: ControlResponseMessage = response.into();
1799 let json = serde_json::to_string(&message).unwrap();
1800
1801 assert!(json.contains("\"type\":\"control_response\""));
1802 assert!(json.contains("\"behavior\":\"allow\""));
1803 assert!(json.contains("\"updatedPermissions\""));
1804 assert!(json.contains("\"toolName\":\"Bash\""));
1805 }
1806
1807 #[test]
1808 fn test_tool_permission_request_allow_and_remember_suggestion() {
1809 let req = ToolPermissionRequest {
1810 tool_name: "Bash".to_string(),
1811 input: serde_json::json!({"command": "npm test"}),
1812 permission_suggestions: vec![PermissionSuggestion {
1813 suggestion_type: PermissionType::SetMode,
1814 destination: PermissionDestination::Session,
1815 mode: Some(PermissionModeName::AcceptEdits),
1816 behavior: None,
1817 rules: None,
1818 }],
1819 blocked_path: None,
1820 decision_reason: None,
1821 tool_use_id: None,
1822 };
1823
1824 let response = req.allow_and_remember_suggestion("req-123");
1825 assert!(response.is_some());
1826
1827 let message: ControlResponseMessage = response.unwrap().into();
1828 let json = serde_json::to_string(&message).unwrap();
1829
1830 assert!(json.contains("\"type\":\"setMode\""));
1831 assert!(json.contains("\"mode\":\"acceptEdits\""));
1832 }
1833
1834 #[test]
1835 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1836 let req = ToolPermissionRequest {
1837 tool_name: "Bash".to_string(),
1838 input: serde_json::json!({"command": "npm test"}),
1839 permission_suggestions: vec![], blocked_path: None,
1841 decision_reason: None,
1842 tool_use_id: None,
1843 };
1844
1845 let response = req.allow_and_remember_suggestion("req-123");
1846 assert!(response.is_none());
1847 }
1848
1849 fn ask_user_question_request() -> ToolPermissionRequest {
1850 ToolPermissionRequest {
1851 tool_name: "AskUserQuestion".to_string(),
1852 input: serde_json::json!({
1853 "questions": [
1854 {
1855 "question": "Which color do you prefer?",
1856 "header": "Color",
1857 "options": [
1858 {"label": "Red", "description": "warm"},
1859 {"label": "Blue", "description": "cool"}
1860 ],
1861 "multiSelect": false
1862 },
1863 {
1864 "question": "Pick a size",
1865 "header": "Size",
1866 "options": [
1867 {"label": "Small"},
1868 {"label": "Large"}
1869 ],
1870 "multiSelect": false
1871 }
1872 ]
1873 }),
1874 permission_suggestions: vec![],
1875 blocked_path: None,
1876 decision_reason: None,
1877 tool_use_id: None,
1878 }
1879 }
1880
1881 fn extract_updated_input(resp: &ControlResponse) -> Value {
1882 let ControlResponsePayload::Success { response, .. } = &resp.response else {
1883 panic!("expected Success payload");
1884 };
1885 let inner = response.as_ref().expect("response body present");
1886 inner
1887 .get("updatedInput")
1888 .cloned()
1889 .expect("updatedInput field present")
1890 }
1891
1892 #[test]
1893 fn answer_questions_keys_by_question_text_and_preserves_questions() {
1894 let req = ask_user_question_request();
1895 let mut answers = HashMap::new();
1896 answers.insert(0, "Blue".to_string());
1897 answers.insert(1, "Large".to_string());
1898
1899 let resp = req.answer_questions(&answers, "rid-1").unwrap();
1900 let updated = extract_updated_input(&resp);
1901
1902 assert_eq!(
1904 updated["questions"], req.input["questions"],
1905 "questions array must round-trip unchanged"
1906 );
1907 assert_eq!(
1909 updated["answers"]["Which color do you prefer?"],
1910 Value::String("Blue".into())
1911 );
1912 assert_eq!(
1913 updated["answers"]["Pick a size"],
1914 Value::String("Large".into())
1915 );
1916 assert!(updated["answers"].get("Color").is_none());
1918 assert!(updated["answers"].get("Size").is_none());
1919 }
1920
1921 #[test]
1922 fn answer_questions_partial_answers_omits_unanswered() {
1923 let req = ask_user_question_request();
1924 let mut answers = HashMap::new();
1925 answers.insert(1, "Small".to_string());
1926
1927 let resp = req.answer_questions(&answers, "rid-2").unwrap();
1928 let updated = extract_updated_input(&resp);
1929
1930 assert_eq!(
1931 updated["answers"]["Pick a size"],
1932 Value::String("Small".into())
1933 );
1934 assert!(updated["answers"]
1935 .get("Which color do you prefer?")
1936 .is_none());
1937 }
1938
1939 #[test]
1940 fn answer_questions_rejects_wrong_tool() {
1941 let mut req = ask_user_question_request();
1942 req.tool_name = "Bash".to_string();
1943
1944 let mut answers = HashMap::new();
1945 answers.insert(0, "Blue".to_string());
1946 match req.answer_questions(&answers, "rid-3").unwrap_err() {
1947 AskUserQuestionResponseError::WrongTool(name) => assert_eq!(name, "Bash"),
1948 other => panic!("expected WrongTool, got {other:?}"),
1949 }
1950 }
1951
1952 #[test]
1953 fn answer_questions_rejects_unparseable_input() {
1954 let req = ToolPermissionRequest {
1955 tool_name: "AskUserQuestion".to_string(),
1956 input: serde_json::json!({"not_questions": "garbage"}),
1957 permission_suggestions: vec![],
1958 blocked_path: None,
1959 decision_reason: None,
1960 tool_use_id: None,
1961 };
1962
1963 let answers = HashMap::new();
1964 match req.answer_questions(&answers, "rid-4").unwrap_err() {
1965 AskUserQuestionResponseError::ParseInput(_) => {}
1966 other => panic!("expected ParseInput, got {other:?}"),
1967 }
1968 }
1969
1970 #[test]
1971 fn answer_questions_rejects_out_of_range_index() {
1972 let req = ask_user_question_request();
1973 let mut answers = HashMap::new();
1974 answers.insert(7, "ghost".to_string());
1975
1976 match req.answer_questions(&answers, "rid-5").unwrap_err() {
1977 AskUserQuestionResponseError::QuestionIndexOutOfRange { index, total } => {
1978 assert_eq!(index, 7);
1979 assert_eq!(total, 2);
1980 }
1981 other => panic!("expected QuestionIndexOutOfRange, got {other:?}"),
1982 }
1983 }
1984}