1use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ControlRequest {
14 #[serde(rename = "type")]
16 pub ty: ControlRequestType,
17 pub request_id: String,
19 pub request: ControlRequestKind,
21}
22
23#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
25#[serde(rename_all = "snake_case")]
26pub enum ControlRequestType {
27 ControlRequest,
29}
30
31#[derive(Debug, Clone)]
39pub enum ControlRequestKind {
40 CanUseTool {
42 tool_name: String,
44 input: Value,
46 permission_suggestions: Vec<Value>,
48 blocked_path: Option<String>,
50 tool_use_id: String,
52 agent_id: Option<String>,
54 decision_reason: Option<String>,
57 title: Option<String>,
60 display_name: Option<String>,
62 description: Option<String>,
64 },
65 McpMessage {
68 server_name: String,
71 message: Value,
74 },
75 HookCallback {
78 callback_id: String,
80 input: Value,
82 tool_use_id: Option<String>,
84 },
85 Unknown {
90 subtype: String,
92 raw: Value,
94 },
95}
96
97impl Serialize for ControlRequestKind {
98 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
99 use serde::ser::SerializeMap;
100 match self {
101 Self::CanUseTool {
102 tool_name,
103 input,
104 permission_suggestions,
105 blocked_path,
106 tool_use_id,
107 agent_id,
108 decision_reason,
109 title,
110 display_name,
111 description,
112 } => {
113 let mut field_count = 4;
114 if !permission_suggestions.is_empty() {
115 field_count += 1;
116 }
117 if blocked_path.is_some() {
118 field_count += 1;
119 }
120 if agent_id.is_some() {
121 field_count += 1;
122 }
123 for opt in [decision_reason, title, display_name, description] {
124 if opt.is_some() {
125 field_count += 1;
126 }
127 }
128 let mut map = serializer.serialize_map(Some(field_count))?;
129 map.serialize_entry("subtype", "can_use_tool")?;
130 map.serialize_entry("tool_name", tool_name)?;
131 map.serialize_entry("input", input)?;
132 if !permission_suggestions.is_empty() {
133 map.serialize_entry("permission_suggestions", permission_suggestions)?;
134 }
135 if let Some(bp) = blocked_path {
136 map.serialize_entry("blocked_path", bp)?;
137 }
138 map.serialize_entry("tool_use_id", tool_use_id)?;
139 if let Some(aid) = agent_id {
140 map.serialize_entry("agent_id", aid)?;
141 }
142 if let Some(dr) = decision_reason {
143 map.serialize_entry("decision_reason", dr)?;
144 }
145 if let Some(t) = title {
146 map.serialize_entry("title", t)?;
147 }
148 if let Some(dn) = display_name {
149 map.serialize_entry("display_name", dn)?;
150 }
151 if let Some(d) = description {
152 map.serialize_entry("description", d)?;
153 }
154 map.end()
155 }
156 Self::McpMessage { server_name, message } => {
157 let mut map = serializer.serialize_map(Some(3))?;
158 map.serialize_entry("subtype", "mcp_message")?;
159 map.serialize_entry("server_name", server_name)?;
160 map.serialize_entry("message", message)?;
161 map.end()
162 }
163 Self::HookCallback { callback_id, input, tool_use_id } => {
164 let mut field_count = 3;
165 if tool_use_id.is_some() {
166 field_count += 1;
167 }
168 let mut map = serializer.serialize_map(Some(field_count))?;
169 map.serialize_entry("subtype", "hook_callback")?;
170 map.serialize_entry("callback_id", callback_id)?;
171 map.serialize_entry("input", input)?;
172 if let Some(tid) = tool_use_id {
173 map.serialize_entry("tool_use_id", tid)?;
174 }
175 map.end()
176 }
177 Self::Unknown { raw, .. } => raw.serialize(serializer),
178 }
179 }
180}
181
182impl<'de> Deserialize<'de> for ControlRequestKind {
183 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
184 let raw = Value::deserialize(deserializer)?;
185 let subtype = raw.get("subtype").and_then(Value::as_str).unwrap_or("<missing>").to_string();
190 match subtype.as_str() {
191 "can_use_tool" => {
192 let tool_name = raw
193 .get("tool_name")
194 .and_then(Value::as_str)
195 .ok_or_else(|| serde::de::Error::missing_field("tool_name"))?
196 .to_string();
197 let input = raw.get("input").cloned().unwrap_or(Value::Null);
198 let permission_suggestions = raw
199 .get("permission_suggestions")
200 .and_then(|v| v.as_array().cloned())
201 .unwrap_or_default();
202 let blocked_path =
203 raw.get("blocked_path").and_then(Value::as_str).map(str::to_string);
204 let tool_use_id = raw
205 .get("tool_use_id")
206 .and_then(Value::as_str)
207 .ok_or_else(|| serde::de::Error::missing_field("tool_use_id"))?
208 .to_string();
209 let agent_id = raw.get("agent_id").and_then(Value::as_str).map(str::to_string);
210 let decision_reason =
211 raw.get("decision_reason").and_then(Value::as_str).map(str::to_string);
212 let title = raw.get("title").and_then(Value::as_str).map(str::to_string);
213 let display_name =
214 raw.get("display_name").and_then(Value::as_str).map(str::to_string);
215 let description =
216 raw.get("description").and_then(Value::as_str).map(str::to_string);
217 Ok(Self::CanUseTool {
218 tool_name,
219 input,
220 permission_suggestions,
221 blocked_path,
222 tool_use_id,
223 agent_id,
224 decision_reason,
225 title,
226 display_name,
227 description,
228 })
229 }
230 "mcp_message" => {
231 let server_name = raw
232 .get("server_name")
233 .and_then(Value::as_str)
234 .ok_or_else(|| serde::de::Error::missing_field("server_name"))?
235 .to_string();
236 let message = raw.get("message").cloned().unwrap_or(Value::Null);
237 Ok(Self::McpMessage { server_name, message })
238 }
239 "hook_callback" => {
240 let callback_id = raw
241 .get("callback_id")
242 .and_then(Value::as_str)
243 .ok_or_else(|| serde::de::Error::missing_field("callback_id"))?
244 .to_string();
245 let input = raw.get("input").cloned().unwrap_or(Value::Null);
246 let tool_use_id =
247 raw.get("tool_use_id").and_then(Value::as_str).map(str::to_string);
248 Ok(Self::HookCallback { callback_id, input, tool_use_id })
249 }
250 other => Ok(Self::Unknown { subtype: other.to_string(), raw }),
255 }
256 }
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ControlResponse {
262 #[serde(rename = "type", default = "default_control_response_type")]
264 pub ty: ControlResponseType,
265 pub response: ControlResponseKind,
267}
268
269fn default_control_response_type() -> ControlResponseType {
270 ControlResponseType::ControlResponse
271}
272
273#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
275#[serde(rename_all = "snake_case")]
276pub enum ControlResponseType {
277 ControlResponse,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(tag = "subtype", rename_all = "snake_case")]
284pub enum ControlResponseKind {
285 Success {
287 request_id: String,
289 response: Value,
291 },
292 Error {
294 request_id: String,
296 error: String,
298 },
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
308#[serde(tag = "behavior", rename_all = "snake_case")]
309pub enum AllowBehavior {
310 Allow {
314 #[serde(rename = "updatedInput")]
317 updated_input: Value,
318 #[serde(default, rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
321 updated_permissions: Option<Value>,
322 },
323 Deny {
326 message: String,
328 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
332 interrupt: bool,
333 },
334}
335
336impl ControlRequest {
337 pub(crate) fn build_response(
345 &self,
346 behavior: AllowBehavior,
347 ) -> Result<ControlResponse, serde_json::Error> {
348 let response_value = serde_json::to_value(behavior)?;
349 Ok(ControlResponse {
350 ty: ControlResponseType::ControlResponse,
351 response: ControlResponseKind::Success {
352 request_id: self.request_id.clone(),
353 response: response_value,
354 },
355 })
356 }
357
358 pub(crate) fn original_tool_input(&self) -> Option<&Value> {
363 match &self.request {
364 ControlRequestKind::CanUseTool { input, .. } => Some(input),
365 _ => None,
366 }
367 }
368}
369
370#[cfg(test)]
371mod tests_control_types {
372 #[allow(unused_imports)]
374 use super::*;
375
376 use crate::control::{
377 AllowBehavior, ControlRequest, ControlRequestKind, ControlResponse, ControlResponseKind,
378 ControlResponseType,
379 };
380 use serde_json::json;
381
382 #[test]
383 fn parse_can_use_tool_request() {
384 let raw = json!({
385 "type": "control_request",
386 "request_id": "req_1_a1b2c3d4",
387 "request": {
388 "subtype": "can_use_tool",
389 "tool_name": "Edit",
390 "input": {"file_path": "/tmp/x"},
391 "permission_suggestions": [],
392 "blocked_path": null,
393 "tool_use_id": "toolu_01abc",
394 "agent_id": null
395 }
396 });
397 let req: ControlRequest = serde_json::from_value(raw).expect("parse");
398 assert_eq!(req.request_id, "req_1_a1b2c3d4");
399 match req.request {
400 ControlRequestKind::CanUseTool { tool_name, input, tool_use_id, .. } => {
401 assert_eq!(tool_name, "Edit");
402 assert_eq!(input["file_path"], "/tmp/x");
403 assert_eq!(tool_use_id, "toolu_01abc");
404 }
405 other => panic!("expected CanUseTool, got: {other:?}"),
406 }
407 }
408
409 #[test]
410 fn serialize_allow_response_uses_camelcase_updated_input() {
411 let allow = AllowBehavior::Allow {
412 updated_input: json!({"file_path": "/tmp/x"}),
413 updated_permissions: None,
414 };
415 let raw = serde_json::to_value(&allow).expect("ser");
416 assert_eq!(raw["behavior"], "allow");
417 assert_eq!(raw["updatedInput"]["file_path"], "/tmp/x");
419 assert!(raw.get("updated_input").is_none(), "must NOT serialise snake_case");
420 }
421
422 #[test]
423 fn serialize_deny_response_with_interrupt_true() {
424 let deny = AllowBehavior::Deny { message: "not allowed".into(), interrupt: true };
425 let raw = serde_json::to_value(&deny).expect("ser");
426 assert_eq!(raw["behavior"], "deny");
427 assert_eq!(raw["message"], "not allowed");
428 assert_eq!(raw["interrupt"], true);
429 }
430
431 #[test]
432 fn deny_with_interrupt_false_omits_field() {
433 let deny = AllowBehavior::Deny { message: "nope".into(), interrupt: false };
434 let raw = serde_json::to_value(&deny).expect("ser");
435 assert_eq!(raw["behavior"], "deny");
436 assert!(raw.get("interrupt").is_none(), "interrupt must be absent when false");
437 }
438
439 #[test]
440 fn full_control_response_envelope() {
441 let resp = ControlResponse {
442 ty: ControlResponseType::ControlResponse,
443 response: ControlResponseKind::Success {
444 request_id: "req_1_a1b2c3d4".into(),
445 response: serde_json::to_value(AllowBehavior::Allow {
446 updated_input: json!({"file_path": "/tmp/x"}),
447 updated_permissions: None,
448 })
449 .unwrap(),
450 },
451 };
452 let raw = serde_json::to_value(&resp).expect("serialize");
453 assert_eq!(raw["type"], "control_response");
454 assert_eq!(raw["response"]["subtype"], "success");
455 assert_eq!(raw["response"]["request_id"], "req_1_a1b2c3d4");
456 assert_eq!(raw["response"]["response"]["behavior"], "allow");
457 assert_eq!(raw["response"]["response"]["updatedInput"]["file_path"], "/tmp/x");
458 }
459
460 #[test]
461 fn deserialize_missing_subtype_lands_in_unknown_with_missing_sentinel() {
462 let raw = json!({
468 "type": "control_request",
469 "request_id": "r1",
470 "request": {"some": "shape"}
471 });
472 let req: ControlRequest = serde_json::from_value(raw).expect("parse");
473 match req.request {
474 ControlRequestKind::Unknown { subtype, .. } => {
475 assert_eq!(
476 subtype, "<missing>",
477 "missing-subtype must use the explicit sentinel, not empty string"
478 );
479 }
480 other => panic!("expected ControlRequestKind::Unknown, got: {other:?}"),
481 }
482 }
483
484 #[test]
485 fn deserialize_unrecognised_subtype_distinguishable_from_missing() {
486 let raw = json!({
491 "type": "control_request",
492 "request_id": "r1",
493 "request": {"subtype": "future_thing", "data": 1}
494 });
495 let req: ControlRequest = serde_json::from_value(raw).expect("parse");
496 match req.request {
497 ControlRequestKind::Unknown { subtype, .. } => {
498 assert_eq!(subtype, "future_thing");
499 assert_ne!(subtype, "<missing>");
500 }
501 other => panic!("expected ControlRequestKind::Unknown, got: {other:?}"),
502 }
503 }
504}