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/// SDK control message to gracefully interrupt a running Claude session.
846///
847/// When written to the CLI subprocess's stdin, this tells Claude to stop its
848/// current response and return control to the caller without killing the session.
849///
850/// This corresponds to the TypeScript SDK's `SDKControlInterruptRequest` type
851/// and is distinct from closing or aborting the subprocess.
852///
853/// # Example
854///
855/// ```
856/// use claude_codes::SDKControlInterruptRequest;
857///
858/// let interrupt = SDKControlInterruptRequest::new();
859/// let json = serde_json::to_string(&interrupt).unwrap();
860/// assert_eq!(json, r#"{"subtype":"interrupt"}"#);
861/// ```
862#[derive(Debug, Clone, Serialize, Deserialize)]
863pub struct SDKControlInterruptRequest {
864    subtype: SDKControlInterruptSubtype,
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize)]
868enum SDKControlInterruptSubtype {
869    #[serde(rename = "interrupt")]
870    Interrupt,
871}
872
873impl SDKControlInterruptRequest {
874    /// Create a new interrupt request.
875    pub fn new() -> Self {
876        SDKControlInterruptRequest {
877            subtype: SDKControlInterruptSubtype::Interrupt,
878        }
879    }
880}
881
882impl Default for SDKControlInterruptRequest {
883    fn default() -> Self {
884        Self::new()
885    }
886}
887
888/// Wrapper for outgoing control requests (includes type tag)
889#[derive(Debug, Clone, Serialize, Deserialize)]
890pub struct ControlRequestMessage {
891    #[serde(rename = "type")]
892    pub message_type: String,
893    pub request_id: String,
894    pub request: ControlRequestPayload,
895}
896
897impl ControlRequestMessage {
898    /// Create an initialization request to send to CLI
899    pub fn initialize(request_id: impl Into<String>) -> Self {
900        ControlRequestMessage {
901            message_type: "control_request".to_string(),
902            request_id: request_id.into(),
903            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
904        }
905    }
906
907    /// Create an initialization request with hooks configuration
908    pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
909        ControlRequestMessage {
910            message_type: "control_request".to_string(),
911            request_id: request_id.into(),
912            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
913        }
914    }
915}
916
917#[cfg(test)]
918mod tests {
919    use super::*;
920    use crate::io::ClaudeOutput;
921
922    #[test]
923    fn test_deserialize_control_request_can_use_tool() {
924        let json = r#"{
925            "type": "control_request",
926            "request_id": "perm-abc123",
927            "request": {
928                "subtype": "can_use_tool",
929                "tool_name": "Write",
930                "input": {
931                    "file_path": "/home/user/hello.py",
932                    "content": "print('hello')"
933                },
934                "permission_suggestions": [],
935                "blocked_path": null
936            }
937        }"#;
938
939        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
940        assert!(output.is_control_request());
941
942        if let ClaudeOutput::ControlRequest(req) = output {
943            assert_eq!(req.request_id, "perm-abc123");
944            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
945                assert_eq!(perm_req.tool_name, "Write");
946                assert_eq!(
947                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
948                    "/home/user/hello.py"
949                );
950            } else {
951                panic!("Expected CanUseTool payload");
952            }
953        } else {
954            panic!("Expected ControlRequest");
955        }
956    }
957
958    #[test]
959    fn test_deserialize_control_request_edit_tool_real() {
960        // Real production message from Claude CLI
961        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"}}"#;
962
963        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
964        assert!(output.is_control_request());
965        assert_eq!(output.message_type(), "control_request");
966
967        if let ClaudeOutput::ControlRequest(req) = output {
968            assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
969            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
970                assert_eq!(perm_req.tool_name, "Edit");
971                assert_eq!(
972                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
973                    "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
974                );
975                assert!(perm_req.input.get("old_string").is_some());
976                assert!(perm_req.input.get("new_string").is_some());
977                assert!(!perm_req
978                    .input
979                    .get("replace_all")
980                    .unwrap()
981                    .as_bool()
982                    .unwrap());
983            } else {
984                panic!("Expected CanUseTool payload");
985            }
986        } else {
987            panic!("Expected ControlRequest");
988        }
989    }
990
991    #[test]
992    fn test_tool_permission_request_allow() {
993        let req = ToolPermissionRequest {
994            tool_name: "Read".to_string(),
995            input: serde_json::json!({"file_path": "/tmp/test.txt"}),
996            permission_suggestions: vec![],
997            blocked_path: None,
998            decision_reason: None,
999            tool_use_id: None,
1000        };
1001
1002        let response = req.allow("req-123");
1003        let message: ControlResponseMessage = response.into();
1004
1005        let json = serde_json::to_string(&message).unwrap();
1006        assert!(json.contains("\"type\":\"control_response\""));
1007        assert!(json.contains("\"subtype\":\"success\""));
1008        assert!(json.contains("\"request_id\":\"req-123\""));
1009        assert!(json.contains("\"behavior\":\"allow\""));
1010        assert!(json.contains("\"updatedInput\""));
1011    }
1012
1013    #[test]
1014    fn test_tool_permission_request_allow_with_modified_input() {
1015        let req = ToolPermissionRequest {
1016            tool_name: "Write".to_string(),
1017            input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1018            permission_suggestions: vec![],
1019            blocked_path: None,
1020            decision_reason: None,
1021            tool_use_id: None,
1022        };
1023
1024        let modified_input = serde_json::json!({
1025            "file_path": "/tmp/safe/passwd",
1026            "content": "test"
1027        });
1028        let response = req.allow_with(modified_input, "req-456");
1029        let message: ControlResponseMessage = response.into();
1030
1031        let json = serde_json::to_string(&message).unwrap();
1032        assert!(json.contains("/tmp/safe/passwd"));
1033        assert!(!json.contains("/etc/passwd"));
1034    }
1035
1036    #[test]
1037    fn test_tool_permission_request_deny() {
1038        let req = ToolPermissionRequest {
1039            tool_name: "Bash".to_string(),
1040            input: serde_json::json!({"command": "sudo rm -rf /"}),
1041            permission_suggestions: vec![],
1042            blocked_path: None,
1043            decision_reason: None,
1044            tool_use_id: None,
1045        };
1046
1047        let response = req.deny("Dangerous command blocked", "req-789");
1048        let message: ControlResponseMessage = response.into();
1049
1050        let json = serde_json::to_string(&message).unwrap();
1051        assert!(json.contains("\"behavior\":\"deny\""));
1052        assert!(json.contains("Dangerous command blocked"));
1053        assert!(!json.contains("\"interrupt\":true"));
1054    }
1055
1056    #[test]
1057    fn test_tool_permission_request_deny_and_stop() {
1058        let req = ToolPermissionRequest {
1059            tool_name: "Bash".to_string(),
1060            input: serde_json::json!({"command": "rm -rf /"}),
1061            permission_suggestions: vec![],
1062            blocked_path: None,
1063            decision_reason: None,
1064            tool_use_id: None,
1065        };
1066
1067        let response = req.deny_and_stop("Security violation", "req-000");
1068        let message: ControlResponseMessage = response.into();
1069
1070        let json = serde_json::to_string(&message).unwrap();
1071        assert!(json.contains("\"behavior\":\"deny\""));
1072        assert!(json.contains("\"interrupt\":true"));
1073    }
1074
1075    #[test]
1076    fn test_permission_result_serialization() {
1077        // Test allow
1078        let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1079        let json = serde_json::to_string(&allow).unwrap();
1080        assert!(json.contains("\"behavior\":\"allow\""));
1081        assert!(json.contains("\"updatedInput\""));
1082
1083        // Test deny
1084        let deny = PermissionResult::deny("Not allowed");
1085        let json = serde_json::to_string(&deny).unwrap();
1086        assert!(json.contains("\"behavior\":\"deny\""));
1087        assert!(json.contains("\"message\":\"Not allowed\""));
1088        assert!(!json.contains("\"interrupt\""));
1089
1090        // Test deny with interrupt
1091        let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1092        let json = serde_json::to_string(&deny_stop).unwrap();
1093        assert!(json.contains("\"interrupt\":true"));
1094    }
1095
1096    #[test]
1097    fn test_control_request_message_initialize() {
1098        let init = ControlRequestMessage::initialize("init-1");
1099
1100        let json = serde_json::to_string(&init).unwrap();
1101        assert!(json.contains("\"type\":\"control_request\""));
1102        assert!(json.contains("\"request_id\":\"init-1\""));
1103        assert!(json.contains("\"subtype\":\"initialize\""));
1104    }
1105
1106    #[test]
1107    fn test_control_response_error() {
1108        let response = ControlResponse::error("req-err", "Something went wrong");
1109        let message: ControlResponseMessage = response.into();
1110
1111        let json = serde_json::to_string(&message).unwrap();
1112        assert!(json.contains("\"subtype\":\"error\""));
1113        assert!(json.contains("\"error\":\"Something went wrong\""));
1114    }
1115
1116    #[test]
1117    fn test_roundtrip_control_request() {
1118        let original_json = r#"{
1119            "type": "control_request",
1120            "request_id": "test-123",
1121            "request": {
1122                "subtype": "can_use_tool",
1123                "tool_name": "Bash",
1124                "input": {"command": "ls -la"},
1125                "permission_suggestions": []
1126            }
1127        }"#;
1128
1129        let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1130
1131        let reserialized = serde_json::to_string(&output).unwrap();
1132        assert!(reserialized.contains("control_request"));
1133        assert!(reserialized.contains("test-123"));
1134        assert!(reserialized.contains("Bash"));
1135    }
1136
1137    #[test]
1138    fn test_permission_suggestions_parsing() {
1139        let json = r#"{
1140            "type": "control_request",
1141            "request_id": "perm-456",
1142            "request": {
1143                "subtype": "can_use_tool",
1144                "tool_name": "Bash",
1145                "input": {"command": "npm test"},
1146                "permission_suggestions": [
1147                    {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1148                    {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1149                ]
1150            }
1151        }"#;
1152
1153        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1154        if let ClaudeOutput::ControlRequest(req) = output {
1155            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1156                assert_eq!(perm_req.permission_suggestions.len(), 2);
1157                assert_eq!(
1158                    perm_req.permission_suggestions[0].suggestion_type,
1159                    PermissionType::SetMode
1160                );
1161                assert_eq!(
1162                    perm_req.permission_suggestions[0].mode,
1163                    Some(PermissionModeName::AcceptEdits)
1164                );
1165                assert_eq!(
1166                    perm_req.permission_suggestions[0].destination,
1167                    PermissionDestination::Session
1168                );
1169                assert_eq!(
1170                    perm_req.permission_suggestions[1].suggestion_type,
1171                    PermissionType::SetMode
1172                );
1173                assert_eq!(
1174                    perm_req.permission_suggestions[1].mode,
1175                    Some(PermissionModeName::BypassPermissions)
1176                );
1177                assert_eq!(
1178                    perm_req.permission_suggestions[1].destination,
1179                    PermissionDestination::Project
1180                );
1181            } else {
1182                panic!("Expected CanUseTool payload");
1183            }
1184        } else {
1185            panic!("Expected ControlRequest");
1186        }
1187    }
1188
1189    #[test]
1190    fn test_permission_suggestion_set_mode_roundtrip() {
1191        let suggestion = PermissionSuggestion {
1192            suggestion_type: PermissionType::SetMode,
1193            destination: PermissionDestination::Session,
1194            mode: Some(PermissionModeName::AcceptEdits),
1195            behavior: None,
1196            rules: None,
1197        };
1198
1199        let json = serde_json::to_string(&suggestion).unwrap();
1200        assert!(json.contains("\"type\":\"setMode\""));
1201        assert!(json.contains("\"mode\":\"acceptEdits\""));
1202        assert!(json.contains("\"destination\":\"session\""));
1203        assert!(!json.contains("\"behavior\""));
1204        assert!(!json.contains("\"rules\""));
1205
1206        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1207        assert_eq!(parsed, suggestion);
1208    }
1209
1210    #[test]
1211    fn test_permission_suggestion_add_rules_roundtrip() {
1212        let suggestion = PermissionSuggestion {
1213            suggestion_type: PermissionType::AddRules,
1214            destination: PermissionDestination::Session,
1215            mode: None,
1216            behavior: Some(PermissionBehavior::Allow),
1217            rules: Some(vec![serde_json::json!({
1218                "toolName": "Read",
1219                "ruleContent": "//tmp/**"
1220            })]),
1221        };
1222
1223        let json = serde_json::to_string(&suggestion).unwrap();
1224        assert!(json.contains("\"type\":\"addRules\""));
1225        assert!(json.contains("\"behavior\":\"allow\""));
1226        assert!(json.contains("\"destination\":\"session\""));
1227        assert!(json.contains("\"rules\""));
1228        assert!(json.contains("\"toolName\":\"Read\""));
1229        assert!(!json.contains("\"mode\""));
1230
1231        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1232        assert_eq!(parsed, suggestion);
1233    }
1234
1235    #[test]
1236    fn test_permission_suggestion_add_rules_from_real_json() {
1237        let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1238
1239        let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1240        assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1241        assert_eq!(parsed.destination, PermissionDestination::Session);
1242        assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1243        assert!(parsed.rules.is_some());
1244        assert!(parsed.mode.is_none());
1245    }
1246
1247    #[test]
1248    fn test_permission_allow_tool() {
1249        let perm = Permission::allow_tool("Bash", "npm test");
1250
1251        assert_eq!(perm.permission_type, PermissionType::AddRules);
1252        assert_eq!(perm.destination, PermissionDestination::Session);
1253        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1254        assert!(perm.mode.is_none());
1255
1256        let rules = perm.rules.unwrap();
1257        assert_eq!(rules.len(), 1);
1258        assert_eq!(rules[0].tool_name, "Bash");
1259        assert_eq!(rules[0].rule_content, "npm test");
1260    }
1261
1262    #[test]
1263    fn test_permission_allow_tool_with_destination() {
1264        let perm = Permission::allow_tool_with_destination(
1265            "Read",
1266            "/tmp/**",
1267            PermissionDestination::Project,
1268        );
1269
1270        assert_eq!(perm.permission_type, PermissionType::AddRules);
1271        assert_eq!(perm.destination, PermissionDestination::Project);
1272        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1273
1274        let rules = perm.rules.unwrap();
1275        assert_eq!(rules[0].tool_name, "Read");
1276        assert_eq!(rules[0].rule_content, "/tmp/**");
1277    }
1278
1279    #[test]
1280    fn test_permission_set_mode() {
1281        let perm = Permission::set_mode(
1282            PermissionModeName::AcceptEdits,
1283            PermissionDestination::Session,
1284        );
1285
1286        assert_eq!(perm.permission_type, PermissionType::SetMode);
1287        assert_eq!(perm.destination, PermissionDestination::Session);
1288        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1289        assert!(perm.behavior.is_none());
1290        assert!(perm.rules.is_none());
1291    }
1292
1293    #[test]
1294    fn test_permission_serialization() {
1295        let perm = Permission::allow_tool("Bash", "npm test");
1296        let json = serde_json::to_string(&perm).unwrap();
1297
1298        assert!(json.contains("\"type\":\"addRules\""));
1299        assert!(json.contains("\"destination\":\"session\""));
1300        assert!(json.contains("\"behavior\":\"allow\""));
1301        assert!(json.contains("\"toolName\":\"Bash\""));
1302        assert!(json.contains("\"ruleContent\":\"npm test\""));
1303    }
1304
1305    #[test]
1306    fn test_permission_from_suggestion_set_mode() {
1307        let suggestion = PermissionSuggestion {
1308            suggestion_type: PermissionType::SetMode,
1309            destination: PermissionDestination::Session,
1310            mode: Some(PermissionModeName::AcceptEdits),
1311            behavior: None,
1312            rules: None,
1313        };
1314
1315        let perm = Permission::from_suggestion(&suggestion);
1316
1317        assert_eq!(perm.permission_type, PermissionType::SetMode);
1318        assert_eq!(perm.destination, PermissionDestination::Session);
1319        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1320    }
1321
1322    #[test]
1323    fn test_permission_from_suggestion_add_rules() {
1324        let suggestion = PermissionSuggestion {
1325            suggestion_type: PermissionType::AddRules,
1326            destination: PermissionDestination::Session,
1327            mode: None,
1328            behavior: Some(PermissionBehavior::Allow),
1329            rules: Some(vec![serde_json::json!({
1330                "toolName": "Read",
1331                "ruleContent": "/tmp/**"
1332            })]),
1333        };
1334
1335        let perm = Permission::from_suggestion(&suggestion);
1336
1337        assert_eq!(perm.permission_type, PermissionType::AddRules);
1338        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1339
1340        let rules = perm.rules.unwrap();
1341        assert_eq!(rules.len(), 1);
1342        assert_eq!(rules[0].tool_name, "Read");
1343        assert_eq!(rules[0].rule_content, "/tmp/**");
1344    }
1345
1346    #[test]
1347    fn test_permission_result_allow_with_typed_permissions() {
1348        let result = PermissionResult::allow_with_typed_permissions(
1349            serde_json::json!({"command": "npm test"}),
1350            vec![Permission::allow_tool("Bash", "npm test")],
1351        );
1352
1353        let json = serde_json::to_string(&result).unwrap();
1354        assert!(json.contains("\"behavior\":\"allow\""));
1355        assert!(json.contains("\"updatedPermissions\""));
1356        assert!(json.contains("\"toolName\":\"Bash\""));
1357    }
1358
1359    #[test]
1360    fn test_tool_permission_request_allow_and_remember() {
1361        let req = ToolPermissionRequest {
1362            tool_name: "Bash".to_string(),
1363            input: serde_json::json!({"command": "npm test"}),
1364            permission_suggestions: vec![],
1365            blocked_path: None,
1366            decision_reason: None,
1367            tool_use_id: None,
1368        };
1369
1370        let response =
1371            req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1372        let message: ControlResponseMessage = response.into();
1373        let json = serde_json::to_string(&message).unwrap();
1374
1375        assert!(json.contains("\"type\":\"control_response\""));
1376        assert!(json.contains("\"behavior\":\"allow\""));
1377        assert!(json.contains("\"updatedPermissions\""));
1378        assert!(json.contains("\"toolName\":\"Bash\""));
1379    }
1380
1381    #[test]
1382    fn test_tool_permission_request_allow_and_remember_suggestion() {
1383        let req = ToolPermissionRequest {
1384            tool_name: "Bash".to_string(),
1385            input: serde_json::json!({"command": "npm test"}),
1386            permission_suggestions: vec![PermissionSuggestion {
1387                suggestion_type: PermissionType::SetMode,
1388                destination: PermissionDestination::Session,
1389                mode: Some(PermissionModeName::AcceptEdits),
1390                behavior: None,
1391                rules: None,
1392            }],
1393            blocked_path: None,
1394            decision_reason: None,
1395            tool_use_id: None,
1396        };
1397
1398        let response = req.allow_and_remember_suggestion("req-123");
1399        assert!(response.is_some());
1400
1401        let message: ControlResponseMessage = response.unwrap().into();
1402        let json = serde_json::to_string(&message).unwrap();
1403
1404        assert!(json.contains("\"type\":\"setMode\""));
1405        assert!(json.contains("\"mode\":\"acceptEdits\""));
1406    }
1407
1408    #[test]
1409    fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1410        let req = ToolPermissionRequest {
1411            tool_name: "Bash".to_string(),
1412            input: serde_json::json!({"command": "npm test"}),
1413            permission_suggestions: vec![], // No suggestions
1414            blocked_path: None,
1415            decision_reason: None,
1416            tool_use_id: None,
1417        };
1418
1419        let response = req.allow_and_remember_suggestion("req-123");
1420        assert!(response.is_none());
1421    }
1422}