返回 CodeWhale
approval_mode.rs
根目录 / crates / execpolicy / src / approval_mode.rs
1 //! The user-facing approval posture (`Ask` / `Auto-Review` / `Full Access` /
2 //! `Never`). Lives beside `AskForApproval` so policy code and the TUI share
3 //! one definition; the TUI adds only presentation on top.
4
5 /// Determines when tool executions require user approval
6 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7 pub enum ApprovalMode {
8 /// Automatically review risky tool calls before deciding whether to ask.
9 Auto,
10 /// Bypass approvals entirely (YOLO mode / --yolo flag).
11 Bypass,
12 /// Suggest approval for non-safe tools (non-YOLO modes)
13 #[default]
14 Suggest,
15 /// Never execute tools requiring approval
16 Never,
17 }
18
19 impl ApprovalMode {
20 /// Shift+Tab permission cycle order (#0.8.68 M2).
21 pub const PERMISSION_CYCLE: [Self; 3] = [Self::Suggest, Self::Auto, Self::Bypass];
22
23 pub fn label(self) -> &'static str {
24 match self {
25 ApprovalMode::Auto => "AUTO",
26 ApprovalMode::Bypass => "BYPASS",
27 ApprovalMode::Suggest => "SUGGEST",
28 ApprovalMode::Never => "NEVER",
29 }
30 }
31
32 pub fn from_config_value(value: &str) -> Option<Self> {
33 match value.trim().to_ascii_lowercase().as_str() {
34 "auto" | "auto-review" | "auto_review" => Some(ApprovalMode::Auto),
35 "bypass" | "yolo" | "dontask" | "dont_ask" | "bypass-permissions"
36 | "bypasspermissions" | "full-access" | "full_access" | "full" => {
37 Some(ApprovalMode::Bypass)
38 }
39 "suggest" | "suggested" | "on-request" | "untrusted" | "ask" => {
40 Some(ApprovalMode::Suggest)
41 }
42 "never" | "deny" | "denied" => Some(ApprovalMode::Never),
43 _ => None,
44 }
45 }
46
47 #[must_use]
48 pub fn cycle_permission_next(self) -> Self {
49 let Some(index) = Self::PERMISSION_CYCLE.iter().position(|mode| *mode == self) else {
50 return Self::Suggest;
51 };
52 Self::PERMISSION_CYCLE[(index + 1) % Self::PERMISSION_CYCLE.len()]
53 }
54
55 #[must_use]
56 pub fn permission_chip_label(self) -> &'static str {
57 match self {
58 Self::Suggest => "Ask",
59 Self::Auto => "Auto-Review",
60 Self::Bypass => "Full Access",
61 Self::Never => "Never",
62 }
63 }
64 }
65
66 #[cfg(test)]
67 mod tests {
68 use super::*;
69
70 #[test]
71 fn test_approval_mode_labels() {
72 assert_eq!(ApprovalMode::Auto.label(), "AUTO");
73 assert_eq!(ApprovalMode::Suggest.label(), "SUGGEST");
74 assert_eq!(ApprovalMode::Never.label(), "NEVER");
75 }
76
77 #[test]
78 fn test_approval_mode_from_config_value_accepts_aliases() {
79 assert_eq!(
80 ApprovalMode::from_config_value("auto"),
81 Some(ApprovalMode::Auto)
82 );
83 assert_eq!(
84 ApprovalMode::from_config_value("on-request"),
85 Some(ApprovalMode::Suggest)
86 );
87 assert_eq!(
88 ApprovalMode::from_config_value("full_access"),
89 Some(ApprovalMode::Bypass)
90 );
91 assert_eq!(
92 ApprovalMode::from_config_value("deny"),
93 Some(ApprovalMode::Never)
94 );
95 assert_eq!(ApprovalMode::from_config_value("unknown"), None);
96 }
97
98 #[test]
99 fn permission_cycle_is_a_closed_loop_and_never_is_off_cycle() {
100 assert_eq!(
101 ApprovalMode::PERMISSION_CYCLE,
102 [
103 ApprovalMode::Suggest,
104 ApprovalMode::Auto,
105 ApprovalMode::Bypass
106 ]
107 );
108 assert_eq!(
109 ApprovalMode::Suggest.cycle_permission_next(),
110 ApprovalMode::Auto
111 );
112 assert_eq!(
113 ApprovalMode::Auto.cycle_permission_next(),
114 ApprovalMode::Bypass
115 );
116 assert_eq!(
117 ApprovalMode::Bypass.cycle_permission_next(),
118 ApprovalMode::Suggest
119 );
120 // Never is deliberately outside the Shift+Tab cycle; stepping from it
121 // must land on the safest posture rather than panicking or wrapping.
122 assert_eq!(
123 ApprovalMode::Never.cycle_permission_next(),
124 ApprovalMode::Suggest
125 );
126 }
127
128 #[test]
129 fn default_posture_is_suggest() {
130 assert_eq!(ApprovalMode::default(), ApprovalMode::Suggest);
131 }
132 }
133
133 lines RUST