Skip to main content

claude_codes/io/
control.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5// ============================================================================
6// Permission Enums
7// ============================================================================
8
9/// The type of a permission grant.
10///
11/// Determines whether the permission adds rules for specific tools
12/// or sets a broad mode.
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum PermissionType {
15    /// Add fine-grained rules for specific tools.
16    AddRules,
17    /// Set a broad permission mode (e.g., accept all edits).
18    SetMode,
19    /// A type not yet known to this version of the crate.
20    Unknown(String),
21}
22
23impl PermissionType {
24    pub fn as_str(&self) -> &str {
25        match self {
26            Self::AddRules => "addRules",
27            Self::SetMode => "setMode",
28            Self::Unknown(s) => s.as_str(),
29        }
30    }
31}
32
33impl fmt::Display for PermissionType {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.write_str(self.as_str())
36    }
37}
38
39impl From<&str> for PermissionType {
40    fn from(s: &str) -> Self {
41        match s {
42            "addRules" => Self::AddRules,
43            "setMode" => Self::SetMode,
44            other => Self::Unknown(other.to_string()),
45        }
46    }
47}
48
49impl Serialize for PermissionType {
50    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
51        serializer.serialize_str(self.as_str())
52    }
53}
54
55impl<'de> Deserialize<'de> for PermissionType {
56    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
57        let s = String::deserialize(deserializer)?;
58        Ok(Self::from(s.as_str()))
59    }
60}
61
62/// Where a permission applies.
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub enum PermissionDestination {
65    /// Applies only to the current session.
66    Session,
67    /// Persists across sessions for the project.
68    Project,
69    /// A destination not yet known to this version of the crate.
70    Unknown(String),
71}
72
73impl PermissionDestination {
74    pub fn as_str(&self) -> &str {
75        match self {
76            Self::Session => "session",
77            Self::Project => "project",
78            Self::Unknown(s) => s.as_str(),
79        }
80    }
81}
82
83impl fmt::Display for PermissionDestination {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        f.write_str(self.as_str())
86    }
87}
88
89impl From<&str> for PermissionDestination {
90    fn from(s: &str) -> Self {
91        match s {
92            "session" => Self::Session,
93            "project" => Self::Project,
94            other => Self::Unknown(other.to_string()),
95        }
96    }
97}
98
99impl Serialize for PermissionDestination {
100    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
101        serializer.serialize_str(self.as_str())
102    }
103}
104
105impl<'de> Deserialize<'de> for PermissionDestination {
106    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
107        let s = String::deserialize(deserializer)?;
108        Ok(Self::from(s.as_str()))
109    }
110}
111
112/// The behavior of a permission rule.
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
114pub enum PermissionBehavior {
115    /// Allow the tool action.
116    Allow,
117    /// Deny the tool action.
118    Deny,
119    /// A behavior not yet known to this version of the crate.
120    Unknown(String),
121}
122
123impl PermissionBehavior {
124    pub fn as_str(&self) -> &str {
125        match self {
126            Self::Allow => "allow",
127            Self::Deny => "deny",
128            Self::Unknown(s) => s.as_str(),
129        }
130    }
131}
132
133impl fmt::Display for PermissionBehavior {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        f.write_str(self.as_str())
136    }
137}
138
139impl From<&str> for PermissionBehavior {
140    fn from(s: &str) -> Self {
141        match s {
142            "allow" => Self::Allow,
143            "deny" => Self::Deny,
144            other => Self::Unknown(other.to_string()),
145        }
146    }
147}
148
149impl Serialize for PermissionBehavior {
150    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
151        serializer.serialize_str(self.as_str())
152    }
153}
154
155impl<'de> Deserialize<'de> for PermissionBehavior {
156    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
157        let s = String::deserialize(deserializer)?;
158        Ok(Self::from(s.as_str()))
159    }
160}
161
162/// Named permission modes that can be set via `setMode`.
163#[derive(Debug, Clone, PartialEq, Eq, Hash)]
164pub enum PermissionModeName {
165    /// Accept all file edits without prompting.
166    AcceptEdits,
167    /// Bypass all permission checks.
168    BypassPermissions,
169    /// A mode not yet known to this version of the crate.
170    Unknown(String),
171}
172
173impl PermissionModeName {
174    pub fn as_str(&self) -> &str {
175        match self {
176            Self::AcceptEdits => "acceptEdits",
177            Self::BypassPermissions => "bypassPermissions",
178            Self::Unknown(s) => s.as_str(),
179        }
180    }
181}
182
183impl fmt::Display for PermissionModeName {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.write_str(self.as_str())
186    }
187}
188
189impl From<&str> for PermissionModeName {
190    fn from(s: &str) -> Self {
191        match s {
192            "acceptEdits" => Self::AcceptEdits,
193            "bypassPermissions" => Self::BypassPermissions,
194            other => Self::Unknown(other.to_string()),
195        }
196    }
197}
198
199impl Serialize for PermissionModeName {
200    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
201        serializer.serialize_str(self.as_str())
202    }
203}
204
205impl<'de> Deserialize<'de> for PermissionModeName {
206    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
207        let s = String::deserialize(deserializer)?;
208        Ok(Self::from(s.as_str()))
209    }
210}
211
212// ============================================================================
213// Control Protocol Types (for bidirectional tool approval)
214// ============================================================================
215
216/// Control request from CLI (tool permission requests, hooks, etc.)
217///
218/// When using `--permission-prompt-tool stdio`, the CLI sends these requests
219/// asking for approval before executing tools. The SDK must respond with a
220/// [`ControlResponse`].
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct ControlRequest {
223    /// Unique identifier for this request (used to correlate responses)
224    pub request_id: String,
225    /// The request payload
226    pub request: ControlRequestPayload,
227}
228
229/// Control request payload variants
230#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(tag = "subtype", rename_all = "snake_case")]
232pub enum ControlRequestPayload {
233    /// Tool permission request - Claude wants to use a tool
234    CanUseTool(ToolPermissionRequest),
235    /// Hook callback request
236    HookCallback(HookCallbackRequest),
237    /// MCP message request
238    McpMessage(McpMessageRequest),
239    /// Initialize request (sent by SDK to CLI)
240    Initialize(InitializeRequest),
241}
242
243/// A permission to grant for "remember this decision" functionality.
244///
245/// When responding to a tool permission request, you can include permissions
246/// that should be granted to avoid repeated prompts for similar actions.
247///
248/// # Example
249///
250/// ```
251/// use claude_codes::{Permission, PermissionModeName, PermissionDestination};
252///
253/// // Grant permission for a specific bash command
254/// let perm = Permission::allow_tool("Bash", "npm test");
255///
256/// // Grant permission to set a mode for the session
257/// let mode_perm = Permission::set_mode(PermissionModeName::AcceptEdits, PermissionDestination::Session);
258/// ```
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
260pub struct Permission {
261    /// The type of permission (e.g., addRules, setMode)
262    #[serde(rename = "type")]
263    pub permission_type: PermissionType,
264    /// Where to apply this permission (e.g., session, project)
265    pub destination: PermissionDestination,
266    /// The permission mode (for setMode type)
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub mode: Option<PermissionModeName>,
269    /// The behavior (for addRules type, e.g., allow, deny)
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub behavior: Option<PermissionBehavior>,
272    /// The rules to add (for addRules type)
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub rules: Option<Vec<PermissionRule>>,
275}
276
277/// A rule within a permission grant.
278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
279pub struct PermissionRule {
280    /// The name of the tool this rule applies to
281    #[serde(rename = "toolName")]
282    pub tool_name: String,
283    /// The rule content (glob pattern or command pattern)
284    #[serde(rename = "ruleContent")]
285    pub rule_content: String,
286}
287
288impl Permission {
289    /// Create a permission to allow a specific tool with a rule pattern.
290    ///
291    /// # Example
292    /// ```
293    /// use claude_codes::Permission;
294    ///
295    /// // Allow "npm test" bash command for this session
296    /// let perm = Permission::allow_tool("Bash", "npm test");
297    ///
298    /// // Allow reading from /tmp directory
299    /// let read_perm = Permission::allow_tool("Read", "/tmp/**");
300    /// ```
301    pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
302        Permission {
303            permission_type: PermissionType::AddRules,
304            destination: PermissionDestination::Session,
305            mode: None,
306            behavior: Some(PermissionBehavior::Allow),
307            rules: Some(vec![PermissionRule {
308                tool_name: tool_name.into(),
309                rule_content: rule_content.into(),
310            }]),
311        }
312    }
313
314    /// Create a permission to allow a tool with a specific destination.
315    ///
316    /// # Example
317    /// ```
318    /// use claude_codes::{Permission, PermissionDestination};
319    ///
320    /// // Allow for the entire project, not just session
321    /// let perm = Permission::allow_tool_with_destination("Bash", "npm test", PermissionDestination::Project);
322    /// ```
323    pub fn allow_tool_with_destination(
324        tool_name: impl Into<String>,
325        rule_content: impl Into<String>,
326        destination: PermissionDestination,
327    ) -> Self {
328        Permission {
329            permission_type: PermissionType::AddRules,
330            destination,
331            mode: None,
332            behavior: Some(PermissionBehavior::Allow),
333            rules: Some(vec![PermissionRule {
334                tool_name: tool_name.into(),
335                rule_content: rule_content.into(),
336            }]),
337        }
338    }
339
340    /// Create a permission to set a mode (like acceptEdits or bypassPermissions).
341    ///
342    /// # Example
343    /// ```
344    /// use claude_codes::{Permission, PermissionModeName, PermissionDestination};
345    ///
346    /// // Accept all edits for this session
347    /// let perm = Permission::set_mode(PermissionModeName::AcceptEdits, PermissionDestination::Session);
348    /// ```
349    pub fn set_mode(mode: PermissionModeName, destination: PermissionDestination) -> Self {
350        Permission {
351            permission_type: PermissionType::SetMode,
352            destination,
353            mode: Some(mode),
354            behavior: None,
355            rules: None,
356        }
357    }
358
359    /// Create a permission from a PermissionSuggestion.
360    ///
361    /// This is useful when you want to grant a permission that Claude suggested.
362    ///
363    /// # Example
364    /// ```
365    /// use claude_codes::{Permission, PermissionSuggestion, PermissionType, PermissionDestination, PermissionModeName};
366    ///
367    /// // Convert a suggestion to a permission for the response
368    /// let suggestion = PermissionSuggestion {
369    ///     suggestion_type: PermissionType::SetMode,
370    ///     destination: PermissionDestination::Session,
371    ///     mode: Some(PermissionModeName::AcceptEdits),
372    ///     behavior: None,
373    ///     rules: None,
374    /// };
375    /// let perm = Permission::from_suggestion(&suggestion);
376    /// ```
377    pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
378        Permission {
379            permission_type: suggestion.suggestion_type.clone(),
380            destination: suggestion.destination.clone(),
381            mode: suggestion.mode.clone(),
382            behavior: suggestion.behavior.clone(),
383            rules: suggestion.rules.as_ref().map(|rules| {
384                rules
385                    .iter()
386                    .filter_map(|v| {
387                        Some(PermissionRule {
388                            tool_name: v.get("toolName")?.as_str()?.to_string(),
389                            rule_content: v.get("ruleContent")?.as_str()?.to_string(),
390                        })
391                    })
392                    .collect()
393            }),
394        }
395    }
396}
397
398/// A suggested permission for tool approval.
399///
400/// When Claude requests tool permission, it may include suggestions for
401/// permissions that could be granted to avoid repeated prompts for similar
402/// actions. The format varies based on the suggestion type:
403///
404/// - `setMode`: `{"type": "setMode", "mode": "acceptEdits", "destination": "session"}`
405/// - `addRules`: `{"type": "addRules", "rules": [...], "behavior": "allow", "destination": "session"}`
406///
407/// Use the helper methods to access common fields.
408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
409pub struct PermissionSuggestion {
410    /// The type of suggestion (e.g., setMode, addRules)
411    #[serde(rename = "type")]
412    pub suggestion_type: PermissionType,
413    /// Where to apply this permission (e.g., session, project)
414    pub destination: PermissionDestination,
415    /// The permission mode (for setMode type)
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub mode: Option<PermissionModeName>,
418    /// The behavior (for addRules type, e.g., allow)
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub behavior: Option<PermissionBehavior>,
421    /// The rules to add (for addRules type)
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub rules: Option<Vec<Value>>,
424}
425
426/// Tool permission request details
427///
428/// This is sent when Claude wants to use a tool. The SDK should evaluate
429/// the request and respond with allow/deny using the ergonomic builder methods.
430///
431/// # Example
432///
433/// ```
434/// use claude_codes::{ToolPermissionRequest, ControlResponse};
435/// use serde_json::json;
436///
437/// fn handle_permission(req: &ToolPermissionRequest, request_id: &str) -> ControlResponse {
438///     // Block dangerous bash commands
439///     if req.tool_name == "Bash" {
440///         if let Some(cmd) = req.input.get("command").and_then(|v| v.as_str()) {
441///             if cmd.contains("rm -rf") {
442///                 return req.deny("Dangerous command blocked", request_id);
443///             }
444///         }
445///     }
446///
447///     // Allow everything else
448///     req.allow(request_id)
449/// }
450/// ```
451#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct ToolPermissionRequest {
453    /// Name of the tool Claude wants to use (e.g., "Bash", "Write", "Read")
454    pub tool_name: String,
455    /// Input parameters for the tool
456    pub input: Value,
457    /// Suggested permissions that could be granted to avoid repeated prompts
458    #[serde(default)]
459    pub permission_suggestions: Vec<PermissionSuggestion>,
460    /// Path that was blocked (if this is a retry after path-based denial)
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub blocked_path: Option<String>,
463    /// Reason why this tool use requires approval
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub decision_reason: Option<String>,
466    /// The tool use ID for this request
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub tool_use_id: Option<String>,
469}
470
471impl ToolPermissionRequest {
472    /// Allow the tool to execute with its original input.
473    ///
474    /// # Example
475    /// ```
476    /// # use claude_codes::ToolPermissionRequest;
477    /// # use serde_json::json;
478    /// let req = ToolPermissionRequest {
479    ///     tool_name: "Read".to_string(),
480    ///     input: json!({"file_path": "/tmp/test.txt"}),
481    ///     permission_suggestions: vec![],
482    ///     blocked_path: None,
483    ///     decision_reason: None,
484    ///     tool_use_id: None,
485    /// };
486    /// let response = req.allow("req-123");
487    /// ```
488    pub fn allow(&self, request_id: &str) -> ControlResponse {
489        ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
490    }
491
492    /// Allow the tool to execute with modified input.
493    ///
494    /// Use this to sanitize or redirect tool inputs. For example, redirecting
495    /// file writes to a safe directory.
496    ///
497    /// # Example
498    /// ```
499    /// # use claude_codes::ToolPermissionRequest;
500    /// # use serde_json::json;
501    /// let req = ToolPermissionRequest {
502    ///     tool_name: "Write".to_string(),
503    ///     input: json!({"file_path": "/etc/passwd", "content": "test"}),
504    ///     permission_suggestions: vec![],
505    ///     blocked_path: None,
506    ///     decision_reason: None,
507    ///     tool_use_id: None,
508    /// };
509    /// // Redirect to safe location
510    /// let safe_input = json!({"file_path": "/tmp/safe/passwd", "content": "test"});
511    /// let response = req.allow_with(safe_input, "req-123");
512    /// ```
513    pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
514        ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
515    }
516
517    /// Allow with updated permissions list (raw JSON Values).
518    ///
519    /// Prefer using `allow_and_remember` for type safety.
520    pub fn allow_with_permissions(
521        &self,
522        modified_input: Value,
523        permissions: Vec<Value>,
524        request_id: &str,
525    ) -> ControlResponse {
526        ControlResponse::from_result(
527            request_id,
528            PermissionResult::allow_with_permissions(modified_input, permissions),
529        )
530    }
531
532    /// Allow the tool and grant permissions for "remember this decision".
533    ///
534    /// This is the ergonomic way to allow a tool while also granting permissions
535    /// so similar actions won't require approval in the future.
536    ///
537    /// # Example
538    /// ```
539    /// use claude_codes::{ToolPermissionRequest, Permission};
540    /// use serde_json::json;
541    ///
542    /// let req = ToolPermissionRequest {
543    ///     tool_name: "Bash".to_string(),
544    ///     input: json!({"command": "npm test"}),
545    ///     permission_suggestions: vec![],
546    ///     blocked_path: None,
547    ///     decision_reason: None,
548    ///     tool_use_id: None,
549    /// };
550    ///
551    /// // Allow and remember this decision for the session
552    /// let response = req.allow_and_remember(
553    ///     vec![Permission::allow_tool("Bash", "npm test")],
554    ///     "req-123",
555    /// );
556    /// ```
557    pub fn allow_and_remember(
558        &self,
559        permissions: Vec<Permission>,
560        request_id: &str,
561    ) -> ControlResponse {
562        ControlResponse::from_result(
563            request_id,
564            PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
565        )
566    }
567
568    /// Allow the tool with modified input and grant permissions.
569    ///
570    /// Combines input modification with "remember this decision" functionality.
571    pub fn allow_with_and_remember(
572        &self,
573        modified_input: Value,
574        permissions: Vec<Permission>,
575        request_id: &str,
576    ) -> ControlResponse {
577        ControlResponse::from_result(
578            request_id,
579            PermissionResult::allow_with_typed_permissions(modified_input, permissions),
580        )
581    }
582
583    /// Allow the tool and remember using the first permission suggestion.
584    ///
585    /// This is a convenience method for the common case of accepting Claude's
586    /// first suggested permission (usually the most relevant one).
587    ///
588    /// Returns `None` if there are no permission suggestions.
589    ///
590    /// # Example
591    /// ```
592    /// use claude_codes::ToolPermissionRequest;
593    /// use serde_json::json;
594    ///
595    /// let req = ToolPermissionRequest {
596    ///     tool_name: "Bash".to_string(),
597    ///     input: json!({"command": "npm test"}),
598    ///     permission_suggestions: vec![],  // Would have suggestions in real use
599    ///     blocked_path: None,
600    ///     decision_reason: None,
601    ///     tool_use_id: None,
602    /// };
603    ///
604    /// // Try to allow with first suggestion, or just allow without remembering
605    /// let response = req.allow_and_remember_suggestion("req-123")
606    ///     .unwrap_or_else(|| req.allow("req-123"));
607    /// ```
608    pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
609        self.permission_suggestions.first().map(|suggestion| {
610            let perm = Permission::from_suggestion(suggestion);
611            self.allow_and_remember(vec![perm], request_id)
612        })
613    }
614
615    /// Deny the tool execution.
616    ///
617    /// The message will be shown to Claude, who may try a different approach.
618    ///
619    /// # Example
620    /// ```
621    /// # use claude_codes::ToolPermissionRequest;
622    /// # use serde_json::json;
623    /// let req = ToolPermissionRequest {
624    ///     tool_name: "Bash".to_string(),
625    ///     input: json!({"command": "sudo rm -rf /"}),
626    ///     permission_suggestions: vec![],
627    ///     blocked_path: None,
628    ///     decision_reason: None,
629    ///     tool_use_id: None,
630    /// };
631    /// let response = req.deny("Dangerous command blocked by policy", "req-123");
632    /// ```
633    pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
634        ControlResponse::from_result(request_id, PermissionResult::deny(message))
635    }
636
637    /// Deny the tool execution and stop the entire session.
638    ///
639    /// Use this for severe policy violations that should halt all processing.
640    pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
641        ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
642    }
643}
644
645/// Result of a permission decision
646///
647/// This type represents the decision made by the permission callback.
648/// It can be serialized directly into the control response format.
649#[derive(Debug, Clone, Serialize, Deserialize)]
650#[serde(tag = "behavior", rename_all = "snake_case")]
651pub enum PermissionResult {
652    /// Allow the tool to execute
653    Allow {
654        /// The (possibly modified) input to pass to the tool
655        #[serde(rename = "updatedInput")]
656        updated_input: Value,
657        /// Optional updated permissions list
658        #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
659        updated_permissions: Option<Vec<Value>>,
660    },
661    /// Deny the tool execution
662    Deny {
663        /// Message explaining why the tool was denied
664        message: String,
665        /// If true, stop the entire session
666        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
667        interrupt: bool,
668    },
669}
670
671impl PermissionResult {
672    /// Create an allow result with the given input
673    pub fn allow(input: Value) -> Self {
674        PermissionResult::Allow {
675            updated_input: input,
676            updated_permissions: None,
677        }
678    }
679
680    /// Create an allow result with raw permissions (as JSON Values).
681    ///
682    /// Prefer using `allow_with_typed_permissions` for type safety.
683    pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
684        PermissionResult::Allow {
685            updated_input: input,
686            updated_permissions: Some(permissions),
687        }
688    }
689
690    /// Create an allow result with typed permissions.
691    ///
692    /// This is the preferred way to grant permissions for "remember this decision"
693    /// functionality.
694    ///
695    /// # Example
696    /// ```
697    /// use claude_codes::{Permission, PermissionResult};
698    /// use serde_json::json;
699    ///
700    /// let result = PermissionResult::allow_with_typed_permissions(
701    ///     json!({"command": "npm test"}),
702    ///     vec![Permission::allow_tool("Bash", "npm test")],
703    /// );
704    /// ```
705    pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
706        let permission_values: Vec<Value> = permissions
707            .into_iter()
708            .filter_map(|p| serde_json::to_value(p).ok())
709            .collect();
710        PermissionResult::Allow {
711            updated_input: input,
712            updated_permissions: Some(permission_values),
713        }
714    }
715
716    /// Create a deny result
717    pub fn deny(message: impl Into<String>) -> Self {
718        PermissionResult::Deny {
719            message: message.into(),
720            interrupt: false,
721        }
722    }
723
724    /// Create a deny result that also interrupts the session
725    pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
726        PermissionResult::Deny {
727            message: message.into(),
728            interrupt: true,
729        }
730    }
731}
732
733/// Hook callback request
734#[derive(Debug, Clone, Serialize, Deserialize)]
735pub struct HookCallbackRequest {
736    pub callback_id: String,
737    pub input: Value,
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub tool_use_id: Option<String>,
740}
741
742/// MCP message request
743#[derive(Debug, Clone, Serialize, Deserialize)]
744pub struct McpMessageRequest {
745    pub server_name: String,
746    pub message: Value,
747}
748
749/// Initialize request (SDK -> CLI)
750#[derive(Debug, Clone, Serialize, Deserialize)]
751pub struct InitializeRequest {
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub hooks: Option<Value>,
754}
755
756/// Control response to CLI
757///
758/// Built using the ergonomic methods on [`ToolPermissionRequest`] or
759/// constructed directly for other control request types.
760#[derive(Debug, Clone, Serialize, Deserialize)]
761pub struct ControlResponse {
762    /// The request ID this response corresponds to
763    pub response: ControlResponsePayload,
764}
765
766impl ControlResponse {
767    /// Create a success response from a PermissionResult
768    ///
769    /// This is the preferred way to construct permission responses.
770    pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
771        // Serialize the PermissionResult to Value for the response
772        let response_value = serde_json::to_value(&result)
773            .expect("PermissionResult serialization should never fail");
774        ControlResponse {
775            response: ControlResponsePayload::Success {
776                request_id: request_id.to_string(),
777                response: Some(response_value),
778            },
779        }
780    }
781
782    /// Create a success response with the given payload (raw Value)
783    pub fn success(request_id: &str, response_data: Value) -> Self {
784        ControlResponse {
785            response: ControlResponsePayload::Success {
786                request_id: request_id.to_string(),
787                response: Some(response_data),
788            },
789        }
790    }
791
792    /// Create an empty success response (for acks)
793    pub fn success_empty(request_id: &str) -> Self {
794        ControlResponse {
795            response: ControlResponsePayload::Success {
796                request_id: request_id.to_string(),
797                response: None,
798            },
799        }
800    }
801
802    /// Create an error response
803    pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
804        ControlResponse {
805            response: ControlResponsePayload::Error {
806                request_id: request_id.to_string(),
807                error: error_message.into(),
808            },
809        }
810    }
811}
812
813/// Control response payload
814#[derive(Debug, Clone, Serialize, Deserialize)]
815#[serde(tag = "subtype", rename_all = "snake_case")]
816pub enum ControlResponsePayload {
817    Success {
818        request_id: String,
819        #[serde(skip_serializing_if = "Option::is_none")]
820        response: Option<Value>,
821    },
822    Error {
823        request_id: String,
824        error: String,
825    },
826}
827
828/// Wrapper for outgoing control responses (includes type tag)
829#[derive(Debug, Clone, Serialize, Deserialize)]
830pub struct ControlResponseMessage {
831    #[serde(rename = "type")]
832    pub message_type: String,
833    pub response: ControlResponsePayload,
834}
835
836impl From<ControlResponse> for ControlResponseMessage {
837    fn from(resp: ControlResponse) -> Self {
838        ControlResponseMessage {
839            message_type: "control_response".to_string(),
840            response: resp.response,
841        }
842    }
843}
844
845/// Wrapper for outgoing control requests (includes type tag)
846#[derive(Debug, Clone, Serialize, Deserialize)]
847pub struct ControlRequestMessage {
848    #[serde(rename = "type")]
849    pub message_type: String,
850    pub request_id: String,
851    pub request: ControlRequestPayload,
852}
853
854impl ControlRequestMessage {
855    /// Create an initialization request to send to CLI
856    pub fn initialize(request_id: impl Into<String>) -> Self {
857        ControlRequestMessage {
858            message_type: "control_request".to_string(),
859            request_id: request_id.into(),
860            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
861        }
862    }
863
864    /// Create an initialization request with hooks configuration
865    pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
866        ControlRequestMessage {
867            message_type: "control_request".to_string(),
868            request_id: request_id.into(),
869            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
870        }
871    }
872}
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877    use crate::io::ClaudeOutput;
878
879    #[test]
880    fn test_deserialize_control_request_can_use_tool() {
881        let json = r#"{
882            "type": "control_request",
883            "request_id": "perm-abc123",
884            "request": {
885                "subtype": "can_use_tool",
886                "tool_name": "Write",
887                "input": {
888                    "file_path": "/home/user/hello.py",
889                    "content": "print('hello')"
890                },
891                "permission_suggestions": [],
892                "blocked_path": null
893            }
894        }"#;
895
896        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
897        assert!(output.is_control_request());
898
899        if let ClaudeOutput::ControlRequest(req) = output {
900            assert_eq!(req.request_id, "perm-abc123");
901            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
902                assert_eq!(perm_req.tool_name, "Write");
903                assert_eq!(
904                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
905                    "/home/user/hello.py"
906                );
907            } else {
908                panic!("Expected CanUseTool payload");
909            }
910        } else {
911            panic!("Expected ControlRequest");
912        }
913    }
914
915    #[test]
916    fn test_deserialize_control_request_edit_tool_real() {
917        // Real production message from Claude CLI
918        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"}}"#;
919
920        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
921        assert!(output.is_control_request());
922        assert_eq!(output.message_type(), "control_request");
923
924        if let ClaudeOutput::ControlRequest(req) = output {
925            assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
926            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
927                assert_eq!(perm_req.tool_name, "Edit");
928                assert_eq!(
929                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
930                    "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
931                );
932                assert!(perm_req.input.get("old_string").is_some());
933                assert!(perm_req.input.get("new_string").is_some());
934                assert!(!perm_req
935                    .input
936                    .get("replace_all")
937                    .unwrap()
938                    .as_bool()
939                    .unwrap());
940            } else {
941                panic!("Expected CanUseTool payload");
942            }
943        } else {
944            panic!("Expected ControlRequest");
945        }
946    }
947
948    #[test]
949    fn test_tool_permission_request_allow() {
950        let req = ToolPermissionRequest {
951            tool_name: "Read".to_string(),
952            input: serde_json::json!({"file_path": "/tmp/test.txt"}),
953            permission_suggestions: vec![],
954            blocked_path: None,
955            decision_reason: None,
956            tool_use_id: None,
957        };
958
959        let response = req.allow("req-123");
960        let message: ControlResponseMessage = response.into();
961
962        let json = serde_json::to_string(&message).unwrap();
963        assert!(json.contains("\"type\":\"control_response\""));
964        assert!(json.contains("\"subtype\":\"success\""));
965        assert!(json.contains("\"request_id\":\"req-123\""));
966        assert!(json.contains("\"behavior\":\"allow\""));
967        assert!(json.contains("\"updatedInput\""));
968    }
969
970    #[test]
971    fn test_tool_permission_request_allow_with_modified_input() {
972        let req = ToolPermissionRequest {
973            tool_name: "Write".to_string(),
974            input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
975            permission_suggestions: vec![],
976            blocked_path: None,
977            decision_reason: None,
978            tool_use_id: None,
979        };
980
981        let modified_input = serde_json::json!({
982            "file_path": "/tmp/safe/passwd",
983            "content": "test"
984        });
985        let response = req.allow_with(modified_input, "req-456");
986        let message: ControlResponseMessage = response.into();
987
988        let json = serde_json::to_string(&message).unwrap();
989        assert!(json.contains("/tmp/safe/passwd"));
990        assert!(!json.contains("/etc/passwd"));
991    }
992
993    #[test]
994    fn test_tool_permission_request_deny() {
995        let req = ToolPermissionRequest {
996            tool_name: "Bash".to_string(),
997            input: serde_json::json!({"command": "sudo rm -rf /"}),
998            permission_suggestions: vec![],
999            blocked_path: None,
1000            decision_reason: None,
1001            tool_use_id: None,
1002        };
1003
1004        let response = req.deny("Dangerous command blocked", "req-789");
1005        let message: ControlResponseMessage = response.into();
1006
1007        let json = serde_json::to_string(&message).unwrap();
1008        assert!(json.contains("\"behavior\":\"deny\""));
1009        assert!(json.contains("Dangerous command blocked"));
1010        assert!(!json.contains("\"interrupt\":true"));
1011    }
1012
1013    #[test]
1014    fn test_tool_permission_request_deny_and_stop() {
1015        let req = ToolPermissionRequest {
1016            tool_name: "Bash".to_string(),
1017            input: serde_json::json!({"command": "rm -rf /"}),
1018            permission_suggestions: vec![],
1019            blocked_path: None,
1020            decision_reason: None,
1021            tool_use_id: None,
1022        };
1023
1024        let response = req.deny_and_stop("Security violation", "req-000");
1025        let message: ControlResponseMessage = response.into();
1026
1027        let json = serde_json::to_string(&message).unwrap();
1028        assert!(json.contains("\"behavior\":\"deny\""));
1029        assert!(json.contains("\"interrupt\":true"));
1030    }
1031
1032    #[test]
1033    fn test_permission_result_serialization() {
1034        // Test allow
1035        let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1036        let json = serde_json::to_string(&allow).unwrap();
1037        assert!(json.contains("\"behavior\":\"allow\""));
1038        assert!(json.contains("\"updatedInput\""));
1039
1040        // Test deny
1041        let deny = PermissionResult::deny("Not allowed");
1042        let json = serde_json::to_string(&deny).unwrap();
1043        assert!(json.contains("\"behavior\":\"deny\""));
1044        assert!(json.contains("\"message\":\"Not allowed\""));
1045        assert!(!json.contains("\"interrupt\""));
1046
1047        // Test deny with interrupt
1048        let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1049        let json = serde_json::to_string(&deny_stop).unwrap();
1050        assert!(json.contains("\"interrupt\":true"));
1051    }
1052
1053    #[test]
1054    fn test_control_request_message_initialize() {
1055        let init = ControlRequestMessage::initialize("init-1");
1056
1057        let json = serde_json::to_string(&init).unwrap();
1058        assert!(json.contains("\"type\":\"control_request\""));
1059        assert!(json.contains("\"request_id\":\"init-1\""));
1060        assert!(json.contains("\"subtype\":\"initialize\""));
1061    }
1062
1063    #[test]
1064    fn test_control_response_error() {
1065        let response = ControlResponse::error("req-err", "Something went wrong");
1066        let message: ControlResponseMessage = response.into();
1067
1068        let json = serde_json::to_string(&message).unwrap();
1069        assert!(json.contains("\"subtype\":\"error\""));
1070        assert!(json.contains("\"error\":\"Something went wrong\""));
1071    }
1072
1073    #[test]
1074    fn test_roundtrip_control_request() {
1075        let original_json = r#"{
1076            "type": "control_request",
1077            "request_id": "test-123",
1078            "request": {
1079                "subtype": "can_use_tool",
1080                "tool_name": "Bash",
1081                "input": {"command": "ls -la"},
1082                "permission_suggestions": []
1083            }
1084        }"#;
1085
1086        let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1087
1088        let reserialized = serde_json::to_string(&output).unwrap();
1089        assert!(reserialized.contains("control_request"));
1090        assert!(reserialized.contains("test-123"));
1091        assert!(reserialized.contains("Bash"));
1092    }
1093
1094    #[test]
1095    fn test_permission_suggestions_parsing() {
1096        let json = r#"{
1097            "type": "control_request",
1098            "request_id": "perm-456",
1099            "request": {
1100                "subtype": "can_use_tool",
1101                "tool_name": "Bash",
1102                "input": {"command": "npm test"},
1103                "permission_suggestions": [
1104                    {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1105                    {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1106                ]
1107            }
1108        }"#;
1109
1110        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1111        if let ClaudeOutput::ControlRequest(req) = output {
1112            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1113                assert_eq!(perm_req.permission_suggestions.len(), 2);
1114                assert_eq!(
1115                    perm_req.permission_suggestions[0].suggestion_type,
1116                    PermissionType::SetMode
1117                );
1118                assert_eq!(
1119                    perm_req.permission_suggestions[0].mode,
1120                    Some(PermissionModeName::AcceptEdits)
1121                );
1122                assert_eq!(
1123                    perm_req.permission_suggestions[0].destination,
1124                    PermissionDestination::Session
1125                );
1126                assert_eq!(
1127                    perm_req.permission_suggestions[1].suggestion_type,
1128                    PermissionType::SetMode
1129                );
1130                assert_eq!(
1131                    perm_req.permission_suggestions[1].mode,
1132                    Some(PermissionModeName::BypassPermissions)
1133                );
1134                assert_eq!(
1135                    perm_req.permission_suggestions[1].destination,
1136                    PermissionDestination::Project
1137                );
1138            } else {
1139                panic!("Expected CanUseTool payload");
1140            }
1141        } else {
1142            panic!("Expected ControlRequest");
1143        }
1144    }
1145
1146    #[test]
1147    fn test_permission_suggestion_set_mode_roundtrip() {
1148        let suggestion = PermissionSuggestion {
1149            suggestion_type: PermissionType::SetMode,
1150            destination: PermissionDestination::Session,
1151            mode: Some(PermissionModeName::AcceptEdits),
1152            behavior: None,
1153            rules: None,
1154        };
1155
1156        let json = serde_json::to_string(&suggestion).unwrap();
1157        assert!(json.contains("\"type\":\"setMode\""));
1158        assert!(json.contains("\"mode\":\"acceptEdits\""));
1159        assert!(json.contains("\"destination\":\"session\""));
1160        assert!(!json.contains("\"behavior\""));
1161        assert!(!json.contains("\"rules\""));
1162
1163        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1164        assert_eq!(parsed, suggestion);
1165    }
1166
1167    #[test]
1168    fn test_permission_suggestion_add_rules_roundtrip() {
1169        let suggestion = PermissionSuggestion {
1170            suggestion_type: PermissionType::AddRules,
1171            destination: PermissionDestination::Session,
1172            mode: None,
1173            behavior: Some(PermissionBehavior::Allow),
1174            rules: Some(vec![serde_json::json!({
1175                "toolName": "Read",
1176                "ruleContent": "//tmp/**"
1177            })]),
1178        };
1179
1180        let json = serde_json::to_string(&suggestion).unwrap();
1181        assert!(json.contains("\"type\":\"addRules\""));
1182        assert!(json.contains("\"behavior\":\"allow\""));
1183        assert!(json.contains("\"destination\":\"session\""));
1184        assert!(json.contains("\"rules\""));
1185        assert!(json.contains("\"toolName\":\"Read\""));
1186        assert!(!json.contains("\"mode\""));
1187
1188        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1189        assert_eq!(parsed, suggestion);
1190    }
1191
1192    #[test]
1193    fn test_permission_suggestion_add_rules_from_real_json() {
1194        let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1195
1196        let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1197        assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1198        assert_eq!(parsed.destination, PermissionDestination::Session);
1199        assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1200        assert!(parsed.rules.is_some());
1201        assert!(parsed.mode.is_none());
1202    }
1203
1204    #[test]
1205    fn test_permission_allow_tool() {
1206        let perm = Permission::allow_tool("Bash", "npm test");
1207
1208        assert_eq!(perm.permission_type, PermissionType::AddRules);
1209        assert_eq!(perm.destination, PermissionDestination::Session);
1210        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1211        assert!(perm.mode.is_none());
1212
1213        let rules = perm.rules.unwrap();
1214        assert_eq!(rules.len(), 1);
1215        assert_eq!(rules[0].tool_name, "Bash");
1216        assert_eq!(rules[0].rule_content, "npm test");
1217    }
1218
1219    #[test]
1220    fn test_permission_allow_tool_with_destination() {
1221        let perm = Permission::allow_tool_with_destination(
1222            "Read",
1223            "/tmp/**",
1224            PermissionDestination::Project,
1225        );
1226
1227        assert_eq!(perm.permission_type, PermissionType::AddRules);
1228        assert_eq!(perm.destination, PermissionDestination::Project);
1229        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1230
1231        let rules = perm.rules.unwrap();
1232        assert_eq!(rules[0].tool_name, "Read");
1233        assert_eq!(rules[0].rule_content, "/tmp/**");
1234    }
1235
1236    #[test]
1237    fn test_permission_set_mode() {
1238        let perm = Permission::set_mode(
1239            PermissionModeName::AcceptEdits,
1240            PermissionDestination::Session,
1241        );
1242
1243        assert_eq!(perm.permission_type, PermissionType::SetMode);
1244        assert_eq!(perm.destination, PermissionDestination::Session);
1245        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1246        assert!(perm.behavior.is_none());
1247        assert!(perm.rules.is_none());
1248    }
1249
1250    #[test]
1251    fn test_permission_serialization() {
1252        let perm = Permission::allow_tool("Bash", "npm test");
1253        let json = serde_json::to_string(&perm).unwrap();
1254
1255        assert!(json.contains("\"type\":\"addRules\""));
1256        assert!(json.contains("\"destination\":\"session\""));
1257        assert!(json.contains("\"behavior\":\"allow\""));
1258        assert!(json.contains("\"toolName\":\"Bash\""));
1259        assert!(json.contains("\"ruleContent\":\"npm test\""));
1260    }
1261
1262    #[test]
1263    fn test_permission_from_suggestion_set_mode() {
1264        let suggestion = PermissionSuggestion {
1265            suggestion_type: PermissionType::SetMode,
1266            destination: PermissionDestination::Session,
1267            mode: Some(PermissionModeName::AcceptEdits),
1268            behavior: None,
1269            rules: None,
1270        };
1271
1272        let perm = Permission::from_suggestion(&suggestion);
1273
1274        assert_eq!(perm.permission_type, PermissionType::SetMode);
1275        assert_eq!(perm.destination, PermissionDestination::Session);
1276        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1277    }
1278
1279    #[test]
1280    fn test_permission_from_suggestion_add_rules() {
1281        let suggestion = PermissionSuggestion {
1282            suggestion_type: PermissionType::AddRules,
1283            destination: PermissionDestination::Session,
1284            mode: None,
1285            behavior: Some(PermissionBehavior::Allow),
1286            rules: Some(vec![serde_json::json!({
1287                "toolName": "Read",
1288                "ruleContent": "/tmp/**"
1289            })]),
1290        };
1291
1292        let perm = Permission::from_suggestion(&suggestion);
1293
1294        assert_eq!(perm.permission_type, PermissionType::AddRules);
1295        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1296
1297        let rules = perm.rules.unwrap();
1298        assert_eq!(rules.len(), 1);
1299        assert_eq!(rules[0].tool_name, "Read");
1300        assert_eq!(rules[0].rule_content, "/tmp/**");
1301    }
1302
1303    #[test]
1304    fn test_permission_result_allow_with_typed_permissions() {
1305        let result = PermissionResult::allow_with_typed_permissions(
1306            serde_json::json!({"command": "npm test"}),
1307            vec![Permission::allow_tool("Bash", "npm test")],
1308        );
1309
1310        let json = serde_json::to_string(&result).unwrap();
1311        assert!(json.contains("\"behavior\":\"allow\""));
1312        assert!(json.contains("\"updatedPermissions\""));
1313        assert!(json.contains("\"toolName\":\"Bash\""));
1314    }
1315
1316    #[test]
1317    fn test_tool_permission_request_allow_and_remember() {
1318        let req = ToolPermissionRequest {
1319            tool_name: "Bash".to_string(),
1320            input: serde_json::json!({"command": "npm test"}),
1321            permission_suggestions: vec![],
1322            blocked_path: None,
1323            decision_reason: None,
1324            tool_use_id: None,
1325        };
1326
1327        let response =
1328            req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1329        let message: ControlResponseMessage = response.into();
1330        let json = serde_json::to_string(&message).unwrap();
1331
1332        assert!(json.contains("\"type\":\"control_response\""));
1333        assert!(json.contains("\"behavior\":\"allow\""));
1334        assert!(json.contains("\"updatedPermissions\""));
1335        assert!(json.contains("\"toolName\":\"Bash\""));
1336    }
1337
1338    #[test]
1339    fn test_tool_permission_request_allow_and_remember_suggestion() {
1340        let req = ToolPermissionRequest {
1341            tool_name: "Bash".to_string(),
1342            input: serde_json::json!({"command": "npm test"}),
1343            permission_suggestions: vec![PermissionSuggestion {
1344                suggestion_type: PermissionType::SetMode,
1345                destination: PermissionDestination::Session,
1346                mode: Some(PermissionModeName::AcceptEdits),
1347                behavior: None,
1348                rules: None,
1349            }],
1350            blocked_path: None,
1351            decision_reason: None,
1352            tool_use_id: None,
1353        };
1354
1355        let response = req.allow_and_remember_suggestion("req-123");
1356        assert!(response.is_some());
1357
1358        let message: ControlResponseMessage = response.unwrap().into();
1359        let json = serde_json::to_string(&message).unwrap();
1360
1361        assert!(json.contains("\"type\":\"setMode\""));
1362        assert!(json.contains("\"mode\":\"acceptEdits\""));
1363    }
1364
1365    #[test]
1366    fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1367        let req = ToolPermissionRequest {
1368            tool_name: "Bash".to_string(),
1369            input: serde_json::json!({"command": "npm test"}),
1370            permission_suggestions: vec![], // No suggestions
1371            blocked_path: None,
1372            decision_reason: None,
1373            tool_use_id: None,
1374        };
1375
1376        let response = req.allow_and_remember_suggestion("req-123");
1377        assert!(response.is_none());
1378    }
1379}