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    /// Allow an `AskUserQuestion` permission request by supplying the user's
645    /// answers.
646    ///
647    /// The CLI echoes the `updatedInput` we return back into the
648    /// `tool_use_result` it sends to its frontend; that frontend reads
649    /// `tool_use_result.questions` and calls `questions.map(...)`. If we
650    /// returned only `{answers: …}` (the natural shape if you think of
651    /// the response as "just the new info"), the frontend would crash with
652    /// `undefined is not an object (evaluating 'q.map')`.
653    ///
654    /// This helper parses `self.input` as
655    /// [`AskUserQuestionInput`](crate::AskUserQuestionInput), attaches the
656    /// supplied `answers`, and serializes it back — preserving the
657    /// original `questions` (and any `metadata`) on the wire alongside the
658    /// new answers. Returns an error if `self.input` doesn't parse as an
659    /// `AskUserQuestionInput`; non-`AskUserQuestion` callers should keep
660    /// using [`allow`](Self::allow) / [`allow_with`](Self::allow_with).
661    ///
662    /// # Example
663    /// ```
664    /// # use claude_codes::ToolPermissionRequest;
665    /// # use serde_json::json;
666    /// # use std::collections::HashMap;
667    /// let req = ToolPermissionRequest {
668    ///     tool_name: "AskUserQuestion".to_string(),
669    ///     input: json!({"questions": [
670    ///         {"question": "?", "header": "Q", "options": [], "multiSelect": false}
671    ///     ]}),
672    ///     permission_suggestions: vec![],
673    ///     blocked_path: None,
674    ///     decision_reason: None,
675    ///     tool_use_id: None,
676    /// };
677    /// let mut answers = HashMap::new();
678    /// answers.insert("Q".into(), "yes".into());
679    /// let response = req.answer_questions(answers, "req-123").unwrap();
680    /// ```
681    pub fn answer_questions(
682        &self,
683        answers: std::collections::HashMap<String, String>,
684        request_id: &str,
685    ) -> Result<ControlResponse, serde_json::Error> {
686        let mut typed: crate::tool_inputs::AskUserQuestionInput =
687            serde_json::from_value(self.input.clone())?;
688        typed.answers = Some(answers);
689        let updated_input = serde_json::to_value(&typed)?;
690        Ok(ControlResponse::from_result(
691            request_id,
692            PermissionResult::allow(updated_input),
693        ))
694    }
695}
696
697/// Result of a permission decision
698///
699/// This type represents the decision made by the permission callback.
700/// It can be serialized directly into the control response format.
701#[derive(Debug, Clone, Serialize, Deserialize)]
702#[serde(tag = "behavior", rename_all = "snake_case")]
703pub enum PermissionResult {
704    /// Allow the tool to execute
705    Allow {
706        /// The (possibly modified) input to pass to the tool
707        #[serde(rename = "updatedInput")]
708        updated_input: Value,
709        /// Optional updated permissions list
710        #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
711        updated_permissions: Option<Vec<Value>>,
712    },
713    /// Deny the tool execution
714    Deny {
715        /// Message explaining why the tool was denied
716        message: String,
717        /// If true, stop the entire session
718        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
719        interrupt: bool,
720    },
721}
722
723impl PermissionResult {
724    /// Create an allow result with the given input
725    pub fn allow(input: Value) -> Self {
726        PermissionResult::Allow {
727            updated_input: input,
728            updated_permissions: None,
729        }
730    }
731
732    /// Create an allow result with raw permissions (as JSON Values).
733    ///
734    /// Prefer using `allow_with_typed_permissions` for type safety.
735    pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
736        PermissionResult::Allow {
737            updated_input: input,
738            updated_permissions: Some(permissions),
739        }
740    }
741
742    /// Create an allow result with typed permissions.
743    ///
744    /// This is the preferred way to grant permissions for "remember this decision"
745    /// functionality.
746    ///
747    /// # Example
748    /// ```
749    /// use claude_codes::{Permission, PermissionResult};
750    /// use serde_json::json;
751    ///
752    /// let result = PermissionResult::allow_with_typed_permissions(
753    ///     json!({"command": "npm test"}),
754    ///     vec![Permission::allow_tool("Bash", "npm test")],
755    /// );
756    /// ```
757    pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
758        let permission_values: Vec<Value> = permissions
759            .into_iter()
760            .filter_map(|p| serde_json::to_value(p).ok())
761            .collect();
762        PermissionResult::Allow {
763            updated_input: input,
764            updated_permissions: Some(permission_values),
765        }
766    }
767
768    /// Create a deny result
769    pub fn deny(message: impl Into<String>) -> Self {
770        PermissionResult::Deny {
771            message: message.into(),
772            interrupt: false,
773        }
774    }
775
776    /// Create a deny result that also interrupts the session
777    pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
778        PermissionResult::Deny {
779            message: message.into(),
780            interrupt: true,
781        }
782    }
783}
784
785/// Hook callback request
786#[derive(Debug, Clone, Serialize, Deserialize)]
787pub struct HookCallbackRequest {
788    pub callback_id: String,
789    pub input: Value,
790    #[serde(skip_serializing_if = "Option::is_none")]
791    pub tool_use_id: Option<String>,
792}
793
794/// MCP message request
795#[derive(Debug, Clone, Serialize, Deserialize)]
796pub struct McpMessageRequest {
797    pub server_name: String,
798    pub message: Value,
799}
800
801/// Initialize request (SDK -> CLI)
802#[derive(Debug, Clone, Serialize, Deserialize)]
803pub struct InitializeRequest {
804    #[serde(skip_serializing_if = "Option::is_none")]
805    pub hooks: Option<Value>,
806}
807
808/// Control response to CLI
809///
810/// Built using the ergonomic methods on [`ToolPermissionRequest`] or
811/// constructed directly for other control request types.
812#[derive(Debug, Clone, Serialize, Deserialize)]
813pub struct ControlResponse {
814    /// The request ID this response corresponds to
815    pub response: ControlResponsePayload,
816}
817
818impl ControlResponse {
819    /// Create a success response from a PermissionResult
820    ///
821    /// This is the preferred way to construct permission responses.
822    pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
823        // Serialize the PermissionResult to Value for the response
824        let response_value = serde_json::to_value(&result)
825            .expect("PermissionResult serialization should never fail");
826        ControlResponse {
827            response: ControlResponsePayload::Success {
828                request_id: request_id.to_string(),
829                response: Some(response_value),
830            },
831        }
832    }
833
834    /// Create a success response with the given payload (raw Value)
835    pub fn success(request_id: &str, response_data: Value) -> Self {
836        ControlResponse {
837            response: ControlResponsePayload::Success {
838                request_id: request_id.to_string(),
839                response: Some(response_data),
840            },
841        }
842    }
843
844    /// Create an empty success response (for acks)
845    pub fn success_empty(request_id: &str) -> Self {
846        ControlResponse {
847            response: ControlResponsePayload::Success {
848                request_id: request_id.to_string(),
849                response: None,
850            },
851        }
852    }
853
854    /// Create an error response
855    pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
856        ControlResponse {
857            response: ControlResponsePayload::Error {
858                request_id: request_id.to_string(),
859                error: error_message.into(),
860            },
861        }
862    }
863}
864
865/// Control response payload
866#[derive(Debug, Clone, Serialize, Deserialize)]
867#[serde(tag = "subtype", rename_all = "snake_case")]
868pub enum ControlResponsePayload {
869    Success {
870        request_id: String,
871        #[serde(skip_serializing_if = "Option::is_none")]
872        response: Option<Value>,
873    },
874    Error {
875        request_id: String,
876        error: String,
877    },
878}
879
880/// Wrapper for outgoing control responses (includes type tag)
881#[derive(Debug, Clone, Serialize, Deserialize)]
882pub struct ControlResponseMessage {
883    #[serde(rename = "type")]
884    pub message_type: String,
885    pub response: ControlResponsePayload,
886}
887
888impl From<ControlResponse> for ControlResponseMessage {
889    fn from(resp: ControlResponse) -> Self {
890        ControlResponseMessage {
891            message_type: "control_response".to_string(),
892            response: resp.response,
893        }
894    }
895}
896
897/// SDK control message to gracefully interrupt a running Claude session.
898///
899/// When written to the CLI subprocess's stdin, this tells Claude to stop its
900/// current response and return control to the caller without killing the session.
901///
902/// This corresponds to the TypeScript SDK's `SDKControlInterruptRequest` type
903/// and is distinct from closing or aborting the subprocess.
904///
905/// # Example
906///
907/// ```
908/// use claude_codes::SDKControlInterruptRequest;
909///
910/// let interrupt = SDKControlInterruptRequest::new();
911/// let json = serde_json::to_string(&interrupt).unwrap();
912/// assert_eq!(json, r#"{"subtype":"interrupt"}"#);
913/// ```
914#[derive(Debug, Clone, Serialize, Deserialize)]
915pub struct SDKControlInterruptRequest {
916    subtype: SDKControlInterruptSubtype,
917}
918
919#[derive(Debug, Clone, Serialize, Deserialize)]
920enum SDKControlInterruptSubtype {
921    #[serde(rename = "interrupt")]
922    Interrupt,
923}
924
925impl SDKControlInterruptRequest {
926    /// Create a new interrupt request.
927    pub fn new() -> Self {
928        SDKControlInterruptRequest {
929            subtype: SDKControlInterruptSubtype::Interrupt,
930        }
931    }
932}
933
934impl Default for SDKControlInterruptRequest {
935    fn default() -> Self {
936        Self::new()
937    }
938}
939
940/// Wrapper for outgoing control requests (includes type tag)
941#[derive(Debug, Clone, Serialize, Deserialize)]
942pub struct ControlRequestMessage {
943    #[serde(rename = "type")]
944    pub message_type: String,
945    pub request_id: String,
946    pub request: ControlRequestPayload,
947}
948
949impl ControlRequestMessage {
950    /// Create an initialization request to send to CLI
951    pub fn initialize(request_id: impl Into<String>) -> Self {
952        ControlRequestMessage {
953            message_type: "control_request".to_string(),
954            request_id: request_id.into(),
955            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
956        }
957    }
958
959    /// Create an initialization request with hooks configuration
960    pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
961        ControlRequestMessage {
962            message_type: "control_request".to_string(),
963            request_id: request_id.into(),
964            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
965        }
966    }
967}
968
969#[cfg(test)]
970mod tests {
971    use super::*;
972    use crate::io::ClaudeOutput;
973
974    #[test]
975    fn test_deserialize_control_request_can_use_tool() {
976        let json = r#"{
977            "type": "control_request",
978            "request_id": "perm-abc123",
979            "request": {
980                "subtype": "can_use_tool",
981                "tool_name": "Write",
982                "input": {
983                    "file_path": "/home/user/hello.py",
984                    "content": "print('hello')"
985                },
986                "permission_suggestions": [],
987                "blocked_path": null
988            }
989        }"#;
990
991        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
992        assert!(output.is_control_request());
993
994        if let ClaudeOutput::ControlRequest(req) = output {
995            assert_eq!(req.request_id, "perm-abc123");
996            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
997                assert_eq!(perm_req.tool_name, "Write");
998                assert_eq!(
999                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1000                    "/home/user/hello.py"
1001                );
1002            } else {
1003                panic!("Expected CanUseTool payload");
1004            }
1005        } else {
1006            panic!("Expected ControlRequest");
1007        }
1008    }
1009
1010    #[test]
1011    fn test_deserialize_control_request_edit_tool_real() {
1012        // Real production message from Claude CLI
1013        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"}}"#;
1014
1015        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1016        assert!(output.is_control_request());
1017        assert_eq!(output.message_type(), "control_request");
1018
1019        if let ClaudeOutput::ControlRequest(req) = output {
1020            assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1021            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1022                assert_eq!(perm_req.tool_name, "Edit");
1023                assert_eq!(
1024                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1025                    "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1026                );
1027                assert!(perm_req.input.get("old_string").is_some());
1028                assert!(perm_req.input.get("new_string").is_some());
1029                assert!(!perm_req
1030                    .input
1031                    .get("replace_all")
1032                    .unwrap()
1033                    .as_bool()
1034                    .unwrap());
1035            } else {
1036                panic!("Expected CanUseTool payload");
1037            }
1038        } else {
1039            panic!("Expected ControlRequest");
1040        }
1041    }
1042
1043    #[test]
1044    fn test_tool_permission_request_allow() {
1045        let req = ToolPermissionRequest {
1046            tool_name: "Read".to_string(),
1047            input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1048            permission_suggestions: vec![],
1049            blocked_path: None,
1050            decision_reason: None,
1051            tool_use_id: None,
1052        };
1053
1054        let response = req.allow("req-123");
1055        let message: ControlResponseMessage = response.into();
1056
1057        let json = serde_json::to_string(&message).unwrap();
1058        assert!(json.contains("\"type\":\"control_response\""));
1059        assert!(json.contains("\"subtype\":\"success\""));
1060        assert!(json.contains("\"request_id\":\"req-123\""));
1061        assert!(json.contains("\"behavior\":\"allow\""));
1062        assert!(json.contains("\"updatedInput\""));
1063    }
1064
1065    #[test]
1066    fn test_tool_permission_request_allow_with_modified_input() {
1067        let req = ToolPermissionRequest {
1068            tool_name: "Write".to_string(),
1069            input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1070            permission_suggestions: vec![],
1071            blocked_path: None,
1072            decision_reason: None,
1073            tool_use_id: None,
1074        };
1075
1076        let modified_input = serde_json::json!({
1077            "file_path": "/tmp/safe/passwd",
1078            "content": "test"
1079        });
1080        let response = req.allow_with(modified_input, "req-456");
1081        let message: ControlResponseMessage = response.into();
1082
1083        let json = serde_json::to_string(&message).unwrap();
1084        assert!(json.contains("/tmp/safe/passwd"));
1085        assert!(!json.contains("/etc/passwd"));
1086    }
1087
1088    #[test]
1089    fn test_tool_permission_request_deny() {
1090        let req = ToolPermissionRequest {
1091            tool_name: "Bash".to_string(),
1092            input: serde_json::json!({"command": "sudo rm -rf /"}),
1093            permission_suggestions: vec![],
1094            blocked_path: None,
1095            decision_reason: None,
1096            tool_use_id: None,
1097        };
1098
1099        let response = req.deny("Dangerous command blocked", "req-789");
1100        let message: ControlResponseMessage = response.into();
1101
1102        let json = serde_json::to_string(&message).unwrap();
1103        assert!(json.contains("\"behavior\":\"deny\""));
1104        assert!(json.contains("Dangerous command blocked"));
1105        assert!(!json.contains("\"interrupt\":true"));
1106    }
1107
1108    #[test]
1109    fn test_tool_permission_request_deny_and_stop() {
1110        let req = ToolPermissionRequest {
1111            tool_name: "Bash".to_string(),
1112            input: serde_json::json!({"command": "rm -rf /"}),
1113            permission_suggestions: vec![],
1114            blocked_path: None,
1115            decision_reason: None,
1116            tool_use_id: None,
1117        };
1118
1119        let response = req.deny_and_stop("Security violation", "req-000");
1120        let message: ControlResponseMessage = response.into();
1121
1122        let json = serde_json::to_string(&message).unwrap();
1123        assert!(json.contains("\"behavior\":\"deny\""));
1124        assert!(json.contains("\"interrupt\":true"));
1125    }
1126
1127    #[test]
1128    fn test_permission_result_serialization() {
1129        // Test allow
1130        let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1131        let json = serde_json::to_string(&allow).unwrap();
1132        assert!(json.contains("\"behavior\":\"allow\""));
1133        assert!(json.contains("\"updatedInput\""));
1134
1135        // Test deny
1136        let deny = PermissionResult::deny("Not allowed");
1137        let json = serde_json::to_string(&deny).unwrap();
1138        assert!(json.contains("\"behavior\":\"deny\""));
1139        assert!(json.contains("\"message\":\"Not allowed\""));
1140        assert!(!json.contains("\"interrupt\""));
1141
1142        // Test deny with interrupt
1143        let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1144        let json = serde_json::to_string(&deny_stop).unwrap();
1145        assert!(json.contains("\"interrupt\":true"));
1146    }
1147
1148    #[test]
1149    fn test_control_request_message_initialize() {
1150        let init = ControlRequestMessage::initialize("init-1");
1151
1152        let json = serde_json::to_string(&init).unwrap();
1153        assert!(json.contains("\"type\":\"control_request\""));
1154        assert!(json.contains("\"request_id\":\"init-1\""));
1155        assert!(json.contains("\"subtype\":\"initialize\""));
1156    }
1157
1158    #[test]
1159    fn test_control_response_error() {
1160        let response = ControlResponse::error("req-err", "Something went wrong");
1161        let message: ControlResponseMessage = response.into();
1162
1163        let json = serde_json::to_string(&message).unwrap();
1164        assert!(json.contains("\"subtype\":\"error\""));
1165        assert!(json.contains("\"error\":\"Something went wrong\""));
1166    }
1167
1168    #[test]
1169    fn test_roundtrip_control_request() {
1170        let original_json = r#"{
1171            "type": "control_request",
1172            "request_id": "test-123",
1173            "request": {
1174                "subtype": "can_use_tool",
1175                "tool_name": "Bash",
1176                "input": {"command": "ls -la"},
1177                "permission_suggestions": []
1178            }
1179        }"#;
1180
1181        let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1182
1183        let reserialized = serde_json::to_string(&output).unwrap();
1184        assert!(reserialized.contains("control_request"));
1185        assert!(reserialized.contains("test-123"));
1186        assert!(reserialized.contains("Bash"));
1187    }
1188
1189    #[test]
1190    fn test_permission_suggestions_parsing() {
1191        let json = r#"{
1192            "type": "control_request",
1193            "request_id": "perm-456",
1194            "request": {
1195                "subtype": "can_use_tool",
1196                "tool_name": "Bash",
1197                "input": {"command": "npm test"},
1198                "permission_suggestions": [
1199                    {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1200                    {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1201                ]
1202            }
1203        }"#;
1204
1205        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1206        if let ClaudeOutput::ControlRequest(req) = output {
1207            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1208                assert_eq!(perm_req.permission_suggestions.len(), 2);
1209                assert_eq!(
1210                    perm_req.permission_suggestions[0].suggestion_type,
1211                    PermissionType::SetMode
1212                );
1213                assert_eq!(
1214                    perm_req.permission_suggestions[0].mode,
1215                    Some(PermissionModeName::AcceptEdits)
1216                );
1217                assert_eq!(
1218                    perm_req.permission_suggestions[0].destination,
1219                    PermissionDestination::Session
1220                );
1221                assert_eq!(
1222                    perm_req.permission_suggestions[1].suggestion_type,
1223                    PermissionType::SetMode
1224                );
1225                assert_eq!(
1226                    perm_req.permission_suggestions[1].mode,
1227                    Some(PermissionModeName::BypassPermissions)
1228                );
1229                assert_eq!(
1230                    perm_req.permission_suggestions[1].destination,
1231                    PermissionDestination::Project
1232                );
1233            } else {
1234                panic!("Expected CanUseTool payload");
1235            }
1236        } else {
1237            panic!("Expected ControlRequest");
1238        }
1239    }
1240
1241    #[test]
1242    fn test_permission_suggestion_set_mode_roundtrip() {
1243        let suggestion = PermissionSuggestion {
1244            suggestion_type: PermissionType::SetMode,
1245            destination: PermissionDestination::Session,
1246            mode: Some(PermissionModeName::AcceptEdits),
1247            behavior: None,
1248            rules: None,
1249        };
1250
1251        let json = serde_json::to_string(&suggestion).unwrap();
1252        assert!(json.contains("\"type\":\"setMode\""));
1253        assert!(json.contains("\"mode\":\"acceptEdits\""));
1254        assert!(json.contains("\"destination\":\"session\""));
1255        assert!(!json.contains("\"behavior\""));
1256        assert!(!json.contains("\"rules\""));
1257
1258        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1259        assert_eq!(parsed, suggestion);
1260    }
1261
1262    #[test]
1263    fn test_permission_suggestion_add_rules_roundtrip() {
1264        let suggestion = PermissionSuggestion {
1265            suggestion_type: PermissionType::AddRules,
1266            destination: PermissionDestination::Session,
1267            mode: None,
1268            behavior: Some(PermissionBehavior::Allow),
1269            rules: Some(vec![serde_json::json!({
1270                "toolName": "Read",
1271                "ruleContent": "//tmp/**"
1272            })]),
1273        };
1274
1275        let json = serde_json::to_string(&suggestion).unwrap();
1276        assert!(json.contains("\"type\":\"addRules\""));
1277        assert!(json.contains("\"behavior\":\"allow\""));
1278        assert!(json.contains("\"destination\":\"session\""));
1279        assert!(json.contains("\"rules\""));
1280        assert!(json.contains("\"toolName\":\"Read\""));
1281        assert!(!json.contains("\"mode\""));
1282
1283        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1284        assert_eq!(parsed, suggestion);
1285    }
1286
1287    #[test]
1288    fn test_permission_suggestion_add_rules_from_real_json() {
1289        let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1290
1291        let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1292        assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1293        assert_eq!(parsed.destination, PermissionDestination::Session);
1294        assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1295        assert!(parsed.rules.is_some());
1296        assert!(parsed.mode.is_none());
1297    }
1298
1299    #[test]
1300    fn test_permission_allow_tool() {
1301        let perm = Permission::allow_tool("Bash", "npm test");
1302
1303        assert_eq!(perm.permission_type, PermissionType::AddRules);
1304        assert_eq!(perm.destination, PermissionDestination::Session);
1305        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1306        assert!(perm.mode.is_none());
1307
1308        let rules = perm.rules.unwrap();
1309        assert_eq!(rules.len(), 1);
1310        assert_eq!(rules[0].tool_name, "Bash");
1311        assert_eq!(rules[0].rule_content, "npm test");
1312    }
1313
1314    #[test]
1315    fn test_permission_allow_tool_with_destination() {
1316        let perm = Permission::allow_tool_with_destination(
1317            "Read",
1318            "/tmp/**",
1319            PermissionDestination::Project,
1320        );
1321
1322        assert_eq!(perm.permission_type, PermissionType::AddRules);
1323        assert_eq!(perm.destination, PermissionDestination::Project);
1324        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1325
1326        let rules = perm.rules.unwrap();
1327        assert_eq!(rules[0].tool_name, "Read");
1328        assert_eq!(rules[0].rule_content, "/tmp/**");
1329    }
1330
1331    #[test]
1332    fn test_permission_set_mode() {
1333        let perm = Permission::set_mode(
1334            PermissionModeName::AcceptEdits,
1335            PermissionDestination::Session,
1336        );
1337
1338        assert_eq!(perm.permission_type, PermissionType::SetMode);
1339        assert_eq!(perm.destination, PermissionDestination::Session);
1340        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1341        assert!(perm.behavior.is_none());
1342        assert!(perm.rules.is_none());
1343    }
1344
1345    #[test]
1346    fn test_permission_serialization() {
1347        let perm = Permission::allow_tool("Bash", "npm test");
1348        let json = serde_json::to_string(&perm).unwrap();
1349
1350        assert!(json.contains("\"type\":\"addRules\""));
1351        assert!(json.contains("\"destination\":\"session\""));
1352        assert!(json.contains("\"behavior\":\"allow\""));
1353        assert!(json.contains("\"toolName\":\"Bash\""));
1354        assert!(json.contains("\"ruleContent\":\"npm test\""));
1355    }
1356
1357    #[test]
1358    fn test_permission_from_suggestion_set_mode() {
1359        let suggestion = PermissionSuggestion {
1360            suggestion_type: PermissionType::SetMode,
1361            destination: PermissionDestination::Session,
1362            mode: Some(PermissionModeName::AcceptEdits),
1363            behavior: None,
1364            rules: None,
1365        };
1366
1367        let perm = Permission::from_suggestion(&suggestion);
1368
1369        assert_eq!(perm.permission_type, PermissionType::SetMode);
1370        assert_eq!(perm.destination, PermissionDestination::Session);
1371        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1372    }
1373
1374    #[test]
1375    fn test_permission_from_suggestion_add_rules() {
1376        let suggestion = PermissionSuggestion {
1377            suggestion_type: PermissionType::AddRules,
1378            destination: PermissionDestination::Session,
1379            mode: None,
1380            behavior: Some(PermissionBehavior::Allow),
1381            rules: Some(vec![serde_json::json!({
1382                "toolName": "Read",
1383                "ruleContent": "/tmp/**"
1384            })]),
1385        };
1386
1387        let perm = Permission::from_suggestion(&suggestion);
1388
1389        assert_eq!(perm.permission_type, PermissionType::AddRules);
1390        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1391
1392        let rules = perm.rules.unwrap();
1393        assert_eq!(rules.len(), 1);
1394        assert_eq!(rules[0].tool_name, "Read");
1395        assert_eq!(rules[0].rule_content, "/tmp/**");
1396    }
1397
1398    #[test]
1399    fn test_permission_result_allow_with_typed_permissions() {
1400        let result = PermissionResult::allow_with_typed_permissions(
1401            serde_json::json!({"command": "npm test"}),
1402            vec![Permission::allow_tool("Bash", "npm test")],
1403        );
1404
1405        let json = serde_json::to_string(&result).unwrap();
1406        assert!(json.contains("\"behavior\":\"allow\""));
1407        assert!(json.contains("\"updatedPermissions\""));
1408        assert!(json.contains("\"toolName\":\"Bash\""));
1409    }
1410
1411    #[test]
1412    fn test_tool_permission_request_allow_and_remember() {
1413        let req = ToolPermissionRequest {
1414            tool_name: "Bash".to_string(),
1415            input: serde_json::json!({"command": "npm test"}),
1416            permission_suggestions: vec![],
1417            blocked_path: None,
1418            decision_reason: None,
1419            tool_use_id: None,
1420        };
1421
1422        let response =
1423            req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1424        let message: ControlResponseMessage = response.into();
1425        let json = serde_json::to_string(&message).unwrap();
1426
1427        assert!(json.contains("\"type\":\"control_response\""));
1428        assert!(json.contains("\"behavior\":\"allow\""));
1429        assert!(json.contains("\"updatedPermissions\""));
1430        assert!(json.contains("\"toolName\":\"Bash\""));
1431    }
1432
1433    #[test]
1434    fn test_tool_permission_request_allow_and_remember_suggestion() {
1435        let req = ToolPermissionRequest {
1436            tool_name: "Bash".to_string(),
1437            input: serde_json::json!({"command": "npm test"}),
1438            permission_suggestions: vec![PermissionSuggestion {
1439                suggestion_type: PermissionType::SetMode,
1440                destination: PermissionDestination::Session,
1441                mode: Some(PermissionModeName::AcceptEdits),
1442                behavior: None,
1443                rules: None,
1444            }],
1445            blocked_path: None,
1446            decision_reason: None,
1447            tool_use_id: None,
1448        };
1449
1450        let response = req.allow_and_remember_suggestion("req-123");
1451        assert!(response.is_some());
1452
1453        let message: ControlResponseMessage = response.unwrap().into();
1454        let json = serde_json::to_string(&message).unwrap();
1455
1456        assert!(json.contains("\"type\":\"setMode\""));
1457        assert!(json.contains("\"mode\":\"acceptEdits\""));
1458    }
1459
1460    #[test]
1461    fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1462        let req = ToolPermissionRequest {
1463            tool_name: "Bash".to_string(),
1464            input: serde_json::json!({"command": "npm test"}),
1465            permission_suggestions: vec![], // No suggestions
1466            blocked_path: None,
1467            decision_reason: None,
1468            tool_use_id: None,
1469        };
1470
1471        let response = req.allow_and_remember_suggestion("req-123");
1472        assert!(response.is_none());
1473    }
1474
1475    /// Reproducer for the bug where a downstream viewer/frontend rendering
1476    /// an `AskUserQuestion` response crashes with
1477    /// `undefined is not an object (evaluating 'q.map')`. The frontend reads
1478    /// `tool_use_result.questions` (which the CLI populates by echoing the
1479    /// `updatedInput` we returned in the `Allow` response) and calls
1480    /// `questions.map(...)`. When `updatedInput` omits the `questions`
1481    /// array — which is the natural shape if a caller just supplies an
1482    /// `{answers: …}` payload — the field is missing and `q.map` blows up.
1483    ///
1484    /// This test asserts that the convenience for answering questions
1485    /// preserves the original `questions` list alongside the user's
1486    /// answers, so the wire payload has both fields.
1487    #[test]
1488    fn test_ask_user_question_answer_preserves_questions_in_updated_input() {
1489        let req = ToolPermissionRequest {
1490            tool_name: "AskUserQuestion".to_string(),
1491            input: serde_json::json!({
1492                "questions": [{
1493                    "question": "Which color do you prefer?",
1494                    "header": "Color",
1495                    "options": [
1496                        {"label": "Red", "description": "A warm color"},
1497                        {"label": "Blue", "description": "A cool color"},
1498                    ],
1499                    "multiSelect": false,
1500                }],
1501            }),
1502            permission_suggestions: vec![],
1503            blocked_path: None,
1504            decision_reason: None,
1505            tool_use_id: None,
1506        };
1507
1508        let mut answers = std::collections::HashMap::new();
1509        answers.insert("Color".to_string(), "Blue".to_string());
1510
1511        let response = req
1512            .answer_questions(answers, "req-q1")
1513            .expect("AskUserQuestion input round-trips through the typed helper");
1514        let wire = serde_json::to_value(&response).expect("ControlResponse serializes");
1515
1516        // ControlResponse nests the PermissionResult value at
1517        // `response.response`; the PermissionResult itself carries
1518        // `behavior: "allow"` + `updatedInput`.
1519        let updated_input = &wire["response"]["response"]["updatedInput"];
1520        assert_eq!(
1521            updated_input["questions"][0]["header"], "Color",
1522            "questions array must round-trip in updatedInput so the \
1523             frontend's `questions.map(...)` doesn't fault on undefined"
1524        );
1525        assert_eq!(updated_input["answers"]["Color"], "Blue");
1526    }
1527}