| 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 |