Skip to main content

claude_codes/io/
control.rs

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