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
84impl fmt::Display for ApproveMode {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            ApproveMode::Auto => write!(f, "auto"),
88            ApproveMode::Ask => write!(f, "ask"),
89            ApproveMode::Strict => write!(f, "strict"),
90        }
91    }
92}
93
94// ============================================================================
95// Approval Answer
96// ============================================================================
97
98/// User's response to an approval prompt.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum ApprovalAnswer {
101    /// Proceed with execution.
102    Yes,
103    /// Skip this tool call (return a "rejected" message to the AI).
104    No,
105    /// Abort the entire turn.
106    Abort,
107}
108
109// ============================================================================
110// Approval Request
111// ============================================================================
112
113/// A human-readable summary of what is about to happen.
114#[derive(Debug, Clone)]
115pub struct ApprovalRequest {
116    pub tool_name: String,
117    pub risk_level: RiskLevel,
118    pub summary: String,
119}
120
121impl ApprovalRequest {
122    /// Build an approval request from tool name, risk level, and parameters.
123    pub fn new(tool_name: &str, risk: RiskLevel, params: &Value) -> Self {
124        Self {
125            tool_name: tool_name.to_string(),
126            risk_level: risk,
127            summary: build_summary(tool_name, params),
128        }
129    }
130}
131
132/// Build a human-readable summary for the tool operation.
133fn build_summary(tool_name: &str, params: &Value) -> String {
134    match tool_name {
135        "write" => summary_write(params),
136        "edit" => summary_edit(params),
137        "multi_edit" => summary_multi_edit(params),
138        "bash" => summary_bash(params),
139        "todo_write" => "ๆ›ดๆ–ฐไปปๅŠกๆธ…ๅ•".to_string(),
140        _ => format!("ๆ‰ง่กŒๅทฅๅ…ท: {}", tool_name),
141    }
142}
143
144fn summary_write(params: &Value) -> String {
145    let path = params["path"].as_str().unwrap_or("<unknown>");
146    format!("ๅ†™ๅ…ฅๆ–‡ไปถ: {}", path)
147}
148
149fn summary_edit(params: &Value) -> String {
150    let path = params["path"].as_str().unwrap_or("<unknown>");
151    format!("็ผ–่พ‘ๆ–‡ไปถ: {}", path)
152}
153
154fn summary_multi_edit(params: &Value) -> String {
155    let path = params["path"].as_str().unwrap_or("<unknown>");
156    let count = params["edits"].as_array().map(|a| a.len()).unwrap_or(0);
157    format!("ๆ‰น้‡็ผ–่พ‘ๆ–‡ไปถ: {} ({} ๅค„ไฟฎๆ”น)", path, count)
158}
159
160fn summary_bash(params: &Value) -> String {
161    let cmd = params["command"].as_str().unwrap_or("<unknown>");
162    let display_cmd = if cmd.len() > 120 {
163        format!("{}...", &cmd[..120])
164    } else {
165        cmd.to_string()
166    };
167    format!("ๆ‰ง่กŒๅ‘ฝไปค: {}", display_cmd)
168}
169
170// ============================================================================
171// Core Functions
172// ============================================================================
173
174/// Determine whether approval is needed given the mode and risk level.
175pub fn needs_approval(mode: ApproveMode, risk: RiskLevel) -> bool {
176    match mode {
177        ApproveMode::Auto => false,
178        ApproveMode::Ask => risk >= RiskLevel::Mutating,
179        ApproveMode::Strict => true,
180    }
181}
182
183/// Convenience function: build request and prompt user.
184pub fn build_approval_request(tool_name: &str, risk: RiskLevel, params: &Value) -> ApprovalRequest {
185    ApprovalRequest::new(tool_name, risk, params)
186}
187
188/// Display the approval prompt and wait for user input.
189/// Returns the user's answer.
190pub fn prompt_approval(request: &ApprovalRequest) -> ApprovalAnswer {
191    println!();
192    println!("โ”Œโ”€ ็กฎ่ฎค่ฏทๆฑ‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
193    println!("โ”‚ {} {}", request.risk_level.icon(), request.summary);
194    println!("โ”‚ ้ฃŽ้™ฉ็ญ‰็บง: {}", request.risk_level);
195    println!("โ”‚");
196    println!("โ”‚ [y] ๆ‰ง่กŒ  [n] ่ทณ่ฟ‡  [a] ไธญๆญขๆœฌ่ฝฎ");
197    println!("โ””โ”€โ”€โ”€๏ฟฝ๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
198    print!("> ");
199    let _ = io::stdout().flush();
200
201    let answer = read_approval_answer();
202    println!();
203    answer
204}
205
206/// Read a single answer from stdin.
207fn read_approval_answer() -> ApprovalAnswer {
208    let stdin = io::stdin();
209    let mut line = String::new();
210    if stdin.lock().read_line(&mut line).is_err() {
211        return ApprovalAnswer::No;
212    }
213    match line.trim().to_lowercase().as_str() {
214        "y" | "yes" | "" => ApprovalAnswer::Yes,
215        "n" | "no" => ApprovalAnswer::No,
216        "a" | "abort" | "q" | "quit" => ApprovalAnswer::Abort,
217        _ => ApprovalAnswer::Yes, // default to yes for unrecognized input
218    }
219}