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