Skip to main content

matrixcode_core/
approval.rs

1//! Approval gate: interactive confirmation before executing mutating or dangerous tools.
2//!
3//! Three modes:
4//! - `Auto`: execute everything without asking (trust the AI).
5//! - `Ask` (default): pause before mutating/dangerous operations.
6//! - `Strict`: pause before every tool call.
7
8use std::fmt;
9use std::io::{self, BufRead, Write as _};
10
11use serde_json::Value;
12
13// ============================================================================
14// Risk Level
15// ============================================================================
16
17/// Risk level assigned to each tool operation.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19pub enum RiskLevel {
20    /// Read-only, no side effects (e.g., read, search, glob, ls).
21    Safe,
22    /// Modifies files but in a controlled way (e.g., write, edit, multi_edit, todo_write).
23    Mutating,
24    /// Potentially dangerous or irreversible (e.g., bash commands).
25    Dangerous,
26}
27
28impl fmt::Display for RiskLevel {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            RiskLevel::Safe => write!(f, "safe"),
32            RiskLevel::Mutating => write!(f, "mutating"),
33            RiskLevel::Dangerous => write!(f, "dangerous"),
34        }
35    }
36}
37
38impl RiskLevel {
39    /// Get the icon symbol for this risk level.
40    pub fn icon(&self) -> &'static str {
41        match self {
42            RiskLevel::Safe => "โ„น๏ธ ",
43            RiskLevel::Mutating => "๐Ÿ“",
44            RiskLevel::Dangerous => "โš ๏ธ ",
45        }
46    }
47}
48
49// ============================================================================
50// Approve Mode
51// ============================================================================
52
53/// Approval mode controlling when the user is prompted.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
55pub enum ApproveMode {
56    /// Never ask, execute everything automatically.
57    Auto,
58    /// Ask before mutating and dangerous operations (default).
59    #[default]
60    Ask,
61    /// Ask before every tool call, including safe ones.
62    Strict,
63}
64
65impl ApproveMode {
66    pub fn parse(s: &str) -> Self {
67        match s.to_lowercase().as_str() {
68            "auto" => ApproveMode::Auto,
69            "strict" => ApproveMode::Strict,
70            _ => ApproveMode::Ask,
71        }
72    }
73
74    /// Cycle to the next mode: Ask -> Auto -> Strict -> Ask
75    pub fn next(&self) -> Self {
76        match self {
77            ApproveMode::Ask => ApproveMode::Auto,
78            ApproveMode::Auto => ApproveMode::Strict,
79            ApproveMode::Strict => ApproveMode::Ask,
80        }
81    }
82
83    /// Convert to u8 for atomic storage.
84    pub fn to_u8(self) -> u8 {
85        match self {
86            ApproveMode::Auto => 0,
87            ApproveMode::Ask => 1,
88            ApproveMode::Strict => 2,
89        }
90    }
91
92    /// Convert from u8 (atomic load).
93    pub fn from_u8(v: u8) -> Self {
94        match v {
95            0 => ApproveMode::Auto,
96            2 => ApproveMode::Strict,
97            _ => ApproveMode::Ask,
98        }
99    }
100}
101
102impl fmt::Display for ApproveMode {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            ApproveMode::Auto => write!(f, "auto"),
106            ApproveMode::Ask => write!(f, "ask"),
107            ApproveMode::Strict => write!(f, "strict"),
108        }
109    }
110}
111
112// ============================================================================
113// Approval Answer
114// ============================================================================
115
116/// User's response to an approval prompt.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum ApprovalAnswer {
119    /// Proceed with execution.
120    Yes,
121    /// Skip this tool call (return a "rejected" message to the AI).
122    No,
123    /// Abort the entire turn.
124    Abort,
125}
126
127// ============================================================================
128// Approval Request
129// ============================================================================
130
131/// A human-readable summary of what is about to happen.
132#[derive(Debug, Clone)]
133pub struct ApprovalRequest {
134    pub tool_name: String,
135    pub risk_level: RiskLevel,
136    pub summary: String,
137}
138
139impl ApprovalRequest {
140    /// Build an approval request from tool name, risk level, and parameters.
141    pub fn new(tool_name: &str, risk: RiskLevel, params: &Value) -> Self {
142        Self {
143            tool_name: tool_name.to_string(),
144            risk_level: risk,
145            summary: build_summary(tool_name, params),
146        }
147    }
148}
149
150/// Build a human-readable summary for the tool operation.
151fn build_summary(tool_name: &str, params: &Value) -> String {
152    match tool_name {
153        "write" => summary_write(params),
154        "edit" => summary_edit(params),
155        "multi_edit" => summary_multi_edit(params),
156        "bash" => summary_bash(params),
157        "todo_write" => "ๆ›ดๆ–ฐไปปๅŠกๆธ…ๅ•".to_string(),
158        _ => format!("ๆ‰ง่กŒๅทฅๅ…ท: {}", tool_name),
159    }
160}
161
162fn summary_write(params: &Value) -> String {
163    let path = params["path"].as_str().unwrap_or("<unknown>");
164    format!("ๅ†™ๅ…ฅๆ–‡ไปถ: {}", path)
165}
166
167fn summary_edit(params: &Value) -> String {
168    let path = params["path"].as_str().unwrap_or("<unknown>");
169    format!("็ผ–่พ‘ๆ–‡ไปถ: {}", path)
170}
171
172fn summary_multi_edit(params: &Value) -> String {
173    let path = params["path"].as_str().unwrap_or("<unknown>");
174    let count = params["edits"].as_array().map(|a| a.len()).unwrap_or(0);
175    format!("ๆ‰น้‡็ผ–่พ‘ๆ–‡ไปถ: {} ({} ๅค„ไฟฎๆ”น)", path, count)
176}
177
178fn summary_bash(params: &Value) -> String {
179    let cmd = params["command"].as_str().unwrap_or("<unknown>");
180    let display_cmd = if cmd.len() > 120 {
181        let mut end = 120;
182        while end > 0 && !cmd.is_char_boundary(end) {
183            end -= 1;
184        }
185        format!("{}...", &cmd[..end])
186    } else {
187        cmd.to_string()
188    };
189    format!("ๆ‰ง่กŒๅ‘ฝไปค: {}", display_cmd)
190}
191
192// ============================================================================
193// Core Functions
194// ============================================================================
195
196/// Determine whether approval is needed given the mode and risk level.
197pub fn needs_approval(mode: ApproveMode, risk: RiskLevel) -> bool {
198    match mode {
199        ApproveMode::Auto => false,
200        ApproveMode::Ask => risk >= RiskLevel::Mutating,
201        ApproveMode::Strict => true,
202    }
203}
204
205/// Convenience function: build request and prompt user.
206pub fn build_approval_request(tool_name: &str, risk: RiskLevel, params: &Value) -> ApprovalRequest {
207    ApprovalRequest::new(tool_name, risk, params)
208}
209
210/// Display the approval prompt and wait for user input.
211/// Returns the user's answer.
212pub fn prompt_approval(request: &ApprovalRequest) -> ApprovalAnswer {
213    println!();
214    println!("โ”Œโ”€ ็กฎ่ฎค่ฏทๆฑ‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
215    println!("โ”‚ {} {}", request.risk_level.icon(), request.summary);
216    println!("โ”‚ ้ฃŽ้™ฉ็ญ‰็บง: {}", request.risk_level);
217    println!("โ”‚");
218    println!("โ”‚ [y] ๆ‰ง่กŒ  [n] ่ทณ่ฟ‡  [a] ไธญๆญขๆœฌ่ฝฎ");
219    println!("โ””โ”€โ”€โ”€๏ฟฝ๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
220    print!("> ");
221    let _ = io::stdout().flush();
222
223    let answer = read_approval_answer();
224    println!();
225    answer
226}
227
228/// Read a single answer from stdin.
229fn read_approval_answer() -> ApprovalAnswer {
230    let stdin = io::stdin();
231    let mut line = String::new();
232    if stdin.lock().read_line(&mut line).is_err() {
233        return ApprovalAnswer::No;
234    }
235    match line.trim().to_lowercase().as_str() {
236        "y" | "yes" | "" => ApprovalAnswer::Yes,
237        "n" | "no" => ApprovalAnswer::No,
238        "a" | "abort" | "q" | "quit" => ApprovalAnswer::Abort,
239        _ => ApprovalAnswer::Yes, // default to yes for unrecognized input
240    }
241}