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    /// Parse a successful response payload as the CLI `get_usage` response.
924    pub fn get_usage_response(&self) -> Option<Result<GetUsageResponse, serde_json::Error>> {
925        match &self.response {
926            ControlResponsePayload::Success {
927                response: Some(value),
928                ..
929            } => Some(serde_json::from_value(value.clone())),
930            _ => None,
931        }
932    }
933}
934
935/// Control response payload
936#[derive(Debug, Clone, Serialize, Deserialize)]
937#[serde(tag = "subtype", rename_all = "snake_case")]
938pub enum ControlResponsePayload {
939    Success {
940        request_id: String,
941        #[serde(skip_serializing_if = "Option::is_none")]
942        response: Option<Value>,
943    },
944    Error {
945        request_id: String,
946        error: String,
947    },
948}
949
950/// Typed payload returned by the CLI `get_usage` control request.
951#[derive(Debug, Clone, Serialize, Deserialize)]
952pub struct GetUsageResponse {
953    pub session: UsageSession,
954    pub subscription_type: Option<String>,
955    pub rate_limits_available: bool,
956    pub rate_limits: Option<UsageRateLimits>,
957    pub behaviors: UsageBehaviors,
958    #[serde(flatten)]
959    pub extra: serde_json::Map<String, Value>,
960}
961
962#[derive(Debug, Clone, Default, Serialize, Deserialize)]
963pub struct UsageSession {
964    pub total_cost_usd: f64,
965    pub total_api_duration_ms: u64,
966    pub total_duration_ms: u64,
967    pub total_lines_added: u64,
968    pub total_lines_removed: u64,
969    pub model_usage: HashMap<String, UsageModelUsage>,
970    #[serde(flatten)]
971    pub extra: serde_json::Map<String, Value>,
972}
973
974#[derive(Debug, Clone, Default, Serialize, Deserialize)]
975#[serde(rename_all = "camelCase")]
976pub struct UsageModelUsage {
977    #[serde(default)]
978    pub input_tokens: u64,
979    #[serde(default)]
980    pub output_tokens: u64,
981    #[serde(default)]
982    pub cache_read_input_tokens: u64,
983    #[serde(default)]
984    pub cache_creation_input_tokens: u64,
985    #[serde(default)]
986    pub web_search_requests: u64,
987    #[serde(default, rename = "costUSD")]
988    pub cost_usd: f64,
989    #[serde(default)]
990    pub context_window: u64,
991    #[serde(default)]
992    pub max_output_tokens: u64,
993    #[serde(flatten)]
994    pub extra: serde_json::Map<String, Value>,
995}
996
997#[derive(Debug, Clone, Default, Serialize, Deserialize)]
998pub struct UsageRateLimits {
999    #[serde(default, skip_serializing_if = "Option::is_none")]
1000    pub five_hour: Option<UsageRateLimitWindow>,
1001    #[serde(default, skip_serializing_if = "Option::is_none")]
1002    pub seven_day: Option<UsageRateLimitWindow>,
1003    #[serde(default, skip_serializing_if = "Option::is_none")]
1004    pub seven_day_oauth_apps: Option<UsageRateLimitWindow>,
1005    #[serde(default, skip_serializing_if = "Option::is_none")]
1006    pub seven_day_opus: Option<UsageRateLimitWindow>,
1007    #[serde(default, skip_serializing_if = "Option::is_none")]
1008    pub seven_day_sonnet: Option<UsageRateLimitWindow>,
1009    #[serde(default, skip_serializing_if = "Option::is_none")]
1010    pub model_scoped: Option<Vec<ModelScopedRateLimit>>,
1011    #[serde(flatten)]
1012    pub extra: serde_json::Map<String, Value>,
1013}
1014
1015#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1016pub struct UsageRateLimitWindow {
1017    pub utilization: Option<f64>,
1018    pub resets_at: Option<String>,
1019    #[serde(flatten)]
1020    pub extra: serde_json::Map<String, Value>,
1021}
1022
1023#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1024pub struct ModelScopedRateLimit {
1025    pub display_name: String,
1026    pub utilization: Option<f64>,
1027    pub resets_at: Option<String>,
1028    #[serde(flatten)]
1029    pub extra: serde_json::Map<String, Value>,
1030}
1031
1032#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1033pub struct UsageBehaviors {
1034    pub request_count: u64,
1035    pub session_count: u64,
1036    pub behaviors: Vec<UsageBehavior>,
1037    pub agents: Value,
1038    pub skills: Value,
1039    pub plugins: Value,
1040    pub mcp_servers: Value,
1041    #[serde(flatten)]
1042    pub extra: serde_json::Map<String, Value>,
1043}
1044
1045#[derive(Debug, Clone, Serialize, Deserialize)]
1046pub struct UsageBehavior {
1047    pub key: String,
1048    pub pct: f64,
1049    pub count: u64,
1050    #[serde(flatten)]
1051    pub extra: serde_json::Map<String, Value>,
1052}
1053
1054/// Wrapper for outgoing control responses (includes type tag)
1055#[derive(Debug, Clone, Serialize, Deserialize)]
1056pub struct ControlResponseMessage {
1057    #[serde(rename = "type")]
1058    pub message_type: String,
1059    pub response: ControlResponsePayload,
1060}
1061
1062impl From<ControlResponse> for ControlResponseMessage {
1063    fn from(resp: ControlResponse) -> Self {
1064        ControlResponseMessage {
1065            message_type: "control_response".to_string(),
1066            response: resp.response,
1067        }
1068    }
1069}
1070
1071/// SDK control message to gracefully interrupt a running Claude session.
1072///
1073/// When written to the CLI subprocess's stdin, this tells Claude to stop its
1074/// current response and return control to the caller without killing the session.
1075///
1076/// This corresponds to the TypeScript SDK's `SDKControlInterruptRequest` type
1077/// and is distinct from closing or aborting the subprocess.
1078///
1079/// # Example
1080///
1081/// ```
1082/// use claude_codes::SDKControlInterruptRequest;
1083///
1084/// let interrupt = SDKControlInterruptRequest::new();
1085/// let json = serde_json::to_string(&interrupt).unwrap();
1086/// assert_eq!(json, r#"{"subtype":"interrupt"}"#);
1087/// ```
1088#[derive(Debug, Clone, Serialize, Deserialize)]
1089pub struct SDKControlInterruptRequest {
1090    subtype: SDKControlInterruptSubtype,
1091}
1092
1093#[derive(Debug, Clone, Serialize, Deserialize)]
1094enum SDKControlInterruptSubtype {
1095    #[serde(rename = "interrupt")]
1096    Interrupt,
1097}
1098
1099impl SDKControlInterruptRequest {
1100    /// Create a new interrupt request.
1101    pub fn new() -> Self {
1102        SDKControlInterruptRequest {
1103            subtype: SDKControlInterruptSubtype::Interrupt,
1104        }
1105    }
1106}
1107
1108impl Default for SDKControlInterruptRequest {
1109    fn default() -> Self {
1110        Self::new()
1111    }
1112}
1113
1114/// Wrapper for outgoing control requests (includes type tag)
1115#[derive(Debug, Clone, Serialize, Deserialize)]
1116pub struct ControlRequestMessage {
1117    #[serde(rename = "type")]
1118    pub message_type: String,
1119    pub request_id: String,
1120    pub request: ControlRequestPayload,
1121}
1122
1123impl ControlRequestMessage {
1124    /// Create an initialization request to send to CLI
1125    pub fn initialize(request_id: impl Into<String>) -> Self {
1126        ControlRequestMessage {
1127            message_type: "control_request".to_string(),
1128            request_id: request_id.into(),
1129            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
1130        }
1131    }
1132
1133    /// Create an initialization request with hooks configuration
1134    pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
1135        ControlRequestMessage {
1136            message_type: "control_request".to_string(),
1137            request_id: request_id.into(),
1138            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
1139        }
1140    }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145    use super::*;
1146    use crate::io::ClaudeOutput;
1147
1148    #[test]
1149    fn test_deserialize_control_request_can_use_tool() {
1150        let json = r#"{
1151            "type": "control_request",
1152            "request_id": "perm-abc123",
1153            "request": {
1154                "subtype": "can_use_tool",
1155                "tool_name": "Write",
1156                "input": {
1157                    "file_path": "/home/user/hello.py",
1158                    "content": "print('hello')"
1159                },
1160                "permission_suggestions": [],
1161                "blocked_path": null
1162            }
1163        }"#;
1164
1165        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1166        assert!(output.is_control_request());
1167
1168        if let ClaudeOutput::ControlRequest(req) = output {
1169            assert_eq!(req.request_id, "perm-abc123");
1170            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1171                assert_eq!(perm_req.tool_name, "Write");
1172                assert_eq!(
1173                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1174                    "/home/user/hello.py"
1175                );
1176            } else {
1177                panic!("Expected CanUseTool payload");
1178            }
1179        } else {
1180            panic!("Expected ControlRequest");
1181        }
1182    }
1183
1184    #[test]
1185    fn test_deserialize_control_request_edit_tool_real() {
1186        // Real production message from Claude CLI
1187        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"}}"#;
1188
1189        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1190        assert!(output.is_control_request());
1191        assert_eq!(output.message_type(), "control_request");
1192
1193        if let ClaudeOutput::ControlRequest(req) = output {
1194            assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1195            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1196                assert_eq!(perm_req.tool_name, "Edit");
1197                assert_eq!(
1198                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1199                    "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1200                );
1201                assert!(perm_req.input.get("old_string").is_some());
1202                assert!(perm_req.input.get("new_string").is_some());
1203                assert!(!perm_req
1204                    .input
1205                    .get("replace_all")
1206                    .unwrap()
1207                    .as_bool()
1208                    .unwrap());
1209            } else {
1210                panic!("Expected CanUseTool payload");
1211            }
1212        } else {
1213            panic!("Expected ControlRequest");
1214        }
1215    }
1216
1217    #[test]
1218    fn test_tool_permission_request_allow() {
1219        let req = ToolPermissionRequest {
1220            tool_name: "Read".to_string(),
1221            input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1222            permission_suggestions: vec![],
1223            blocked_path: None,
1224            decision_reason: None,
1225            tool_use_id: None,
1226        };
1227
1228        let response = req.allow("req-123");
1229        let message: ControlResponseMessage = response.into();
1230
1231        let json = serde_json::to_string(&message).unwrap();
1232        assert!(json.contains("\"type\":\"control_response\""));
1233        assert!(json.contains("\"subtype\":\"success\""));
1234        assert!(json.contains("\"request_id\":\"req-123\""));
1235        assert!(json.contains("\"behavior\":\"allow\""));
1236        assert!(json.contains("\"updatedInput\""));
1237    }
1238
1239    #[test]
1240    fn test_tool_permission_request_allow_with_modified_input() {
1241        let req = ToolPermissionRequest {
1242            tool_name: "Write".to_string(),
1243            input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1244            permission_suggestions: vec![],
1245            blocked_path: None,
1246            decision_reason: None,
1247            tool_use_id: None,
1248        };
1249
1250        let modified_input = serde_json::json!({
1251            "file_path": "/tmp/safe/passwd",
1252            "content": "test"
1253        });
1254        let response = req.allow_with(modified_input, "req-456");
1255        let message: ControlResponseMessage = response.into();
1256
1257        let json = serde_json::to_string(&message).unwrap();
1258        assert!(json.contains("/tmp/safe/passwd"));
1259        assert!(!json.contains("/etc/passwd"));
1260    }
1261
1262    #[test]
1263    fn test_tool_permission_request_deny() {
1264        let req = ToolPermissionRequest {
1265            tool_name: "Bash".to_string(),
1266            input: serde_json::json!({"command": "sudo rm -rf /"}),
1267            permission_suggestions: vec![],
1268            blocked_path: None,
1269            decision_reason: None,
1270            tool_use_id: None,
1271        };
1272
1273        let response = req.deny("Dangerous command blocked", "req-789");
1274        let message: ControlResponseMessage = response.into();
1275
1276        let json = serde_json::to_string(&message).unwrap();
1277        assert!(json.contains("\"behavior\":\"deny\""));
1278        assert!(json.contains("Dangerous command blocked"));
1279        assert!(!json.contains("\"interrupt\":true"));
1280    }
1281
1282    #[test]
1283    fn test_tool_permission_request_deny_and_stop() {
1284        let req = ToolPermissionRequest {
1285            tool_name: "Bash".to_string(),
1286            input: serde_json::json!({"command": "rm -rf /"}),
1287            permission_suggestions: vec![],
1288            blocked_path: None,
1289            decision_reason: None,
1290            tool_use_id: None,
1291        };
1292
1293        let response = req.deny_and_stop("Security violation", "req-000");
1294        let message: ControlResponseMessage = response.into();
1295
1296        let json = serde_json::to_string(&message).unwrap();
1297        assert!(json.contains("\"behavior\":\"deny\""));
1298        assert!(json.contains("\"interrupt\":true"));
1299    }
1300
1301    #[test]
1302    fn test_permission_result_serialization() {
1303        // Test allow
1304        let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1305        let json = serde_json::to_string(&allow).unwrap();
1306        assert!(json.contains("\"behavior\":\"allow\""));
1307        assert!(json.contains("\"updatedInput\""));
1308
1309        // Test deny
1310        let deny = PermissionResult::deny("Not allowed");
1311        let json = serde_json::to_string(&deny).unwrap();
1312        assert!(json.contains("\"behavior\":\"deny\""));
1313        assert!(json.contains("\"message\":\"Not allowed\""));
1314        assert!(!json.contains("\"interrupt\""));
1315
1316        // Test deny with interrupt
1317        let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1318        let json = serde_json::to_string(&deny_stop).unwrap();
1319        assert!(json.contains("\"interrupt\":true"));
1320    }
1321
1322    #[test]
1323    fn test_control_request_message_initialize() {
1324        let init = ControlRequestMessage::initialize("init-1");
1325
1326        let json = serde_json::to_string(&init).unwrap();
1327        assert!(json.contains("\"type\":\"control_request\""));
1328        assert!(json.contains("\"request_id\":\"init-1\""));
1329        assert!(json.contains("\"subtype\":\"initialize\""));
1330    }
1331
1332    #[test]
1333    fn test_control_response_error() {
1334        let response = ControlResponse::error("req-err", "Something went wrong");
1335        let message: ControlResponseMessage = response.into();
1336
1337        let json = serde_json::to_string(&message).unwrap();
1338        assert!(json.contains("\"subtype\":\"error\""));
1339        assert!(json.contains("\"error\":\"Something went wrong\""));
1340    }
1341
1342    #[test]
1343    fn test_get_usage_response_parses_model_scoped_limits() {
1344        let response = ControlResponse::success(
1345            "usage-1",
1346            serde_json::json!({
1347                "session": {
1348                    "total_cost_usd": 0.5,
1349                    "total_api_duration_ms": 100,
1350                    "total_duration_ms": 200,
1351                    "total_lines_added": 3,
1352                    "total_lines_removed": 1,
1353                    "model_usage": {
1354                        "claude-sonnet-4-6": {
1355                            "inputTokens": 10,
1356                            "outputTokens": 2,
1357                            "cacheReadInputTokens": 1,
1358                            "cacheCreationInputTokens": 0,
1359                            "webSearchRequests": 0,
1360                            "costUSD": 0.01,
1361                            "contextWindow": 200000,
1362                            "maxOutputTokens": 64000
1363                        }
1364                    }
1365                },
1366                "subscription_type": "pro",
1367                "rate_limits_available": true,
1368                "rate_limits": {
1369                    "five_hour": {"utilization": 0.2, "resets_at": "2026-07-09T22:00:00Z"},
1370                    "model_scoped": [
1371                        {"display_name": "Sonnet", "utilization": 0.4, "resets_at": "2026-07-16T00:00:00Z"}
1372                    ]
1373                },
1374                "behaviors": {
1375                    "request_count": 1,
1376                    "session_count": 1,
1377                    "behaviors": [{"key": "cron", "pct": 100.0, "count": 1}],
1378                    "agents": [],
1379                    "skills": [],
1380                    "plugins": [],
1381                    "mcp_servers": []
1382                }
1383            }),
1384        );
1385
1386        let usage = response.get_usage_response().unwrap().unwrap();
1387        assert_eq!(usage.subscription_type.as_deref(), Some("pro"));
1388        let model = usage.session.model_usage.get("claude-sonnet-4-6").unwrap();
1389        assert_eq!(model.context_window, 200000);
1390        assert_eq!(model.max_output_tokens, 64000);
1391        let limits = usage.rate_limits.unwrap();
1392        assert_eq!(limits.five_hour.unwrap().utilization, Some(0.2));
1393        assert_eq!(limits.model_scoped.unwrap()[0].display_name, "Sonnet");
1394    }
1395
1396    #[test]
1397    fn test_roundtrip_control_request() {
1398        let original_json = r#"{
1399            "type": "control_request",
1400            "request_id": "test-123",
1401            "request": {
1402                "subtype": "can_use_tool",
1403                "tool_name": "Bash",
1404                "input": {"command": "ls -la"},
1405                "permission_suggestions": []
1406            }
1407        }"#;
1408
1409        let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1410
1411        let reserialized = serde_json::to_string(&output).unwrap();
1412        assert!(reserialized.contains("control_request"));
1413        assert!(reserialized.contains("test-123"));
1414        assert!(reserialized.contains("Bash"));
1415    }
1416
1417    #[test]
1418    fn test_permission_suggestions_parsing() {
1419        let json = r#"{
1420            "type": "control_request",
1421            "request_id": "perm-456",
1422            "request": {
1423                "subtype": "can_use_tool",
1424                "tool_name": "Bash",
1425                "input": {"command": "npm test"},
1426                "permission_suggestions": [
1427                    {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1428                    {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1429                ]
1430            }
1431        }"#;
1432
1433        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1434        if let ClaudeOutput::ControlRequest(req) = output {
1435            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1436                assert_eq!(perm_req.permission_suggestions.len(), 2);
1437                assert_eq!(
1438                    perm_req.permission_suggestions[0].suggestion_type,
1439                    PermissionType::SetMode
1440                );
1441                assert_eq!(
1442                    perm_req.permission_suggestions[0].mode,
1443                    Some(PermissionModeName::AcceptEdits)
1444                );
1445                assert_eq!(
1446                    perm_req.permission_suggestions[0].destination,
1447                    PermissionDestination::Session
1448                );
1449                assert_eq!(
1450                    perm_req.permission_suggestions[1].suggestion_type,
1451                    PermissionType::SetMode
1452                );
1453                assert_eq!(
1454                    perm_req.permission_suggestions[1].mode,
1455                    Some(PermissionModeName::BypassPermissions)
1456                );
1457                assert_eq!(
1458                    perm_req.permission_suggestions[1].destination,
1459                    PermissionDestination::Project
1460                );
1461            } else {
1462                panic!("Expected CanUseTool payload");
1463            }
1464        } else {
1465            panic!("Expected ControlRequest");
1466        }
1467    }
1468
1469    #[test]
1470    fn test_permission_suggestion_set_mode_roundtrip() {
1471        let suggestion = PermissionSuggestion {
1472            suggestion_type: PermissionType::SetMode,
1473            destination: PermissionDestination::Session,
1474            mode: Some(PermissionModeName::AcceptEdits),
1475            behavior: None,
1476            rules: None,
1477        };
1478
1479        let json = serde_json::to_string(&suggestion).unwrap();
1480        assert!(json.contains("\"type\":\"setMode\""));
1481        assert!(json.contains("\"mode\":\"acceptEdits\""));
1482        assert!(json.contains("\"destination\":\"session\""));
1483        assert!(!json.contains("\"behavior\""));
1484        assert!(!json.contains("\"rules\""));
1485
1486        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1487        assert_eq!(parsed, suggestion);
1488    }
1489
1490    #[test]
1491    fn test_permission_suggestion_add_rules_roundtrip() {
1492        let suggestion = PermissionSuggestion {
1493            suggestion_type: PermissionType::AddRules,
1494            destination: PermissionDestination::Session,
1495            mode: None,
1496            behavior: Some(PermissionBehavior::Allow),
1497            rules: Some(vec![serde_json::json!({
1498                "toolName": "Read",
1499                "ruleContent": "//tmp/**"
1500            })]),
1501        };
1502
1503        let json = serde_json::to_string(&suggestion).unwrap();
1504        assert!(json.contains("\"type\":\"addRules\""));
1505        assert!(json.contains("\"behavior\":\"allow\""));
1506        assert!(json.contains("\"destination\":\"session\""));
1507        assert!(json.contains("\"rules\""));
1508        assert!(json.contains("\"toolName\":\"Read\""));
1509        assert!(!json.contains("\"mode\""));
1510
1511        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1512        assert_eq!(parsed, suggestion);
1513    }
1514
1515    #[test]
1516    fn test_permission_suggestion_add_rules_from_real_json() {
1517        let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1518
1519        let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1520        assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1521        assert_eq!(parsed.destination, PermissionDestination::Session);
1522        assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1523        assert!(parsed.rules.is_some());
1524        assert!(parsed.mode.is_none());
1525    }
1526
1527    #[test]
1528    fn test_permission_allow_tool() {
1529        let perm = Permission::allow_tool("Bash", "npm test");
1530
1531        assert_eq!(perm.permission_type, PermissionType::AddRules);
1532        assert_eq!(perm.destination, PermissionDestination::Session);
1533        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1534        assert!(perm.mode.is_none());
1535
1536        let rules = perm.rules.unwrap();
1537        assert_eq!(rules.len(), 1);
1538        assert_eq!(rules[0].tool_name, "Bash");
1539        assert_eq!(rules[0].rule_content, "npm test");
1540    }
1541
1542    #[test]
1543    fn test_permission_allow_tool_with_destination() {
1544        let perm = Permission::allow_tool_with_destination(
1545            "Read",
1546            "/tmp/**",
1547            PermissionDestination::Project,
1548        );
1549
1550        assert_eq!(perm.permission_type, PermissionType::AddRules);
1551        assert_eq!(perm.destination, PermissionDestination::Project);
1552        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1553
1554        let rules = perm.rules.unwrap();
1555        assert_eq!(rules[0].tool_name, "Read");
1556        assert_eq!(rules[0].rule_content, "/tmp/**");
1557    }
1558
1559    #[test]
1560    fn test_permission_set_mode() {
1561        let perm = Permission::set_mode(
1562            PermissionModeName::AcceptEdits,
1563            PermissionDestination::Session,
1564        );
1565
1566        assert_eq!(perm.permission_type, PermissionType::SetMode);
1567        assert_eq!(perm.destination, PermissionDestination::Session);
1568        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1569        assert!(perm.behavior.is_none());
1570        assert!(perm.rules.is_none());
1571    }
1572
1573    #[test]
1574    fn test_permission_serialization() {
1575        let perm = Permission::allow_tool("Bash", "npm test");
1576        let json = serde_json::to_string(&perm).unwrap();
1577
1578        assert!(json.contains("\"type\":\"addRules\""));
1579        assert!(json.contains("\"destination\":\"session\""));
1580        assert!(json.contains("\"behavior\":\"allow\""));
1581        assert!(json.contains("\"toolName\":\"Bash\""));
1582        assert!(json.contains("\"ruleContent\":\"npm test\""));
1583    }
1584
1585    #[test]
1586    fn test_permission_from_suggestion_set_mode() {
1587        let suggestion = PermissionSuggestion {
1588            suggestion_type: PermissionType::SetMode,
1589            destination: PermissionDestination::Session,
1590            mode: Some(PermissionModeName::AcceptEdits),
1591            behavior: None,
1592            rules: None,
1593        };
1594
1595        let perm = Permission::from_suggestion(&suggestion);
1596
1597        assert_eq!(perm.permission_type, PermissionType::SetMode);
1598        assert_eq!(perm.destination, PermissionDestination::Session);
1599        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1600    }
1601
1602    #[test]
1603    fn test_permission_from_suggestion_add_rules() {
1604        let suggestion = PermissionSuggestion {
1605            suggestion_type: PermissionType::AddRules,
1606            destination: PermissionDestination::Session,
1607            mode: None,
1608            behavior: Some(PermissionBehavior::Allow),
1609            rules: Some(vec![serde_json::json!({
1610                "toolName": "Read",
1611                "ruleContent": "/tmp/**"
1612            })]),
1613        };
1614
1615        let perm = Permission::from_suggestion(&suggestion);
1616
1617        assert_eq!(perm.permission_type, PermissionType::AddRules);
1618        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1619
1620        let rules = perm.rules.unwrap();
1621        assert_eq!(rules.len(), 1);
1622        assert_eq!(rules[0].tool_name, "Read");
1623        assert_eq!(rules[0].rule_content, "/tmp/**");
1624    }
1625
1626    #[test]
1627    fn test_permission_result_allow_with_typed_permissions() {
1628        let result = PermissionResult::allow_with_typed_permissions(
1629            serde_json::json!({"command": "npm test"}),
1630            vec![Permission::allow_tool("Bash", "npm test")],
1631        );
1632
1633        let json = serde_json::to_string(&result).unwrap();
1634        assert!(json.contains("\"behavior\":\"allow\""));
1635        assert!(json.contains("\"updatedPermissions\""));
1636        assert!(json.contains("\"toolName\":\"Bash\""));
1637    }
1638
1639    #[test]
1640    fn test_tool_permission_request_allow_and_remember() {
1641        let req = ToolPermissionRequest {
1642            tool_name: "Bash".to_string(),
1643            input: serde_json::json!({"command": "npm test"}),
1644            permission_suggestions: vec![],
1645            blocked_path: None,
1646            decision_reason: None,
1647            tool_use_id: None,
1648        };
1649
1650        let response =
1651            req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1652        let message: ControlResponseMessage = response.into();
1653        let json = serde_json::to_string(&message).unwrap();
1654
1655        assert!(json.contains("\"type\":\"control_response\""));
1656        assert!(json.contains("\"behavior\":\"allow\""));
1657        assert!(json.contains("\"updatedPermissions\""));
1658        assert!(json.contains("\"toolName\":\"Bash\""));
1659    }
1660
1661    #[test]
1662    fn test_tool_permission_request_allow_and_remember_suggestion() {
1663        let req = ToolPermissionRequest {
1664            tool_name: "Bash".to_string(),
1665            input: serde_json::json!({"command": "npm test"}),
1666            permission_suggestions: vec![PermissionSuggestion {
1667                suggestion_type: PermissionType::SetMode,
1668                destination: PermissionDestination::Session,
1669                mode: Some(PermissionModeName::AcceptEdits),
1670                behavior: None,
1671                rules: None,
1672            }],
1673            blocked_path: None,
1674            decision_reason: None,
1675            tool_use_id: None,
1676        };
1677
1678        let response = req.allow_and_remember_suggestion("req-123");
1679        assert!(response.is_some());
1680
1681        let message: ControlResponseMessage = response.unwrap().into();
1682        let json = serde_json::to_string(&message).unwrap();
1683
1684        assert!(json.contains("\"type\":\"setMode\""));
1685        assert!(json.contains("\"mode\":\"acceptEdits\""));
1686    }
1687
1688    #[test]
1689    fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1690        let req = ToolPermissionRequest {
1691            tool_name: "Bash".to_string(),
1692            input: serde_json::json!({"command": "npm test"}),
1693            permission_suggestions: vec![], // No suggestions
1694            blocked_path: None,
1695            decision_reason: None,
1696            tool_use_id: None,
1697        };
1698
1699        let response = req.allow_and_remember_suggestion("req-123");
1700        assert!(response.is_none());
1701    }
1702
1703    fn ask_user_question_request() -> ToolPermissionRequest {
1704        ToolPermissionRequest {
1705            tool_name: "AskUserQuestion".to_string(),
1706            input: serde_json::json!({
1707                "questions": [
1708                    {
1709                        "question": "Which color do you prefer?",
1710                        "header": "Color",
1711                        "options": [
1712                            {"label": "Red", "description": "warm"},
1713                            {"label": "Blue", "description": "cool"}
1714                        ],
1715                        "multiSelect": false
1716                    },
1717                    {
1718                        "question": "Pick a size",
1719                        "header": "Size",
1720                        "options": [
1721                            {"label": "Small"},
1722                            {"label": "Large"}
1723                        ],
1724                        "multiSelect": false
1725                    }
1726                ]
1727            }),
1728            permission_suggestions: vec![],
1729            blocked_path: None,
1730            decision_reason: None,
1731            tool_use_id: None,
1732        }
1733    }
1734
1735    fn extract_updated_input(resp: &ControlResponse) -> Value {
1736        let ControlResponsePayload::Success { response, .. } = &resp.response else {
1737            panic!("expected Success payload");
1738        };
1739        let inner = response.as_ref().expect("response body present");
1740        inner
1741            .get("updatedInput")
1742            .cloned()
1743            .expect("updatedInput field present")
1744    }
1745
1746    #[test]
1747    fn answer_questions_keys_by_question_text_and_preserves_questions() {
1748        let req = ask_user_question_request();
1749        let mut answers = HashMap::new();
1750        answers.insert(0, "Blue".to_string());
1751        answers.insert(1, "Large".to_string());
1752
1753        let resp = req.answer_questions(&answers, "rid-1").unwrap();
1754        let updated = extract_updated_input(&resp);
1755
1756        // questions array is preserved verbatim
1757        assert_eq!(
1758            updated["questions"], req.input["questions"],
1759            "questions array must round-trip unchanged"
1760        );
1761        // answers keyed by question text (not header, not index)
1762        assert_eq!(
1763            updated["answers"]["Which color do you prefer?"],
1764            Value::String("Blue".into())
1765        );
1766        assert_eq!(
1767            updated["answers"]["Pick a size"],
1768            Value::String("Large".into())
1769        );
1770        // header is NOT used as the key
1771        assert!(updated["answers"].get("Color").is_none());
1772        assert!(updated["answers"].get("Size").is_none());
1773    }
1774
1775    #[test]
1776    fn answer_questions_partial_answers_omits_unanswered() {
1777        let req = ask_user_question_request();
1778        let mut answers = HashMap::new();
1779        answers.insert(1, "Small".to_string());
1780
1781        let resp = req.answer_questions(&answers, "rid-2").unwrap();
1782        let updated = extract_updated_input(&resp);
1783
1784        assert_eq!(
1785            updated["answers"]["Pick a size"],
1786            Value::String("Small".into())
1787        );
1788        assert!(updated["answers"]
1789            .get("Which color do you prefer?")
1790            .is_none());
1791    }
1792
1793    #[test]
1794    fn answer_questions_rejects_wrong_tool() {
1795        let mut req = ask_user_question_request();
1796        req.tool_name = "Bash".to_string();
1797
1798        let mut answers = HashMap::new();
1799        answers.insert(0, "Blue".to_string());
1800        match req.answer_questions(&answers, "rid-3").unwrap_err() {
1801            AskUserQuestionResponseError::WrongTool(name) => assert_eq!(name, "Bash"),
1802            other => panic!("expected WrongTool, got {other:?}"),
1803        }
1804    }
1805
1806    #[test]
1807    fn answer_questions_rejects_unparseable_input() {
1808        let req = ToolPermissionRequest {
1809            tool_name: "AskUserQuestion".to_string(),
1810            input: serde_json::json!({"not_questions": "garbage"}),
1811            permission_suggestions: vec![],
1812            blocked_path: None,
1813            decision_reason: None,
1814            tool_use_id: None,
1815        };
1816
1817        let answers = HashMap::new();
1818        match req.answer_questions(&answers, "rid-4").unwrap_err() {
1819            AskUserQuestionResponseError::ParseInput(_) => {}
1820            other => panic!("expected ParseInput, got {other:?}"),
1821        }
1822    }
1823
1824    #[test]
1825    fn answer_questions_rejects_out_of_range_index() {
1826        let req = ask_user_question_request();
1827        let mut answers = HashMap::new();
1828        answers.insert(7, "ghost".to_string());
1829
1830        match req.answer_questions(&answers, "rid-5").unwrap_err() {
1831            AskUserQuestionResponseError::QuestionIndexOutOfRange { index, total } => {
1832                assert_eq!(index, 7);
1833                assert_eq!(total, 2);
1834            }
1835            other => panic!("expected QuestionIndexOutOfRange, got {other:?}"),
1836        }
1837    }
1838}