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/// Withdrawal of an in-flight [`ControlRequest`] (`control_cancel_request`).
233///
234/// Either side may send this for a request it originated. The CLI sends it
235/// when it no longer needs the answer, for example a pending `can_use_tool`
236/// prompt after the turn was interrupted, or one another client already
237/// answered. The receiver should drop any UI or pending state for
238/// `request_id`; a [`ControlResponse`] sent afterwards is ignored. There is
239/// no reply to the cancel itself.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ControlCancelRequest {
242    /// The `request_id` of the [`ControlRequest`] being withdrawn.
243    pub request_id: String,
244    /// Session id, stamped only by the remote bridge.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub session_id: Option<String>,
247}
248
249/// Control request payload variants
250#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(tag = "subtype", rename_all = "snake_case")]
252pub enum ControlRequestPayload {
253    /// Tool permission request - Claude wants to use a tool
254    CanUseTool(ToolPermissionRequest),
255    /// Hook callback request
256    HookCallback(HookCallbackRequest),
257    /// MCP message request
258    McpMessage(McpMessageRequest),
259    /// Initialize request (sent by SDK to CLI)
260    Initialize(InitializeRequest),
261    /// Interrupt request (sent by SDK to CLI to stop the in-flight turn)
262    Interrupt,
263}
264
265/// A permission to grant for "remember this decision" functionality.
266///
267/// When responding to a tool permission request, you can include permissions
268/// that should be granted to avoid repeated prompts for similar actions.
269///
270/// # Example
271///
272/// ```
273/// use claude_codes::{Permission, PermissionModeName, PermissionDestination};
274///
275/// // Grant permission for a specific bash command
276/// let perm = Permission::allow_tool("Bash", "npm test");
277///
278/// // Grant permission to set a mode for the session
279/// let mode_perm = Permission::set_mode(PermissionModeName::AcceptEdits, PermissionDestination::Session);
280/// ```
281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
282pub struct Permission {
283    /// The type of permission (e.g., addRules, setMode)
284    #[serde(rename = "type")]
285    pub permission_type: PermissionType,
286    /// Where to apply this permission (e.g., session, project)
287    pub destination: PermissionDestination,
288    /// The permission mode (for setMode type)
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub mode: Option<PermissionModeName>,
291    /// The behavior (for addRules type, e.g., allow, deny)
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub behavior: Option<PermissionBehavior>,
294    /// The rules to add (for addRules type)
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub rules: Option<Vec<PermissionRule>>,
297}
298
299/// A rule within a permission grant.
300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
301pub struct PermissionRule {
302    /// The name of the tool this rule applies to
303    #[serde(rename = "toolName")]
304    pub tool_name: String,
305    /// The rule content (glob pattern or command pattern)
306    #[serde(rename = "ruleContent")]
307    pub rule_content: String,
308}
309
310impl Permission {
311    /// Create a permission to allow a specific tool with a rule pattern.
312    ///
313    /// # Example
314    /// ```
315    /// use claude_codes::Permission;
316    ///
317    /// // Allow "npm test" bash command for this session
318    /// let perm = Permission::allow_tool("Bash", "npm test");
319    ///
320    /// // Allow reading from /tmp directory
321    /// let read_perm = Permission::allow_tool("Read", "/tmp/**");
322    /// ```
323    pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
324        Permission {
325            permission_type: PermissionType::AddRules,
326            destination: PermissionDestination::Session,
327            mode: None,
328            behavior: Some(PermissionBehavior::Allow),
329            rules: Some(vec![PermissionRule {
330                tool_name: tool_name.into(),
331                rule_content: rule_content.into(),
332            }]),
333        }
334    }
335
336    /// Create a permission to allow a tool with a specific destination.
337    ///
338    /// # Example
339    /// ```
340    /// use claude_codes::{Permission, PermissionDestination};
341    ///
342    /// // Allow for the entire project, not just session
343    /// let perm = Permission::allow_tool_with_destination("Bash", "npm test", PermissionDestination::Project);
344    /// ```
345    pub fn allow_tool_with_destination(
346        tool_name: impl Into<String>,
347        rule_content: impl Into<String>,
348        destination: PermissionDestination,
349    ) -> Self {
350        Permission {
351            permission_type: PermissionType::AddRules,
352            destination,
353            mode: None,
354            behavior: Some(PermissionBehavior::Allow),
355            rules: Some(vec![PermissionRule {
356                tool_name: tool_name.into(),
357                rule_content: rule_content.into(),
358            }]),
359        }
360    }
361
362    /// Create a permission to set a mode (like acceptEdits or bypassPermissions).
363    ///
364    /// # Example
365    /// ```
366    /// use claude_codes::{Permission, PermissionModeName, PermissionDestination};
367    ///
368    /// // Accept all edits for this session
369    /// let perm = Permission::set_mode(PermissionModeName::AcceptEdits, PermissionDestination::Session);
370    /// ```
371    pub fn set_mode(mode: PermissionModeName, destination: PermissionDestination) -> Self {
372        Permission {
373            permission_type: PermissionType::SetMode,
374            destination,
375            mode: Some(mode),
376            behavior: None,
377            rules: None,
378        }
379    }
380
381    /// Create a permission from a PermissionSuggestion.
382    ///
383    /// This is useful when you want to grant a permission that Claude suggested.
384    ///
385    /// # Example
386    /// ```
387    /// use claude_codes::{Permission, PermissionSuggestion, PermissionType, PermissionDestination, PermissionModeName};
388    ///
389    /// // Convert a suggestion to a permission for the response
390    /// let suggestion = PermissionSuggestion {
391    ///     suggestion_type: PermissionType::SetMode,
392    ///     destination: PermissionDestination::Session,
393    ///     mode: Some(PermissionModeName::AcceptEdits),
394    ///     behavior: None,
395    ///     rules: None,
396    /// };
397    /// let perm = Permission::from_suggestion(&suggestion);
398    /// ```
399    pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
400        Permission {
401            permission_type: suggestion.suggestion_type.clone(),
402            destination: suggestion.destination.clone(),
403            mode: suggestion.mode.clone(),
404            behavior: suggestion.behavior.clone(),
405            rules: suggestion.rules.as_ref().map(|rules| {
406                rules
407                    .iter()
408                    .filter_map(|v| {
409                        Some(PermissionRule {
410                            tool_name: v.get("toolName")?.as_str()?.to_string(),
411                            rule_content: v.get("ruleContent")?.as_str()?.to_string(),
412                        })
413                    })
414                    .collect()
415            }),
416        }
417    }
418}
419
420/// A suggested permission for tool approval.
421///
422/// When Claude requests tool permission, it may include suggestions for
423/// permissions that could be granted to avoid repeated prompts for similar
424/// actions. The format varies based on the suggestion type:
425///
426/// - `setMode`: `{"type": "setMode", "mode": "acceptEdits", "destination": "session"}`
427/// - `addRules`: `{"type": "addRules", "rules": [...], "behavior": "allow", "destination": "session"}`
428///
429/// Use the helper methods to access common fields.
430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
431pub struct PermissionSuggestion {
432    /// The type of suggestion (e.g., setMode, addRules)
433    #[serde(rename = "type")]
434    pub suggestion_type: PermissionType,
435    /// Where to apply this permission (e.g., session, project)
436    pub destination: PermissionDestination,
437    /// The permission mode (for setMode type)
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub mode: Option<PermissionModeName>,
440    /// The behavior (for addRules type, e.g., allow)
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub behavior: Option<PermissionBehavior>,
443    /// The rules to add (for addRules type)
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub rules: Option<Vec<Value>>,
446}
447
448/// Tool permission request details
449///
450/// This is sent when Claude wants to use a tool. The SDK should evaluate
451/// the request and respond with allow/deny using the ergonomic builder methods.
452///
453/// # Example
454///
455/// ```
456/// use claude_codes::{ToolPermissionRequest, ControlResponse};
457/// use serde_json::json;
458///
459/// fn handle_permission(req: &ToolPermissionRequest, request_id: &str) -> ControlResponse {
460///     // Block dangerous bash commands
461///     if req.tool_name == "Bash" {
462///         if let Some(cmd) = req.input.get("command").and_then(|v| v.as_str()) {
463///             if cmd.contains("rm -rf") {
464///                 return req.deny("Dangerous command blocked", request_id);
465///             }
466///         }
467///     }
468///
469///     // Allow everything else
470///     req.allow(request_id)
471/// }
472/// ```
473#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct ToolPermissionRequest {
475    /// Name of the tool Claude wants to use (e.g., "Bash", "Write", "Read")
476    pub tool_name: String,
477    /// Input parameters for the tool
478    pub input: Value,
479    /// Suggested permissions that could be granted to avoid repeated prompts
480    #[serde(default)]
481    pub permission_suggestions: Vec<PermissionSuggestion>,
482    /// Path that was blocked (if this is a retry after path-based denial)
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub blocked_path: Option<String>,
485    /// Reason why this tool use requires approval
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub decision_reason: Option<String>,
488    /// The tool use ID for this request
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub tool_use_id: Option<String>,
491}
492
493impl ToolPermissionRequest {
494    /// Allow the tool to execute with its original input.
495    ///
496    /// # Example
497    /// ```
498    /// # use claude_codes::ToolPermissionRequest;
499    /// # use serde_json::json;
500    /// let req = ToolPermissionRequest {
501    ///     tool_name: "Read".to_string(),
502    ///     input: json!({"file_path": "/tmp/test.txt"}),
503    ///     permission_suggestions: vec![],
504    ///     blocked_path: None,
505    ///     decision_reason: None,
506    ///     tool_use_id: None,
507    /// };
508    /// let response = req.allow("req-123");
509    /// ```
510    pub fn allow(&self, request_id: &str) -> ControlResponse {
511        ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
512    }
513
514    /// Allow the tool to execute with modified input.
515    ///
516    /// Use this to sanitize or redirect tool inputs. For example, redirecting
517    /// file writes to a safe directory.
518    ///
519    /// # Example
520    /// ```
521    /// # use claude_codes::ToolPermissionRequest;
522    /// # use serde_json::json;
523    /// let req = ToolPermissionRequest {
524    ///     tool_name: "Write".to_string(),
525    ///     input: json!({"file_path": "/etc/passwd", "content": "test"}),
526    ///     permission_suggestions: vec![],
527    ///     blocked_path: None,
528    ///     decision_reason: None,
529    ///     tool_use_id: None,
530    /// };
531    /// // Redirect to safe location
532    /// let safe_input = json!({"file_path": "/tmp/safe/passwd", "content": "test"});
533    /// let response = req.allow_with(safe_input, "req-123");
534    /// ```
535    pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
536        ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
537    }
538
539    /// Allow with updated permissions list (raw JSON Values).
540    ///
541    /// Prefer using `allow_and_remember` for type safety.
542    pub fn allow_with_permissions(
543        &self,
544        modified_input: Value,
545        permissions: Vec<Value>,
546        request_id: &str,
547    ) -> ControlResponse {
548        ControlResponse::from_result(
549            request_id,
550            PermissionResult::allow_with_permissions(modified_input, permissions),
551        )
552    }
553
554    /// Allow the tool and grant permissions for "remember this decision".
555    ///
556    /// This is the ergonomic way to allow a tool while also granting permissions
557    /// so similar actions won't require approval in the future.
558    ///
559    /// # Example
560    /// ```
561    /// use claude_codes::{ToolPermissionRequest, Permission};
562    /// use serde_json::json;
563    ///
564    /// let req = ToolPermissionRequest {
565    ///     tool_name: "Bash".to_string(),
566    ///     input: json!({"command": "npm test"}),
567    ///     permission_suggestions: vec![],
568    ///     blocked_path: None,
569    ///     decision_reason: None,
570    ///     tool_use_id: None,
571    /// };
572    ///
573    /// // Allow and remember this decision for the session
574    /// let response = req.allow_and_remember(
575    ///     vec![Permission::allow_tool("Bash", "npm test")],
576    ///     "req-123",
577    /// );
578    /// ```
579    pub fn allow_and_remember(
580        &self,
581        permissions: Vec<Permission>,
582        request_id: &str,
583    ) -> ControlResponse {
584        ControlResponse::from_result(
585            request_id,
586            PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
587        )
588    }
589
590    /// Allow the tool with modified input and grant permissions.
591    ///
592    /// Combines input modification with "remember this decision" functionality.
593    pub fn allow_with_and_remember(
594        &self,
595        modified_input: Value,
596        permissions: Vec<Permission>,
597        request_id: &str,
598    ) -> ControlResponse {
599        ControlResponse::from_result(
600            request_id,
601            PermissionResult::allow_with_typed_permissions(modified_input, permissions),
602        )
603    }
604
605    /// Allow the tool and remember using the first permission suggestion.
606    ///
607    /// This is a convenience method for the common case of accepting Claude's
608    /// first suggested permission (usually the most relevant one).
609    ///
610    /// Returns `None` if there are no permission suggestions.
611    ///
612    /// # Example
613    /// ```
614    /// use claude_codes::ToolPermissionRequest;
615    /// use serde_json::json;
616    ///
617    /// let req = ToolPermissionRequest {
618    ///     tool_name: "Bash".to_string(),
619    ///     input: json!({"command": "npm test"}),
620    ///     permission_suggestions: vec![],  // Would have suggestions in real use
621    ///     blocked_path: None,
622    ///     decision_reason: None,
623    ///     tool_use_id: None,
624    /// };
625    ///
626    /// // Try to allow with first suggestion, or just allow without remembering
627    /// let response = req.allow_and_remember_suggestion("req-123")
628    ///     .unwrap_or_else(|| req.allow("req-123"));
629    /// ```
630    pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
631        self.permission_suggestions.first().map(|suggestion| {
632            let perm = Permission::from_suggestion(suggestion);
633            self.allow_and_remember(vec![perm], request_id)
634        })
635    }
636
637    /// Deny the tool execution.
638    ///
639    /// The message will be shown to Claude, who may try a different approach.
640    ///
641    /// # Example
642    /// ```
643    /// # use claude_codes::ToolPermissionRequest;
644    /// # use serde_json::json;
645    /// let req = ToolPermissionRequest {
646    ///     tool_name: "Bash".to_string(),
647    ///     input: json!({"command": "sudo rm -rf /"}),
648    ///     permission_suggestions: vec![],
649    ///     blocked_path: None,
650    ///     decision_reason: None,
651    ///     tool_use_id: None,
652    /// };
653    /// let response = req.deny("Dangerous command blocked by policy", "req-123");
654    /// ```
655    pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
656        ControlResponse::from_result(request_id, PermissionResult::deny(message))
657    }
658
659    /// Deny the tool execution and stop the entire session.
660    ///
661    /// Use this for severe policy violations that should halt all processing.
662    pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
663        ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
664    }
665
666    /// Build an `allow` response for an `AskUserQuestion` permission request
667    /// by supplying the user's chosen answers.
668    ///
669    /// The CLI's `AskUserQuestion` tool has a finicky wire contract that
670    /// is easy to get wrong by hand:
671    ///
672    /// - The response's `input` must preserve the original `questions`
673    ///   array. Stripping it (sending just `{"answers": ...}`) makes the
674    ///   downstream viewer crash with
675    ///   `undefined is not an object (evaluating 'q.map')` (the CLI echoes
676    ///   the `updatedInput` we return into `tool_use_result`, and viewers
677    ///   read `tool_use_result.questions`).
678    /// - The `answers` map must be keyed by the full **question text**
679    ///   (the `question` field). Keying by `header` or by question index
680    ///   makes the CLI's tool render `"Your questions have been answered: ."`
681    ///   with an empty answer body, leaving Claude unable to see which
682    ///   option the user picked.
683    ///
684    /// This helper does both correctly: it parses `self.input` as
685    /// [`AskUserQuestionInput`](crate::AskUserQuestionInput), looks each
686    /// answer up by question index, and inserts the answer keyed by
687    /// `q.question`. The original `questions` (and any `metadata`) pass
688    /// through unchanged.
689    ///
690    /// `answers_by_index` maps 0-based question index into the original
691    /// `questions` array to the user's chosen answer label. For
692    /// multi-select questions, join the selected labels with `", "`.
693    /// Questions not in the map are treated as unanswered.
694    ///
695    /// Non-`AskUserQuestion` callers should keep using
696    /// [`allow`](Self::allow) / [`allow_with`](Self::allow_with).
697    ///
698    /// # Errors
699    ///
700    /// Returns [`AskUserQuestionResponseError`] if the request isn't an
701    /// `AskUserQuestion` request, the input doesn't parse, or an answer
702    /// references a question index that doesn't exist.
703    ///
704    /// # Example
705    ///
706    /// ```no_run
707    /// use claude_codes::ToolPermissionRequest;
708    /// use std::collections::HashMap;
709    ///
710    /// fn answer(req: &ToolPermissionRequest, request_id: &str) {
711    ///     let mut answers = HashMap::new();
712    ///     answers.insert(0_usize, "Blue".to_string());
713    ///     let response = req
714    ///         .answer_questions(&answers, request_id)
715    ///         .expect("request is AskUserQuestion-shaped");
716    ///     // send `response` back to the CLI
717    /// }
718    /// ```
719    pub fn answer_questions(
720        &self,
721        answers_by_index: &HashMap<usize, String>,
722        request_id: &str,
723    ) -> Result<ControlResponse, AskUserQuestionResponseError> {
724        if self.tool_name != "AskUserQuestion" {
725            return Err(AskUserQuestionResponseError::WrongTool(
726                self.tool_name.clone(),
727            ));
728        }
729        let parsed: AskUserQuestionInput = serde_json::from_value(self.input.clone())
730            .map_err(AskUserQuestionResponseError::ParseInput)?;
731        let total = parsed.questions.len();
732
733        let mut answers_map = serde_json::Map::new();
734        for (idx, answer) in answers_by_index {
735            let q = parsed.questions.get(*idx).ok_or(
736                AskUserQuestionResponseError::QuestionIndexOutOfRange { index: *idx, total },
737            )?;
738            answers_map.insert(q.question.clone(), Value::String(answer.clone()));
739        }
740
741        let mut updated_input = self.input.clone();
742        // Input is an object — we just deserialized it as `AskUserQuestionInput`.
743        updated_input
744            .as_object_mut()
745            .expect("AskUserQuestion input is a JSON object")
746            .insert("answers".to_string(), Value::Object(answers_map));
747
748        Ok(ControlResponse::from_result(
749            request_id,
750            PermissionResult::allow(updated_input),
751        ))
752    }
753}
754
755/// Errors that can occur when building an `AskUserQuestion` response via
756/// [`ToolPermissionRequest::answer_questions`].
757#[derive(Debug, thiserror::Error)]
758pub enum AskUserQuestionResponseError {
759    /// The request was for a different tool, not `AskUserQuestion`.
760    #[error("expected tool_name=AskUserQuestion, got `{0}`")]
761    WrongTool(String),
762    /// The `input` field didn't deserialize into `AskUserQuestionInput`.
763    #[error("failed to parse AskUserQuestion input: {0}")]
764    ParseInput(#[source] serde_json::Error),
765    /// An answer entry referenced a question index outside the questions array.
766    #[error("answer references question index {index}, but only {total} question(s) were asked")]
767    QuestionIndexOutOfRange {
768        /// The out-of-range index the caller supplied.
769        index: usize,
770        /// The number of questions actually in the request.
771        total: usize,
772    },
773}
774
775/// Result of a permission decision
776///
777/// This type represents the decision made by the permission callback.
778/// It can be serialized directly into the control response format.
779#[derive(Debug, Clone, Serialize, Deserialize)]
780#[serde(tag = "behavior", rename_all = "snake_case")]
781pub enum PermissionResult {
782    /// Allow the tool to execute
783    Allow {
784        /// The (possibly modified) input to pass to the tool
785        #[serde(rename = "updatedInput")]
786        updated_input: Value,
787        /// Optional updated permissions list
788        #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
789        updated_permissions: Option<Vec<Value>>,
790    },
791    /// Deny the tool execution
792    Deny {
793        /// Message explaining why the tool was denied
794        message: String,
795        /// If true, stop the entire session
796        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
797        interrupt: bool,
798    },
799}
800
801impl PermissionResult {
802    /// Create an allow result with the given input
803    pub fn allow(input: Value) -> Self {
804        PermissionResult::Allow {
805            updated_input: input,
806            updated_permissions: None,
807        }
808    }
809
810    /// Create an allow result with raw permissions (as JSON Values).
811    ///
812    /// Prefer using `allow_with_typed_permissions` for type safety.
813    pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
814        PermissionResult::Allow {
815            updated_input: input,
816            updated_permissions: Some(permissions),
817        }
818    }
819
820    /// Create an allow result with typed permissions.
821    ///
822    /// This is the preferred way to grant permissions for "remember this decision"
823    /// functionality.
824    ///
825    /// # Example
826    /// ```
827    /// use claude_codes::{Permission, PermissionResult};
828    /// use serde_json::json;
829    ///
830    /// let result = PermissionResult::allow_with_typed_permissions(
831    ///     json!({"command": "npm test"}),
832    ///     vec![Permission::allow_tool("Bash", "npm test")],
833    /// );
834    /// ```
835    pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
836        let permission_values: Vec<Value> = permissions
837            .into_iter()
838            .filter_map(|p| serde_json::to_value(p).ok())
839            .collect();
840        PermissionResult::Allow {
841            updated_input: input,
842            updated_permissions: Some(permission_values),
843        }
844    }
845
846    /// Create a deny result
847    pub fn deny(message: impl Into<String>) -> Self {
848        PermissionResult::Deny {
849            message: message.into(),
850            interrupt: false,
851        }
852    }
853
854    /// Create a deny result that also interrupts the session
855    pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
856        PermissionResult::Deny {
857            message: message.into(),
858            interrupt: true,
859        }
860    }
861}
862
863/// Hook callback request
864#[derive(Debug, Clone, Serialize, Deserialize)]
865pub struct HookCallbackRequest {
866    pub callback_id: String,
867    pub input: Value,
868    #[serde(skip_serializing_if = "Option::is_none")]
869    pub tool_use_id: Option<String>,
870}
871
872/// MCP message request
873#[derive(Debug, Clone, Serialize, Deserialize)]
874pub struct McpMessageRequest {
875    pub server_name: String,
876    pub message: Value,
877}
878
879/// Initialize request (SDK -> CLI)
880#[derive(Debug, Clone, Serialize, Deserialize)]
881pub struct InitializeRequest {
882    #[serde(skip_serializing_if = "Option::is_none")]
883    pub hooks: Option<Value>,
884}
885
886/// Control response to CLI
887///
888/// Built using the ergonomic methods on [`ToolPermissionRequest`] or
889/// constructed directly for other control request types.
890#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct ControlResponse {
892    /// The request ID this response corresponds to
893    pub response: ControlResponsePayload,
894}
895
896impl ControlResponse {
897    /// Create a success response from a PermissionResult
898    ///
899    /// This is the preferred way to construct permission responses.
900    pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
901        // Serialize the PermissionResult to Value for the response
902        let response_value = serde_json::to_value(&result)
903            .expect("PermissionResult serialization should never fail");
904        ControlResponse {
905            response: ControlResponsePayload::Success {
906                request_id: request_id.to_string(),
907                response: Some(response_value),
908            },
909        }
910    }
911
912    /// Create a success response with the given payload (raw Value)
913    pub fn success(request_id: &str, response_data: Value) -> Self {
914        ControlResponse {
915            response: ControlResponsePayload::Success {
916                request_id: request_id.to_string(),
917                response: Some(response_data),
918            },
919        }
920    }
921
922    /// Create an empty success response (for acks)
923    pub fn success_empty(request_id: &str) -> Self {
924        ControlResponse {
925            response: ControlResponsePayload::Success {
926                request_id: request_id.to_string(),
927                response: None,
928            },
929        }
930    }
931
932    /// Create an error response
933    pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
934        ControlResponse {
935            response: ControlResponsePayload::Error {
936                request_id: request_id.to_string(),
937                error: error_message.into(),
938            },
939        }
940    }
941
942    /// Parse a successful response payload as the CLI `get_usage` response.
943    pub fn get_usage_response(&self) -> Option<Result<GetUsageResponse, serde_json::Error>> {
944        match &self.response {
945            ControlResponsePayload::Success {
946                response: Some(value),
947                ..
948            } => Some(serde_json::from_value(value.clone())),
949            _ => None,
950        }
951    }
952}
953
954/// Control response payload
955#[derive(Debug, Clone, Serialize, Deserialize)]
956#[serde(tag = "subtype", rename_all = "snake_case")]
957pub enum ControlResponsePayload {
958    Success {
959        request_id: String,
960        #[serde(skip_serializing_if = "Option::is_none")]
961        response: Option<Value>,
962    },
963    Error {
964        request_id: String,
965        error: String,
966    },
967}
968
969/// Typed payload returned by the CLI `get_usage` control request.
970#[derive(Debug, Clone, Serialize, Deserialize)]
971pub struct GetUsageResponse {
972    pub session: UsageSession,
973    pub subscription_type: Option<String>,
974    pub rate_limits_available: bool,
975    pub rate_limits: Option<UsageRateLimits>,
976    pub behaviors: UsageBehaviors,
977    #[serde(flatten)]
978    pub extra: serde_json::Map<String, Value>,
979}
980
981#[derive(Debug, Clone, Default, Serialize, Deserialize)]
982pub struct UsageSession {
983    pub total_cost_usd: f64,
984    pub total_api_duration_ms: u64,
985    pub total_duration_ms: u64,
986    pub total_lines_added: u64,
987    pub total_lines_removed: u64,
988    pub model_usage: HashMap<String, UsageModelUsage>,
989    #[serde(flatten)]
990    pub extra: serde_json::Map<String, Value>,
991}
992
993#[derive(Debug, Clone, Default, Serialize, Deserialize)]
994#[serde(rename_all = "camelCase")]
995pub struct UsageModelUsage {
996    #[serde(default)]
997    pub input_tokens: u64,
998    #[serde(default)]
999    pub output_tokens: u64,
1000    #[serde(default)]
1001    pub cache_read_input_tokens: u64,
1002    #[serde(default)]
1003    pub cache_creation_input_tokens: u64,
1004    #[serde(default)]
1005    pub web_search_requests: u64,
1006    #[serde(default, rename = "costUSD")]
1007    pub cost_usd: f64,
1008    #[serde(default)]
1009    pub context_window: u64,
1010    #[serde(default)]
1011    pub max_output_tokens: u64,
1012    #[serde(flatten)]
1013    pub extra: serde_json::Map<String, Value>,
1014}
1015
1016#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1017pub struct UsageRateLimits {
1018    #[serde(default, skip_serializing_if = "Option::is_none")]
1019    pub five_hour: Option<UsageRateLimitWindow>,
1020    #[serde(default, skip_serializing_if = "Option::is_none")]
1021    pub seven_day: Option<UsageRateLimitWindow>,
1022    #[serde(default, skip_serializing_if = "Option::is_none")]
1023    pub seven_day_oauth_apps: Option<UsageRateLimitWindow>,
1024    #[serde(default, skip_serializing_if = "Option::is_none")]
1025    pub seven_day_opus: Option<UsageRateLimitWindow>,
1026    #[serde(default, skip_serializing_if = "Option::is_none")]
1027    pub seven_day_sonnet: Option<UsageRateLimitWindow>,
1028    #[serde(default, skip_serializing_if = "Option::is_none")]
1029    pub model_scoped: Option<Vec<ModelScopedRateLimit>>,
1030    #[serde(flatten)]
1031    pub extra: serde_json::Map<String, Value>,
1032}
1033
1034#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1035pub struct UsageRateLimitWindow {
1036    pub utilization: Option<f64>,
1037    pub resets_at: Option<String>,
1038    #[serde(flatten)]
1039    pub extra: serde_json::Map<String, Value>,
1040}
1041
1042#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1043pub struct ModelScopedRateLimit {
1044    pub display_name: String,
1045    pub utilization: Option<f64>,
1046    pub resets_at: Option<String>,
1047    #[serde(flatten)]
1048    pub extra: serde_json::Map<String, Value>,
1049}
1050
1051#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1052pub struct UsageBehaviors {
1053    pub request_count: u64,
1054    pub session_count: u64,
1055    pub behaviors: Vec<UsageBehavior>,
1056    pub agents: Value,
1057    pub skills: Value,
1058    pub plugins: Value,
1059    pub mcp_servers: Value,
1060    #[serde(flatten)]
1061    pub extra: serde_json::Map<String, Value>,
1062}
1063
1064#[derive(Debug, Clone, Serialize, Deserialize)]
1065pub struct UsageBehavior {
1066    pub key: String,
1067    pub pct: f64,
1068    pub count: u64,
1069    #[serde(flatten)]
1070    pub extra: serde_json::Map<String, Value>,
1071}
1072
1073/// Wrapper for outgoing control responses (includes type tag)
1074#[derive(Debug, Clone, Serialize, Deserialize)]
1075pub struct ControlResponseMessage {
1076    #[serde(rename = "type")]
1077    pub message_type: String,
1078    pub response: ControlResponsePayload,
1079}
1080
1081impl From<ControlResponse> for ControlResponseMessage {
1082    fn from(resp: ControlResponse) -> Self {
1083        ControlResponseMessage {
1084            message_type: "control_response".to_string(),
1085            response: resp.response,
1086        }
1087    }
1088}
1089
1090/// Wrapper for outgoing control requests (includes type tag)
1091#[derive(Debug, Clone, Serialize, Deserialize)]
1092pub struct ControlRequestMessage {
1093    #[serde(rename = "type")]
1094    pub message_type: String,
1095    pub request_id: String,
1096    pub request: ControlRequestPayload,
1097}
1098
1099impl ControlRequestMessage {
1100    /// Create an initialization request to send to CLI
1101    pub fn initialize(request_id: impl Into<String>) -> Self {
1102        ControlRequestMessage {
1103            message_type: "control_request".to_string(),
1104            request_id: request_id.into(),
1105            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
1106        }
1107    }
1108
1109    /// Create an initialization request with hooks configuration
1110    pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
1111        ControlRequestMessage {
1112            message_type: "control_request".to_string(),
1113            request_id: request_id.into(),
1114            request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
1115        }
1116    }
1117
1118    /// Create an interrupt request to stop the in-flight turn.
1119    ///
1120    /// Serializes to the `control_request` envelope the CLI requires:
1121    /// `{"type":"control_request","request_id":...,"request":{"subtype":"interrupt"}}`.
1122    /// The CLI acknowledges with a `control_response` carrying the same
1123    /// `request_id` and ends the turn with a `result` message.
1124    pub fn interrupt(request_id: impl Into<String>) -> Self {
1125        ControlRequestMessage {
1126            message_type: "control_request".to_string(),
1127            request_id: request_id.into(),
1128            request: ControlRequestPayload::Interrupt,
1129        }
1130    }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135    use super::*;
1136    use crate::io::ClaudeOutput;
1137
1138    #[test]
1139    fn test_deserialize_control_request_can_use_tool() {
1140        let json = r#"{
1141            "type": "control_request",
1142            "request_id": "perm-abc123",
1143            "request": {
1144                "subtype": "can_use_tool",
1145                "tool_name": "Write",
1146                "input": {
1147                    "file_path": "/home/user/hello.py",
1148                    "content": "print('hello')"
1149                },
1150                "permission_suggestions": [],
1151                "blocked_path": null
1152            }
1153        }"#;
1154
1155        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1156        assert!(output.is_control_request());
1157
1158        if let ClaudeOutput::ControlRequest(req) = output {
1159            assert_eq!(req.request_id, "perm-abc123");
1160            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1161                assert_eq!(perm_req.tool_name, "Write");
1162                assert_eq!(
1163                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1164                    "/home/user/hello.py"
1165                );
1166            } else {
1167                panic!("Expected CanUseTool payload");
1168            }
1169        } else {
1170            panic!("Expected ControlRequest");
1171        }
1172    }
1173
1174    #[test]
1175    fn test_deserialize_control_request_edit_tool_real() {
1176        // Real production message from Claude CLI
1177        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"}}"#;
1178
1179        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1180        assert!(output.is_control_request());
1181        assert_eq!(output.message_type(), "control_request");
1182
1183        if let ClaudeOutput::ControlRequest(req) = output {
1184            assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1185            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1186                assert_eq!(perm_req.tool_name, "Edit");
1187                assert_eq!(
1188                    perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1189                    "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1190                );
1191                assert!(perm_req.input.get("old_string").is_some());
1192                assert!(perm_req.input.get("new_string").is_some());
1193                assert!(!perm_req
1194                    .input
1195                    .get("replace_all")
1196                    .unwrap()
1197                    .as_bool()
1198                    .unwrap());
1199            } else {
1200                panic!("Expected CanUseTool payload");
1201            }
1202        } else {
1203            panic!("Expected ControlRequest");
1204        }
1205    }
1206
1207    #[test]
1208    fn test_tool_permission_request_allow() {
1209        let req = ToolPermissionRequest {
1210            tool_name: "Read".to_string(),
1211            input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1212            permission_suggestions: vec![],
1213            blocked_path: None,
1214            decision_reason: None,
1215            tool_use_id: None,
1216        };
1217
1218        let response = req.allow("req-123");
1219        let message: ControlResponseMessage = response.into();
1220
1221        let json = serde_json::to_string(&message).unwrap();
1222        assert!(json.contains("\"type\":\"control_response\""));
1223        assert!(json.contains("\"subtype\":\"success\""));
1224        assert!(json.contains("\"request_id\":\"req-123\""));
1225        assert!(json.contains("\"behavior\":\"allow\""));
1226        assert!(json.contains("\"updatedInput\""));
1227    }
1228
1229    #[test]
1230    fn test_tool_permission_request_allow_with_modified_input() {
1231        let req = ToolPermissionRequest {
1232            tool_name: "Write".to_string(),
1233            input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1234            permission_suggestions: vec![],
1235            blocked_path: None,
1236            decision_reason: None,
1237            tool_use_id: None,
1238        };
1239
1240        let modified_input = serde_json::json!({
1241            "file_path": "/tmp/safe/passwd",
1242            "content": "test"
1243        });
1244        let response = req.allow_with(modified_input, "req-456");
1245        let message: ControlResponseMessage = response.into();
1246
1247        let json = serde_json::to_string(&message).unwrap();
1248        assert!(json.contains("/tmp/safe/passwd"));
1249        assert!(!json.contains("/etc/passwd"));
1250    }
1251
1252    #[test]
1253    fn test_tool_permission_request_deny() {
1254        let req = ToolPermissionRequest {
1255            tool_name: "Bash".to_string(),
1256            input: serde_json::json!({"command": "sudo rm -rf /"}),
1257            permission_suggestions: vec![],
1258            blocked_path: None,
1259            decision_reason: None,
1260            tool_use_id: None,
1261        };
1262
1263        let response = req.deny("Dangerous command blocked", "req-789");
1264        let message: ControlResponseMessage = response.into();
1265
1266        let json = serde_json::to_string(&message).unwrap();
1267        assert!(json.contains("\"behavior\":\"deny\""));
1268        assert!(json.contains("Dangerous command blocked"));
1269        assert!(!json.contains("\"interrupt\":true"));
1270    }
1271
1272    #[test]
1273    fn test_tool_permission_request_deny_and_stop() {
1274        let req = ToolPermissionRequest {
1275            tool_name: "Bash".to_string(),
1276            input: serde_json::json!({"command": "rm -rf /"}),
1277            permission_suggestions: vec![],
1278            blocked_path: None,
1279            decision_reason: None,
1280            tool_use_id: None,
1281        };
1282
1283        let response = req.deny_and_stop("Security violation", "req-000");
1284        let message: ControlResponseMessage = response.into();
1285
1286        let json = serde_json::to_string(&message).unwrap();
1287        assert!(json.contains("\"behavior\":\"deny\""));
1288        assert!(json.contains("\"interrupt\":true"));
1289    }
1290
1291    #[test]
1292    fn test_permission_result_serialization() {
1293        // Test allow
1294        let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1295        let json = serde_json::to_string(&allow).unwrap();
1296        assert!(json.contains("\"behavior\":\"allow\""));
1297        assert!(json.contains("\"updatedInput\""));
1298
1299        // Test deny
1300        let deny = PermissionResult::deny("Not allowed");
1301        let json = serde_json::to_string(&deny).unwrap();
1302        assert!(json.contains("\"behavior\":\"deny\""));
1303        assert!(json.contains("\"message\":\"Not allowed\""));
1304        assert!(!json.contains("\"interrupt\""));
1305
1306        // Test deny with interrupt
1307        let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1308        let json = serde_json::to_string(&deny_stop).unwrap();
1309        assert!(json.contains("\"interrupt\":true"));
1310    }
1311
1312    #[test]
1313    fn test_control_request_message_initialize() {
1314        let init = ControlRequestMessage::initialize("init-1");
1315
1316        let json = serde_json::to_string(&init).unwrap();
1317        assert!(json.contains("\"type\":\"control_request\""));
1318        assert!(json.contains("\"request_id\":\"init-1\""));
1319        assert!(json.contains("\"subtype\":\"initialize\""));
1320    }
1321
1322    #[test]
1323    fn test_control_request_message_interrupt_wire_shape() {
1324        // The CLI silently ignores a bare {"subtype":"interrupt"}; it only
1325        // acts on the full control_request envelope (verified against 2.1.211).
1326        let msg = ControlRequestMessage::interrupt("interrupt-1");
1327        assert_eq!(
1328            serde_json::to_value(&msg).unwrap(),
1329            serde_json::json!({
1330                "type": "control_request",
1331                "request_id": "interrupt-1",
1332                "request": {"subtype": "interrupt"}
1333            })
1334        );
1335    }
1336
1337    #[test]
1338    fn test_interrupt_payload_roundtrip() {
1339        let json = r#"{"subtype":"interrupt"}"#;
1340        let payload: ControlRequestPayload = serde_json::from_str(json).unwrap();
1341        assert!(matches!(payload, ControlRequestPayload::Interrupt));
1342        assert_eq!(serde_json::to_string(&payload).unwrap(), json);
1343    }
1344
1345    #[test]
1346    fn test_control_response_error() {
1347        let response = ControlResponse::error("req-err", "Something went wrong");
1348        let message: ControlResponseMessage = response.into();
1349
1350        let json = serde_json::to_string(&message).unwrap();
1351        assert!(json.contains("\"subtype\":\"error\""));
1352        assert!(json.contains("\"error\":\"Something went wrong\""));
1353    }
1354
1355    #[test]
1356    fn test_get_usage_response_parses_model_scoped_limits() {
1357        let response = ControlResponse::success(
1358            "usage-1",
1359            serde_json::json!({
1360                "session": {
1361                    "total_cost_usd": 0.5,
1362                    "total_api_duration_ms": 100,
1363                    "total_duration_ms": 200,
1364                    "total_lines_added": 3,
1365                    "total_lines_removed": 1,
1366                    "model_usage": {
1367                        "claude-sonnet-4-6": {
1368                            "inputTokens": 10,
1369                            "outputTokens": 2,
1370                            "cacheReadInputTokens": 1,
1371                            "cacheCreationInputTokens": 0,
1372                            "webSearchRequests": 0,
1373                            "costUSD": 0.01,
1374                            "contextWindow": 200000,
1375                            "maxOutputTokens": 64000
1376                        }
1377                    }
1378                },
1379                "subscription_type": "pro",
1380                "rate_limits_available": true,
1381                "rate_limits": {
1382                    "five_hour": {"utilization": 0.2, "resets_at": "2026-07-09T22:00:00Z"},
1383                    "model_scoped": [
1384                        {"display_name": "Sonnet", "utilization": 0.4, "resets_at": "2026-07-16T00:00:00Z"}
1385                    ]
1386                },
1387                "behaviors": {
1388                    "request_count": 1,
1389                    "session_count": 1,
1390                    "behaviors": [{"key": "cron", "pct": 100.0, "count": 1}],
1391                    "agents": [],
1392                    "skills": [],
1393                    "plugins": [],
1394                    "mcp_servers": []
1395                }
1396            }),
1397        );
1398
1399        let usage = response.get_usage_response().unwrap().unwrap();
1400        assert_eq!(usage.subscription_type.as_deref(), Some("pro"));
1401        let model = usage.session.model_usage.get("claude-sonnet-4-6").unwrap();
1402        assert_eq!(model.context_window, 200000);
1403        assert_eq!(model.max_output_tokens, 64000);
1404        let limits = usage.rate_limits.unwrap();
1405        assert_eq!(limits.five_hour.unwrap().utilization, Some(0.2));
1406        assert_eq!(limits.model_scoped.unwrap()[0].display_name, "Sonnet");
1407    }
1408
1409    #[test]
1410    fn test_roundtrip_control_request() {
1411        let original_json = r#"{
1412            "type": "control_request",
1413            "request_id": "test-123",
1414            "request": {
1415                "subtype": "can_use_tool",
1416                "tool_name": "Bash",
1417                "input": {"command": "ls -la"},
1418                "permission_suggestions": []
1419            }
1420        }"#;
1421
1422        let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1423
1424        let reserialized = serde_json::to_string(&output).unwrap();
1425        assert!(reserialized.contains("control_request"));
1426        assert!(reserialized.contains("test-123"));
1427        assert!(reserialized.contains("Bash"));
1428    }
1429
1430    #[test]
1431    fn test_permission_suggestions_parsing() {
1432        let json = r#"{
1433            "type": "control_request",
1434            "request_id": "perm-456",
1435            "request": {
1436                "subtype": "can_use_tool",
1437                "tool_name": "Bash",
1438                "input": {"command": "npm test"},
1439                "permission_suggestions": [
1440                    {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1441                    {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1442                ]
1443            }
1444        }"#;
1445
1446        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1447        if let ClaudeOutput::ControlRequest(req) = output {
1448            if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1449                assert_eq!(perm_req.permission_suggestions.len(), 2);
1450                assert_eq!(
1451                    perm_req.permission_suggestions[0].suggestion_type,
1452                    PermissionType::SetMode
1453                );
1454                assert_eq!(
1455                    perm_req.permission_suggestions[0].mode,
1456                    Some(PermissionModeName::AcceptEdits)
1457                );
1458                assert_eq!(
1459                    perm_req.permission_suggestions[0].destination,
1460                    PermissionDestination::Session
1461                );
1462                assert_eq!(
1463                    perm_req.permission_suggestions[1].suggestion_type,
1464                    PermissionType::SetMode
1465                );
1466                assert_eq!(
1467                    perm_req.permission_suggestions[1].mode,
1468                    Some(PermissionModeName::BypassPermissions)
1469                );
1470                assert_eq!(
1471                    perm_req.permission_suggestions[1].destination,
1472                    PermissionDestination::Project
1473                );
1474            } else {
1475                panic!("Expected CanUseTool payload");
1476            }
1477        } else {
1478            panic!("Expected ControlRequest");
1479        }
1480    }
1481
1482    #[test]
1483    fn test_permission_suggestion_set_mode_roundtrip() {
1484        let suggestion = PermissionSuggestion {
1485            suggestion_type: PermissionType::SetMode,
1486            destination: PermissionDestination::Session,
1487            mode: Some(PermissionModeName::AcceptEdits),
1488            behavior: None,
1489            rules: None,
1490        };
1491
1492        let json = serde_json::to_string(&suggestion).unwrap();
1493        assert!(json.contains("\"type\":\"setMode\""));
1494        assert!(json.contains("\"mode\":\"acceptEdits\""));
1495        assert!(json.contains("\"destination\":\"session\""));
1496        assert!(!json.contains("\"behavior\""));
1497        assert!(!json.contains("\"rules\""));
1498
1499        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1500        assert_eq!(parsed, suggestion);
1501    }
1502
1503    #[test]
1504    fn test_permission_suggestion_add_rules_roundtrip() {
1505        let suggestion = PermissionSuggestion {
1506            suggestion_type: PermissionType::AddRules,
1507            destination: PermissionDestination::Session,
1508            mode: None,
1509            behavior: Some(PermissionBehavior::Allow),
1510            rules: Some(vec![serde_json::json!({
1511                "toolName": "Read",
1512                "ruleContent": "//tmp/**"
1513            })]),
1514        };
1515
1516        let json = serde_json::to_string(&suggestion).unwrap();
1517        assert!(json.contains("\"type\":\"addRules\""));
1518        assert!(json.contains("\"behavior\":\"allow\""));
1519        assert!(json.contains("\"destination\":\"session\""));
1520        assert!(json.contains("\"rules\""));
1521        assert!(json.contains("\"toolName\":\"Read\""));
1522        assert!(!json.contains("\"mode\""));
1523
1524        let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1525        assert_eq!(parsed, suggestion);
1526    }
1527
1528    #[test]
1529    fn test_permission_suggestion_add_rules_from_real_json() {
1530        let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1531
1532        let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1533        assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1534        assert_eq!(parsed.destination, PermissionDestination::Session);
1535        assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1536        assert!(parsed.rules.is_some());
1537        assert!(parsed.mode.is_none());
1538    }
1539
1540    #[test]
1541    fn test_permission_allow_tool() {
1542        let perm = Permission::allow_tool("Bash", "npm test");
1543
1544        assert_eq!(perm.permission_type, PermissionType::AddRules);
1545        assert_eq!(perm.destination, PermissionDestination::Session);
1546        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1547        assert!(perm.mode.is_none());
1548
1549        let rules = perm.rules.unwrap();
1550        assert_eq!(rules.len(), 1);
1551        assert_eq!(rules[0].tool_name, "Bash");
1552        assert_eq!(rules[0].rule_content, "npm test");
1553    }
1554
1555    #[test]
1556    fn test_permission_allow_tool_with_destination() {
1557        let perm = Permission::allow_tool_with_destination(
1558            "Read",
1559            "/tmp/**",
1560            PermissionDestination::Project,
1561        );
1562
1563        assert_eq!(perm.permission_type, PermissionType::AddRules);
1564        assert_eq!(perm.destination, PermissionDestination::Project);
1565        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1566
1567        let rules = perm.rules.unwrap();
1568        assert_eq!(rules[0].tool_name, "Read");
1569        assert_eq!(rules[0].rule_content, "/tmp/**");
1570    }
1571
1572    #[test]
1573    fn test_permission_set_mode() {
1574        let perm = Permission::set_mode(
1575            PermissionModeName::AcceptEdits,
1576            PermissionDestination::Session,
1577        );
1578
1579        assert_eq!(perm.permission_type, PermissionType::SetMode);
1580        assert_eq!(perm.destination, PermissionDestination::Session);
1581        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1582        assert!(perm.behavior.is_none());
1583        assert!(perm.rules.is_none());
1584    }
1585
1586    #[test]
1587    fn test_permission_serialization() {
1588        let perm = Permission::allow_tool("Bash", "npm test");
1589        let json = serde_json::to_string(&perm).unwrap();
1590
1591        assert!(json.contains("\"type\":\"addRules\""));
1592        assert!(json.contains("\"destination\":\"session\""));
1593        assert!(json.contains("\"behavior\":\"allow\""));
1594        assert!(json.contains("\"toolName\":\"Bash\""));
1595        assert!(json.contains("\"ruleContent\":\"npm test\""));
1596    }
1597
1598    #[test]
1599    fn test_permission_from_suggestion_set_mode() {
1600        let suggestion = PermissionSuggestion {
1601            suggestion_type: PermissionType::SetMode,
1602            destination: PermissionDestination::Session,
1603            mode: Some(PermissionModeName::AcceptEdits),
1604            behavior: None,
1605            rules: None,
1606        };
1607
1608        let perm = Permission::from_suggestion(&suggestion);
1609
1610        assert_eq!(perm.permission_type, PermissionType::SetMode);
1611        assert_eq!(perm.destination, PermissionDestination::Session);
1612        assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1613    }
1614
1615    #[test]
1616    fn test_permission_from_suggestion_add_rules() {
1617        let suggestion = PermissionSuggestion {
1618            suggestion_type: PermissionType::AddRules,
1619            destination: PermissionDestination::Session,
1620            mode: None,
1621            behavior: Some(PermissionBehavior::Allow),
1622            rules: Some(vec![serde_json::json!({
1623                "toolName": "Read",
1624                "ruleContent": "/tmp/**"
1625            })]),
1626        };
1627
1628        let perm = Permission::from_suggestion(&suggestion);
1629
1630        assert_eq!(perm.permission_type, PermissionType::AddRules);
1631        assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1632
1633        let rules = perm.rules.unwrap();
1634        assert_eq!(rules.len(), 1);
1635        assert_eq!(rules[0].tool_name, "Read");
1636        assert_eq!(rules[0].rule_content, "/tmp/**");
1637    }
1638
1639    #[test]
1640    fn test_permission_result_allow_with_typed_permissions() {
1641        let result = PermissionResult::allow_with_typed_permissions(
1642            serde_json::json!({"command": "npm test"}),
1643            vec![Permission::allow_tool("Bash", "npm test")],
1644        );
1645
1646        let json = serde_json::to_string(&result).unwrap();
1647        assert!(json.contains("\"behavior\":\"allow\""));
1648        assert!(json.contains("\"updatedPermissions\""));
1649        assert!(json.contains("\"toolName\":\"Bash\""));
1650    }
1651
1652    #[test]
1653    fn test_tool_permission_request_allow_and_remember() {
1654        let req = ToolPermissionRequest {
1655            tool_name: "Bash".to_string(),
1656            input: serde_json::json!({"command": "npm test"}),
1657            permission_suggestions: vec![],
1658            blocked_path: None,
1659            decision_reason: None,
1660            tool_use_id: None,
1661        };
1662
1663        let response =
1664            req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1665        let message: ControlResponseMessage = response.into();
1666        let json = serde_json::to_string(&message).unwrap();
1667
1668        assert!(json.contains("\"type\":\"control_response\""));
1669        assert!(json.contains("\"behavior\":\"allow\""));
1670        assert!(json.contains("\"updatedPermissions\""));
1671        assert!(json.contains("\"toolName\":\"Bash\""));
1672    }
1673
1674    #[test]
1675    fn test_tool_permission_request_allow_and_remember_suggestion() {
1676        let req = ToolPermissionRequest {
1677            tool_name: "Bash".to_string(),
1678            input: serde_json::json!({"command": "npm test"}),
1679            permission_suggestions: vec![PermissionSuggestion {
1680                suggestion_type: PermissionType::SetMode,
1681                destination: PermissionDestination::Session,
1682                mode: Some(PermissionModeName::AcceptEdits),
1683                behavior: None,
1684                rules: None,
1685            }],
1686            blocked_path: None,
1687            decision_reason: None,
1688            tool_use_id: None,
1689        };
1690
1691        let response = req.allow_and_remember_suggestion("req-123");
1692        assert!(response.is_some());
1693
1694        let message: ControlResponseMessage = response.unwrap().into();
1695        let json = serde_json::to_string(&message).unwrap();
1696
1697        assert!(json.contains("\"type\":\"setMode\""));
1698        assert!(json.contains("\"mode\":\"acceptEdits\""));
1699    }
1700
1701    #[test]
1702    fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1703        let req = ToolPermissionRequest {
1704            tool_name: "Bash".to_string(),
1705            input: serde_json::json!({"command": "npm test"}),
1706            permission_suggestions: vec![], // No suggestions
1707            blocked_path: None,
1708            decision_reason: None,
1709            tool_use_id: None,
1710        };
1711
1712        let response = req.allow_and_remember_suggestion("req-123");
1713        assert!(response.is_none());
1714    }
1715
1716    fn ask_user_question_request() -> ToolPermissionRequest {
1717        ToolPermissionRequest {
1718            tool_name: "AskUserQuestion".to_string(),
1719            input: serde_json::json!({
1720                "questions": [
1721                    {
1722                        "question": "Which color do you prefer?",
1723                        "header": "Color",
1724                        "options": [
1725                            {"label": "Red", "description": "warm"},
1726                            {"label": "Blue", "description": "cool"}
1727                        ],
1728                        "multiSelect": false
1729                    },
1730                    {
1731                        "question": "Pick a size",
1732                        "header": "Size",
1733                        "options": [
1734                            {"label": "Small"},
1735                            {"label": "Large"}
1736                        ],
1737                        "multiSelect": false
1738                    }
1739                ]
1740            }),
1741            permission_suggestions: vec![],
1742            blocked_path: None,
1743            decision_reason: None,
1744            tool_use_id: None,
1745        }
1746    }
1747
1748    fn extract_updated_input(resp: &ControlResponse) -> Value {
1749        let ControlResponsePayload::Success { response, .. } = &resp.response else {
1750            panic!("expected Success payload");
1751        };
1752        let inner = response.as_ref().expect("response body present");
1753        inner
1754            .get("updatedInput")
1755            .cloned()
1756            .expect("updatedInput field present")
1757    }
1758
1759    #[test]
1760    fn answer_questions_keys_by_question_text_and_preserves_questions() {
1761        let req = ask_user_question_request();
1762        let mut answers = HashMap::new();
1763        answers.insert(0, "Blue".to_string());
1764        answers.insert(1, "Large".to_string());
1765
1766        let resp = req.answer_questions(&answers, "rid-1").unwrap();
1767        let updated = extract_updated_input(&resp);
1768
1769        // questions array is preserved verbatim
1770        assert_eq!(
1771            updated["questions"], req.input["questions"],
1772            "questions array must round-trip unchanged"
1773        );
1774        // answers keyed by question text (not header, not index)
1775        assert_eq!(
1776            updated["answers"]["Which color do you prefer?"],
1777            Value::String("Blue".into())
1778        );
1779        assert_eq!(
1780            updated["answers"]["Pick a size"],
1781            Value::String("Large".into())
1782        );
1783        // header is NOT used as the key
1784        assert!(updated["answers"].get("Color").is_none());
1785        assert!(updated["answers"].get("Size").is_none());
1786    }
1787
1788    #[test]
1789    fn answer_questions_partial_answers_omits_unanswered() {
1790        let req = ask_user_question_request();
1791        let mut answers = HashMap::new();
1792        answers.insert(1, "Small".to_string());
1793
1794        let resp = req.answer_questions(&answers, "rid-2").unwrap();
1795        let updated = extract_updated_input(&resp);
1796
1797        assert_eq!(
1798            updated["answers"]["Pick a size"],
1799            Value::String("Small".into())
1800        );
1801        assert!(updated["answers"]
1802            .get("Which color do you prefer?")
1803            .is_none());
1804    }
1805
1806    #[test]
1807    fn answer_questions_rejects_wrong_tool() {
1808        let mut req = ask_user_question_request();
1809        req.tool_name = "Bash".to_string();
1810
1811        let mut answers = HashMap::new();
1812        answers.insert(0, "Blue".to_string());
1813        match req.answer_questions(&answers, "rid-3").unwrap_err() {
1814            AskUserQuestionResponseError::WrongTool(name) => assert_eq!(name, "Bash"),
1815            other => panic!("expected WrongTool, got {other:?}"),
1816        }
1817    }
1818
1819    #[test]
1820    fn answer_questions_rejects_unparseable_input() {
1821        let req = ToolPermissionRequest {
1822            tool_name: "AskUserQuestion".to_string(),
1823            input: serde_json::json!({"not_questions": "garbage"}),
1824            permission_suggestions: vec![],
1825            blocked_path: None,
1826            decision_reason: None,
1827            tool_use_id: None,
1828        };
1829
1830        let answers = HashMap::new();
1831        match req.answer_questions(&answers, "rid-4").unwrap_err() {
1832            AskUserQuestionResponseError::ParseInput(_) => {}
1833            other => panic!("expected ParseInput, got {other:?}"),
1834        }
1835    }
1836
1837    #[test]
1838    fn answer_questions_rejects_out_of_range_index() {
1839        let req = ask_user_question_request();
1840        let mut answers = HashMap::new();
1841        answers.insert(7, "ghost".to_string());
1842
1843        match req.answer_questions(&answers, "rid-5").unwrap_err() {
1844            AskUserQuestionResponseError::QuestionIndexOutOfRange { index, total } => {
1845                assert_eq!(index, 7);
1846                assert_eq!(total, 2);
1847            }
1848            other => panic!("expected QuestionIndexOutOfRange, got {other:?}"),
1849        }
1850    }
1851}