1use async_trait::async_trait;
66use serde::{Deserialize, Serialize};
67
68use crate::hook::HookPermission;
69use crate::types::ToolCallId;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum PermissionBehavior {
97 Allow,
99 Deny,
101 Ask,
103}
104
105impl PermissionBehavior {
106 pub fn is_allow(&self) -> bool {
107 matches!(self, Self::Allow)
108 }
109
110 pub fn is_deny(&self) -> bool {
111 matches!(self, Self::Deny)
112 }
113
114 pub fn is_ask(&self) -> bool {
115 matches!(self, Self::Ask)
116 }
117
118 pub fn strictness(&self) -> u8 {
122 match self {
123 Self::Allow => 0,
124 Self::Ask => 1,
125 Self::Deny => 2,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
148#[serde(rename_all = "snake_case")]
149pub enum PermissionMode {
150 #[default]
152 Default,
153
154 Plan,
156
157 AcceptEdits,
159
160 Bypass,
166
167 NonInteractive,
169}
170
171impl PermissionMode {
172 pub fn is_bypass(&self) -> bool {
174 matches!(self, Self::Bypass)
175 }
176
177 pub fn is_non_interactive(&self) -> bool {
179 matches!(self, Self::NonInteractive)
180 }
181
182 pub fn is_plan(&self) -> bool {
184 matches!(self, Self::Plan)
185 }
186
187 pub fn transform_ask(&self) -> PermissionBehavior {
193 match self {
194 Self::Bypass => PermissionBehavior::Allow,
195 Self::NonInteractive => PermissionBehavior::Deny,
196 _ => PermissionBehavior::Ask,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
225#[serde(rename_all = "snake_case")]
226pub enum RuleSource {
227 Policy,
231
232 User,
236
237 Project,
241
242 Session,
246
247 Default,
249}
250
251impl RuleSource {
252 pub fn priority(&self) -> u8 {
254 match self {
255 Self::Policy => 100,
256 Self::User => 80,
257 Self::Project => 60,
258 Self::Session => 40,
259 Self::Default => 0,
260 }
261 }
262
263 pub fn is_immutable(&self) -> bool {
265 matches!(self, Self::Policy)
266 }
267}
268
269impl PartialOrd for RuleSource {
270 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
271 Some(self.cmp(other))
272 }
273}
274
275impl Ord for RuleSource {
276 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
277 self.priority().cmp(&other.priority())
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
309pub struct PermissionRule {
310 pub source: RuleSource,
312
313 pub behavior: PermissionBehavior,
315
316 pub permission: String,
318
319 pub pattern: String,
321}
322
323impl PermissionRule {
324 pub fn new(
326 source: RuleSource,
327 behavior: PermissionBehavior,
328 permission: impl Into<String>,
329 pattern: impl Into<String>,
330 ) -> Self {
331 Self {
332 source,
333 behavior,
334 permission: permission.into(),
335 pattern: pattern.into(),
336 }
337 }
338
339 pub fn allow(source: RuleSource, permission: impl Into<String>, pattern: impl Into<String>) -> Self {
341 Self::new(source, PermissionBehavior::Allow, permission, pattern)
342 }
343
344 pub fn deny(source: RuleSource, permission: impl Into<String>, pattern: impl Into<String>) -> Self {
346 Self::new(source, PermissionBehavior::Deny, permission, pattern)
347 }
348
349 pub fn ask(source: RuleSource, permission: impl Into<String>, pattern: impl Into<String>) -> Self {
351 Self::new(source, PermissionBehavior::Ask, permission, pattern)
352 }
353
354 pub fn matches(&self, permission_key: &str, content: &str) -> bool {
356 wildcard_match(permission_key, &self.permission)
357 && wildcard_match(content, &self.pattern)
358 }
359}
360
361#[derive(Debug, Clone, Default, Serialize, Deserialize)]
392pub struct Ruleset {
393 rules: Vec<PermissionRule>,
394}
395
396impl Ruleset {
397 pub fn new() -> Self {
399 Self { rules: Vec::new() }
400 }
401
402 pub fn add(&mut self, rule: PermissionRule) {
404 self.rules.push(rule);
405 }
406
407 pub fn extend(&mut self, rules: impl IntoIterator<Item = PermissionRule>) {
409 self.rules.extend(rules);
410 }
411
412 pub fn remove_source(&mut self, source: RuleSource) {
414 self.rules.retain(|r| r.source != source);
415 }
416
417 pub fn len(&self) -> usize {
419 self.rules.len()
420 }
421
422 pub fn is_empty(&self) -> bool {
424 self.rules.is_empty()
425 }
426
427 pub fn rules(&self) -> &[PermissionRule] {
429 &self.rules
430 }
431
432 pub fn evaluate(&self, permission_key: &str, content: &str) -> Option<PermissionBehavior> {
455 for source in &[
457 RuleSource::Policy,
458 RuleSource::User,
459 RuleSource::Project,
460 RuleSource::Session,
461 RuleSource::Default,
462 ] {
463 let last_match = self
465 .rules
466 .iter()
467 .filter(|r| r.source == *source)
468 .filter(|r| r.matches(permission_key, content))
469 .last();
470
471 if let Some(rule) = last_match {
472 return Some(rule.behavior);
473 }
474 }
475
476 None
477 }
478
479 pub fn has_deny_for(&self, permission_key: &str) -> bool {
483 self.rules.iter().any(|r| {
484 r.behavior.is_deny() && wildcard_match(permission_key, &r.permission)
485 })
486 }
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct PermissionRequest {
511 pub permission: String,
513
514 pub patterns: Vec<String>,
519
520 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub tool_name: Option<String>,
523
524 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub call_id: Option<ToolCallId>,
527
528 #[serde(default)]
530 pub metadata: serde_json::Value,
531
532 #[serde(default, skip_serializing_if = "Vec::is_empty")]
537 pub always_allow_patterns: Vec<String>,
538}
539
540impl PermissionRequest {
541 pub fn new(permission: impl Into<String>, pattern: impl Into<String>) -> Self {
543 Self {
544 permission: permission.into(),
545 patterns: vec![pattern.into()],
546 tool_name: None,
547 call_id: None,
548 metadata: serde_json::Value::Null,
549 always_allow_patterns: Vec::new(),
550 }
551 }
552
553 pub fn with_patterns(
555 permission: impl Into<String>,
556 patterns: impl IntoIterator<Item = impl Into<String>>,
557 ) -> Self {
558 Self {
559 permission: permission.into(),
560 patterns: patterns.into_iter().map(Into::into).collect(),
561 tool_name: None,
562 call_id: None,
563 metadata: serde_json::Value::Null,
564 always_allow_patterns: Vec::new(),
565 }
566 }
567
568 pub fn with_tool_name(mut self, name: impl Into<String>) -> Self {
570 self.tool_name = Some(name.into());
571 self
572 }
573
574 pub fn with_call_id(mut self, id: ToolCallId) -> Self {
576 self.call_id = Some(id);
577 self
578 }
579
580 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
582 self.metadata = metadata;
583 self
584 }
585
586 pub fn with_always_allow(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
588 self.always_allow_patterns = patterns.into_iter().map(Into::into).collect();
589 self
590 }
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
616#[serde(tag = "behavior", rename_all = "snake_case")]
617pub enum PermissionDecision {
618 Allow {
620 reason: PermissionReason,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
623 updated_input: Option<serde_json::Value>,
624 },
625
626 Deny {
628 reason: PermissionReason,
629 message: String,
631 },
632
633 Ask {
635 message: String,
637 #[serde(default, skip_serializing_if = "Vec::is_empty")]
639 suggestions: Vec<PermissionUpdate>,
640 },
641}
642
643impl PermissionDecision {
644 pub fn allow(reason: PermissionReason) -> Self {
646 Self::Allow {
647 reason,
648 updated_input: None,
649 }
650 }
651
652 pub fn allow_with_input(reason: PermissionReason, updated_input: serde_json::Value) -> Self {
654 Self::Allow {
655 reason,
656 updated_input: Some(updated_input),
657 }
658 }
659
660 pub fn deny(reason: PermissionReason, message: impl Into<String>) -> Self {
662 Self::Deny {
663 reason,
664 message: message.into(),
665 }
666 }
667
668 pub fn ask(message: impl Into<String>) -> Self {
670 Self::Ask {
671 message: message.into(),
672 suggestions: Vec::new(),
673 }
674 }
675
676 pub fn ask_with_suggestions(
678 message: impl Into<String>,
679 suggestions: Vec<PermissionUpdate>,
680 ) -> Self {
681 Self::Ask {
682 message: message.into(),
683 suggestions,
684 }
685 }
686
687 pub fn is_allow(&self) -> bool {
688 matches!(self, Self::Allow { .. })
689 }
690
691 pub fn is_deny(&self) -> bool {
692 matches!(self, Self::Deny { .. })
693 }
694
695 pub fn is_ask(&self) -> bool {
696 matches!(self, Self::Ask { .. })
697 }
698
699 pub fn behavior(&self) -> PermissionBehavior {
701 match self {
702 Self::Allow { .. } => PermissionBehavior::Allow,
703 Self::Deny { .. } => PermissionBehavior::Deny,
704 Self::Ask { .. } => PermissionBehavior::Ask,
705 }
706 }
707}
708
709#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726#[serde(tag = "type", rename_all = "snake_case")]
727pub enum PermissionReason {
728 Rule { source: RuleSource },
730 Mode,
732 ToolCheck,
734 Hook,
736 SessionCache,
738 SafetyCheck,
740 UserDecision,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize)]
767#[serde(tag = "type", rename_all = "snake_case")]
768pub enum PermissionResult {
769 Allow,
771 Deny { message: String },
773 Ask { message: String },
775 Passthrough,
777}
778
779impl PermissionResult {
780 pub fn deny(message: impl Into<String>) -> Self {
782 Self::Deny {
783 message: message.into(),
784 }
785 }
786
787 pub fn ask(message: impl Into<String>) -> Self {
789 Self::Ask {
790 message: message.into(),
791 }
792 }
793
794 pub fn is_allow(&self) -> bool {
795 matches!(self, Self::Allow)
796 }
797
798 pub fn is_deny(&self) -> bool {
799 matches!(self, Self::Deny { .. })
800 }
801
802 pub fn is_ask(&self) -> bool {
803 matches!(self, Self::Ask { .. })
804 }
805
806 pub fn is_passthrough(&self) -> bool {
807 matches!(self, Self::Passthrough)
808 }
809}
810
811#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
837#[serde(tag = "type", rename_all = "snake_case")]
838pub enum PermissionReply {
839 AllowOnce,
841
842 AllowAlways,
844
845 DenyOnce,
847
848 DenyAlways,
850
851 DenyWithFeedback { feedback: String },
853}
854
855impl PermissionReply {
856 pub fn is_allow(&self) -> bool {
857 matches!(self, Self::AllowOnce | Self::AllowAlways)
858 }
859
860 pub fn is_deny(&self) -> bool {
861 matches!(self, Self::DenyOnce | Self::DenyAlways | Self::DenyWithFeedback { .. })
862 }
863
864 pub fn is_always(&self) -> bool {
865 matches!(self, Self::AllowAlways | Self::DenyAlways)
866 }
867
868 pub fn has_feedback(&self) -> bool {
870 matches!(self, Self::DenyWithFeedback { .. })
871 }
872
873 pub fn feedback(&self) -> Option<&str> {
875 match self {
876 Self::DenyWithFeedback { feedback } => Some(feedback.as_str()),
877 _ => None,
878 }
879 }
880
881 pub fn behavior(&self) -> PermissionBehavior {
883 if self.is_allow() {
884 PermissionBehavior::Allow
885 } else {
886 PermissionBehavior::Deny
887 }
888 }
889}
890
891#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
914#[serde(tag = "action", rename_all = "snake_case")]
915pub enum PermissionUpdate {
916 AddRule {
918 destination: RuleSource,
919 rule: PermissionRule,
920 },
921 RemoveRules {
923 destination: RuleSource,
924 permission: String,
925 pattern: String,
926 },
927 SetMode {
929 mode: PermissionMode,
930 },
931}
932
933impl PermissionUpdate {
934 pub fn add_rule(destination: RuleSource, rule: PermissionRule) -> Self {
936 Self::AddRule { destination, rule }
937 }
938
939 pub fn remove_rules(
941 destination: RuleSource,
942 permission: impl Into<String>,
943 pattern: impl Into<String>,
944 ) -> Self {
945 Self::RemoveRules {
946 destination,
947 permission: permission.into(),
948 pattern: pattern.into(),
949 }
950 }
951
952 pub fn set_mode(mode: PermissionMode) -> Self {
954 Self::SetMode { mode }
955 }
956}
957
958#[derive(Debug, Clone, Default, Serialize, Deserialize)]
987pub struct SessionPermissionCache {
988 rules: Vec<CacheEntry>,
989}
990
991#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
993struct CacheEntry {
994 permission: String,
995 pattern: String,
996 behavior: PermissionBehavior,
997}
998
999impl SessionPermissionCache {
1000 pub fn new() -> Self {
1002 Self { rules: Vec::new() }
1003 }
1004
1005 pub fn allow_always(&mut self, permission: impl Into<String>, pattern: impl Into<String>) {
1007 self.rules.push(CacheEntry {
1008 permission: permission.into(),
1009 pattern: pattern.into(),
1010 behavior: PermissionBehavior::Allow,
1011 });
1012 }
1013
1014 pub fn deny_always(&mut self, permission: impl Into<String>, pattern: impl Into<String>) {
1016 self.rules.push(CacheEntry {
1017 permission: permission.into(),
1018 pattern: pattern.into(),
1019 behavior: PermissionBehavior::Deny,
1020 });
1021 }
1022
1023 pub fn check(&self, permission_key: &str, content: &str) -> Option<PermissionBehavior> {
1027 self.rules
1028 .iter()
1029 .filter(|e| {
1030 wildcard_match(permission_key, &e.permission)
1031 && wildcard_match(content, &e.pattern)
1032 })
1033 .last()
1034 .map(|e| e.behavior)
1035 }
1036
1037 pub fn len(&self) -> usize {
1039 self.rules.len()
1040 }
1041
1042 pub fn is_empty(&self) -> bool {
1044 self.rules.is_empty()
1045 }
1046
1047 pub fn clear(&mut self) {
1049 self.rules.clear();
1050 }
1051
1052 pub fn to_ruleset(&self) -> Ruleset {
1054 let mut ruleset = Ruleset::new();
1055 for entry in &self.rules {
1056 ruleset.add(PermissionRule::new(
1057 RuleSource::Session,
1058 entry.behavior,
1059 &entry.permission,
1060 &entry.pattern,
1061 ));
1062 }
1063 ruleset
1064 }
1065}
1066
1067#[derive(Debug, Clone)]
1094pub struct DenialTracker {
1095 consecutive_denials: u32,
1097 threshold: u32,
1099 total_denials: u32,
1101}
1102
1103impl DenialTracker {
1104 pub fn new(threshold: u32) -> Self {
1106 Self {
1107 consecutive_denials: 0,
1108 threshold,
1109 total_denials: 0,
1110 }
1111 }
1112
1113 pub fn record_denial(&mut self) {
1115 self.consecutive_denials += 1;
1116 self.total_denials += 1;
1117 }
1118
1119 pub fn record_allow(&mut self) {
1121 self.consecutive_denials = 0;
1122 }
1123
1124 pub fn is_tripped(&self) -> bool {
1126 self.consecutive_denials >= self.threshold
1127 }
1128
1129 pub fn consecutive_count(&self) -> u32 {
1131 self.consecutive_denials
1132 }
1133
1134 pub fn total_count(&self) -> u32 {
1136 self.total_denials
1137 }
1138
1139 pub fn reset(&mut self) {
1141 self.consecutive_denials = 0;
1142 }
1143}
1144
1145impl From<HookPermission> for PermissionBehavior {
1166 fn from(hook_perm: HookPermission) -> Self {
1167 match hook_perm {
1168 HookPermission::Allow => Self::Allow,
1169 HookPermission::Deny { .. } => Self::Deny,
1170 HookPermission::Ask { .. } => Self::Ask,
1171 }
1172 }
1173}
1174
1175impl From<&HookPermission> for PermissionBehavior {
1176 fn from(hook_perm: &HookPermission) -> Self {
1177 match hook_perm {
1178 HookPermission::Allow => Self::Allow,
1179 HookPermission::Deny { .. } => Self::Deny,
1180 HookPermission::Ask { .. } => Self::Ask,
1181 }
1182 }
1183}
1184
1185impl From<PermissionBehavior> for HookPermission {
1187 fn from(behavior: PermissionBehavior) -> Self {
1188 match behavior {
1189 PermissionBehavior::Allow => Self::Allow,
1190 PermissionBehavior::Deny => Self::Deny { reason: None },
1191 PermissionBehavior::Ask => Self::Ask { message: None },
1192 }
1193 }
1194}
1195
1196#[derive(Debug, Clone)]
1204pub struct PermissionCheckInput {
1205 pub request: PermissionRequest,
1207
1208 pub hook_decision: Option<HookPermission>,
1212
1213 pub tool_check: Option<PermissionResult>,
1217
1218 pub mode: PermissionMode,
1220}
1221
1222impl PermissionCheckInput {
1223 pub fn new(request: PermissionRequest, mode: PermissionMode) -> Self {
1225 Self {
1226 request,
1227 hook_decision: None,
1228 tool_check: None,
1229 mode,
1230 }
1231 }
1232
1233 pub fn with_hook_decision(mut self, decision: HookPermission) -> Self {
1235 self.hook_decision = Some(decision);
1236 self
1237 }
1238
1239 pub fn with_tool_check(mut self, result: PermissionResult) -> Self {
1241 self.tool_check = Some(result);
1242 self
1243 }
1244}
1245
1246#[async_trait]
1314pub trait PermissionEngine: Send + Sync {
1315 async fn check(&self, input: PermissionCheckInput) -> PermissionDecision;
1322
1323 async fn prompt_user(&self, decision: &PermissionDecision) -> PermissionReply;
1333
1334 async fn apply_reply(
1338 &self,
1339 request: &PermissionRequest,
1340 reply: &PermissionReply,
1341 );
1342}
1343
1344pub fn evaluate_permission(
1371 ruleset: &Ruleset,
1372 cache: &SessionPermissionCache,
1373 input: &PermissionCheckInput,
1374) -> PermissionDecision {
1375 let permission_key = &input.request.permission;
1376
1377 let mut overall_behavior: Option<PermissionBehavior> = None;
1380
1381 for pattern in &input.request.patterns {
1382 let decision = evaluate_single(ruleset, cache, input, permission_key, pattern);
1383 match decision {
1384 PermissionBehavior::Deny => {
1385 let reason = determine_deny_reason(ruleset, input, permission_key, pattern);
1387 return PermissionDecision::deny(
1388 reason,
1389 format!("Permission denied: {}({})", permission_key, pattern),
1390 );
1391 }
1392 PermissionBehavior::Ask => {
1393 if overall_behavior != Some(PermissionBehavior::Deny) {
1394 overall_behavior = Some(PermissionBehavior::Ask);
1395 }
1396 }
1397 PermissionBehavior::Allow => {
1398 if overall_behavior.is_none() {
1399 overall_behavior = Some(PermissionBehavior::Allow);
1400 }
1401 }
1402 }
1403 }
1404
1405 if input.request.patterns.is_empty() {
1407 let decision = evaluate_single(ruleset, cache, input, permission_key, "*");
1408 overall_behavior = Some(decision);
1409 if decision.is_deny() {
1410 let reason = determine_deny_reason(ruleset, input, permission_key, "*");
1411 return PermissionDecision::deny(reason, format!("Permission denied: {}", permission_key));
1412 }
1413 }
1414
1415 match overall_behavior.unwrap_or(PermissionBehavior::Ask) {
1416 PermissionBehavior::Allow => {
1417 PermissionDecision::allow(PermissionReason::Rule { source: RuleSource::Session })
1418 }
1419 PermissionBehavior::Ask => {
1420 PermissionDecision::ask(format!(
1421 "Allow {} to execute {}?",
1422 input.request.tool_name.as_deref().unwrap_or(permission_key),
1423 input.request.patterns.first().map(|s| s.as_str()).unwrap_or("*"),
1424 ))
1425 }
1426 PermissionBehavior::Deny => {
1427 unreachable!("Deny should have been returned early in the loop")
1429 }
1430 }
1431}
1432
1433fn evaluate_single(
1435 ruleset: &Ruleset,
1436 cache: &SessionPermissionCache,
1437 input: &PermissionCheckInput,
1438 permission_key: &str,
1439 pattern: &str,
1440) -> PermissionBehavior {
1441 let policy_rules: Vec<_> = ruleset
1443 .rules()
1444 .iter()
1445 .filter(|r| r.source == RuleSource::Policy && r.behavior.is_deny())
1446 .filter(|r| r.matches(permission_key, pattern))
1447 .collect();
1448 if !policy_rules.is_empty() {
1449 return PermissionBehavior::Deny;
1450 }
1451
1452 if let Some(ref tool_result) = input.tool_check {
1454 match tool_result {
1455 PermissionResult::Deny { .. } => return PermissionBehavior::Deny,
1456 PermissionResult::Ask { .. } => { }
1457 _ => {}
1458 }
1459 }
1460
1461 if let Some(ref hook_decision) = input.hook_decision {
1463 if hook_decision.is_deny() {
1464 return PermissionBehavior::Deny;
1465 }
1466 }
1467
1468 if let Some(rule_behavior) = ruleset.evaluate(permission_key, pattern) {
1470 match rule_behavior {
1471 PermissionBehavior::Deny => return PermissionBehavior::Deny,
1472 PermissionBehavior::Allow => {
1473 if let Some(ref hook_decision) = input.hook_decision {
1475 if hook_decision.is_ask() {
1476 return PermissionBehavior::Ask;
1477 }
1478 }
1479 if let Some(PermissionResult::Ask { .. }) = input.tool_check {
1481 return PermissionBehavior::Ask;
1482 }
1483 return PermissionBehavior::Allow;
1484 }
1485 PermissionBehavior::Ask => { }
1486 }
1487 }
1488
1489 if let Some(cached) = cache.check(permission_key, pattern) {
1491 match cached {
1492 PermissionBehavior::Allow => return PermissionBehavior::Allow,
1493 PermissionBehavior::Deny => return PermissionBehavior::Deny,
1494 _ => {}
1495 }
1496 }
1497
1498 if let Some(ref hook_decision) = input.hook_decision {
1500 if hook_decision.is_allow() {
1501 return PermissionBehavior::Allow;
1502 }
1503 }
1504
1505 match input.mode {
1507 PermissionMode::Bypass => return PermissionBehavior::Allow,
1508 PermissionMode::NonInteractive => return PermissionBehavior::Deny,
1509 PermissionMode::Plan => return PermissionBehavior::Deny,
1510 _ => {}
1511 }
1512
1513 PermissionBehavior::Ask
1515}
1516
1517fn determine_deny_reason(
1519 ruleset: &Ruleset,
1520 input: &PermissionCheckInput,
1521 permission_key: &str,
1522 pattern: &str,
1523) -> PermissionReason {
1524 let is_policy = ruleset
1526 .rules()
1527 .iter()
1528 .any(|r| r.source == RuleSource::Policy && r.behavior.is_deny() && r.matches(permission_key, pattern));
1529 if is_policy {
1530 return PermissionReason::Rule { source: RuleSource::Policy };
1531 }
1532
1533 if let Some(ref hook) = input.hook_decision {
1535 if hook.is_deny() {
1536 return PermissionReason::Hook;
1537 }
1538 }
1539
1540 if let Some(PermissionResult::Deny { .. }) = &input.tool_check {
1542 return PermissionReason::ToolCheck;
1543 }
1544
1545 for source in &[RuleSource::User, RuleSource::Project, RuleSource::Session, RuleSource::Default] {
1547 let has_deny = ruleset
1548 .rules()
1549 .iter()
1550 .any(|r| r.source == *source && r.behavior.is_deny() && r.matches(permission_key, pattern));
1551 if has_deny {
1552 return PermissionReason::Rule { source: *source };
1553 }
1554 }
1555
1556 if input.mode.is_non_interactive() || input.mode.is_plan() {
1558 return PermissionReason::Mode;
1559 }
1560
1561 PermissionReason::Rule { source: RuleSource::Default }
1562}
1563
1564pub fn wildcard_match(value: &str, pattern: &str) -> bool {
1586 if pattern == "*" {
1587 return true;
1588 }
1589 if !pattern.contains('*') {
1590 return value == pattern;
1591 }
1592
1593 let parts: Vec<&str> = pattern.split('*').collect();
1594
1595 if !parts[0].is_empty() && !value.starts_with(parts[0]) {
1597 return false;
1598 }
1599
1600 let last = parts[parts.len() - 1];
1602 if !last.is_empty() && !value.ends_with(last) {
1603 return false;
1604 }
1605
1606 let mut pos = parts[0].len();
1608 for part in &parts[1..parts.len() - 1] {
1609 if part.is_empty() {
1610 continue;
1611 }
1612 match value[pos..].find(part) {
1613 Some(idx) => pos += idx + part.len(),
1614 None => return false,
1615 }
1616 }
1617
1618 if !last.is_empty() {
1620 let tail_start = value.len() - last.len();
1621 if pos > tail_start {
1622 return false;
1623 }
1624 }
1625
1626 true
1627}
1628
1629#[cfg(test)]
1634mod tests {
1635 use super::*;
1636 use serde_json::json;
1637
1638 #[test]
1641 fn test_behavior_variants() {
1642 assert!(PermissionBehavior::Allow.is_allow());
1643 assert!(PermissionBehavior::Deny.is_deny());
1644 assert!(PermissionBehavior::Ask.is_ask());
1645 }
1646
1647 #[test]
1648 fn test_behavior_strictness_order() {
1649 assert!(PermissionBehavior::Deny.strictness() > PermissionBehavior::Ask.strictness());
1650 assert!(PermissionBehavior::Ask.strictness() > PermissionBehavior::Allow.strictness());
1651 }
1652
1653 #[test]
1654 fn test_behavior_serde_roundtrip() {
1655 for b in [
1656 PermissionBehavior::Allow,
1657 PermissionBehavior::Deny,
1658 PermissionBehavior::Ask,
1659 ] {
1660 let json_str = serde_json::to_string(&b).unwrap();
1661 let restored: PermissionBehavior = serde_json::from_str(&json_str).unwrap();
1662 assert_eq!(b, restored);
1663 }
1664 }
1665
1666 #[test]
1669 fn test_mode_default() {
1670 assert_eq!(PermissionMode::default(), PermissionMode::Default);
1671 }
1672
1673 #[test]
1674 fn test_mode_transform_ask() {
1675 assert_eq!(PermissionMode::Default.transform_ask(), PermissionBehavior::Ask);
1676 assert_eq!(PermissionMode::Bypass.transform_ask(), PermissionBehavior::Allow);
1677 assert_eq!(PermissionMode::NonInteractive.transform_ask(), PermissionBehavior::Deny);
1678 assert_eq!(PermissionMode::Plan.transform_ask(), PermissionBehavior::Ask);
1679 assert_eq!(PermissionMode::AcceptEdits.transform_ask(), PermissionBehavior::Ask);
1680 }
1681
1682 #[test]
1683 fn test_mode_serde_roundtrip() {
1684 for mode in [
1685 PermissionMode::Default,
1686 PermissionMode::Plan,
1687 PermissionMode::AcceptEdits,
1688 PermissionMode::Bypass,
1689 PermissionMode::NonInteractive,
1690 ] {
1691 let json_str = serde_json::to_string(&mode).unwrap();
1692 let restored: PermissionMode = serde_json::from_str(&json_str).unwrap();
1693 assert_eq!(mode, restored);
1694 }
1695 }
1696
1697 #[test]
1700 fn test_rule_source_priority_order() {
1701 assert!(RuleSource::Policy.priority() > RuleSource::User.priority());
1702 assert!(RuleSource::User.priority() > RuleSource::Project.priority());
1703 assert!(RuleSource::Project.priority() > RuleSource::Session.priority());
1704 assert!(RuleSource::Session.priority() > RuleSource::Default.priority());
1705 }
1706
1707 #[test]
1708 fn test_rule_source_ord() {
1709 let mut sources = vec![
1710 RuleSource::Session,
1711 RuleSource::Policy,
1712 RuleSource::Default,
1713 RuleSource::User,
1714 RuleSource::Project,
1715 ];
1716 sources.sort();
1717 assert_eq!(
1718 sources,
1719 vec![
1720 RuleSource::Default,
1721 RuleSource::Session,
1722 RuleSource::Project,
1723 RuleSource::User,
1724 RuleSource::Policy,
1725 ]
1726 );
1727 }
1728
1729 #[test]
1730 fn test_rule_source_immutable() {
1731 assert!(RuleSource::Policy.is_immutable());
1732 assert!(!RuleSource::User.is_immutable());
1733 assert!(!RuleSource::Session.is_immutable());
1734 }
1735
1736 #[test]
1739 fn test_rule_new() {
1740 let rule = PermissionRule::new(
1741 RuleSource::User,
1742 PermissionBehavior::Allow,
1743 "read",
1744 "*",
1745 );
1746 assert_eq!(rule.source, RuleSource::User);
1747 assert_eq!(rule.behavior, PermissionBehavior::Allow);
1748 assert_eq!(rule.permission, "read");
1749 assert_eq!(rule.pattern, "*");
1750 }
1751
1752 #[test]
1753 fn test_rule_shortcuts() {
1754 let allow = PermissionRule::allow(RuleSource::User, "read", "*");
1755 assert!(allow.behavior.is_allow());
1756
1757 let deny = PermissionRule::deny(RuleSource::Policy, "bash", "rm *");
1758 assert!(deny.behavior.is_deny());
1759
1760 let ask = PermissionRule::ask(RuleSource::Project, "edit", "*.env");
1761 assert!(ask.behavior.is_ask());
1762 }
1763
1764 #[test]
1765 fn test_rule_matches_exact() {
1766 let rule = PermissionRule::deny(RuleSource::Policy, "bash", "rm -rf /");
1767 assert!(rule.matches("bash", "rm -rf /"));
1768 assert!(!rule.matches("bash", "ls -la"));
1769 assert!(!rule.matches("edit", "rm -rf /"));
1770 }
1771
1772 #[test]
1773 fn test_rule_matches_wildcard() {
1774 let rule = PermissionRule::deny(RuleSource::Policy, "bash", "rm *");
1775 assert!(rule.matches("bash", "rm -rf /"));
1776 assert!(rule.matches("bash", "rm file.txt"));
1777 assert!(!rule.matches("bash", "ls -la"));
1778 }
1779
1780 #[test]
1781 fn test_rule_matches_permission_wildcard() {
1782 let rule = PermissionRule::allow(RuleSource::User, "*", "*");
1783 assert!(rule.matches("bash", "anything"));
1784 assert!(rule.matches("edit", "anything"));
1785 }
1786
1787 #[test]
1788 fn test_rule_serde_roundtrip() {
1789 let rule = PermissionRule::deny(RuleSource::Policy, "bash", "rm *");
1790 let json_str = serde_json::to_string(&rule).unwrap();
1791 let restored: PermissionRule = serde_json::from_str(&json_str).unwrap();
1792 assert_eq!(rule, restored);
1793 }
1794
1795 #[test]
1798 fn test_ruleset_empty() {
1799 let ruleset = Ruleset::new();
1800 assert!(ruleset.is_empty());
1801 assert_eq!(ruleset.evaluate("bash", "ls"), None);
1802 }
1803
1804 #[test]
1805 fn test_ruleset_single_rule() {
1806 let mut ruleset = Ruleset::new();
1807 ruleset.add(PermissionRule::allow(RuleSource::User, "read", "*"));
1808 assert_eq!(ruleset.evaluate("read", "file.txt"), Some(PermissionBehavior::Allow));
1809 assert_eq!(ruleset.evaluate("bash", "ls"), None);
1810 }
1811
1812 #[test]
1813 fn test_ruleset_last_match_wins_same_source() {
1814 let mut ruleset = Ruleset::new();
1815 ruleset.add(PermissionRule::allow(RuleSource::User, "bash", "*"));
1816 ruleset.add(PermissionRule::deny(RuleSource::User, "bash", "rm *"));
1817
1818 assert_eq!(ruleset.evaluate("bash", "rm -rf /"), Some(PermissionBehavior::Deny));
1820 assert_eq!(ruleset.evaluate("bash", "ls"), Some(PermissionBehavior::Allow));
1822 }
1823
1824 #[test]
1825 fn test_ruleset_higher_source_wins() {
1826 let mut ruleset = Ruleset::new();
1827 ruleset.add(PermissionRule::allow(RuleSource::Session, "bash", "*"));
1829 ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
1831
1832 assert_eq!(ruleset.evaluate("bash", "rm -rf /"), Some(PermissionBehavior::Deny));
1834 assert_eq!(ruleset.evaluate("bash", "ls"), Some(PermissionBehavior::Allow));
1836 }
1837
1838 #[test]
1839 fn test_ruleset_policy_deny_cannot_be_overridden() {
1840 let mut ruleset = Ruleset::new();
1841 ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
1842 ruleset.add(PermissionRule::allow(RuleSource::User, "bash", "*"));
1843 ruleset.add(PermissionRule::allow(RuleSource::Session, "bash", "rm *"));
1844
1845 assert_eq!(ruleset.evaluate("bash", "rm file"), Some(PermissionBehavior::Deny));
1847 }
1848
1849 #[test]
1850 fn test_ruleset_remove_source() {
1851 let mut ruleset = Ruleset::new();
1852 ruleset.add(PermissionRule::allow(RuleSource::User, "read", "*"));
1853 ruleset.add(PermissionRule::deny(RuleSource::Session, "bash", "*"));
1854 assert_eq!(ruleset.len(), 2);
1855
1856 ruleset.remove_source(RuleSource::Session);
1857 assert_eq!(ruleset.len(), 1);
1858 assert_eq!(ruleset.evaluate("bash", "ls"), None);
1859 }
1860
1861 #[test]
1862 fn test_ruleset_has_deny_for() {
1863 let mut ruleset = Ruleset::new();
1864 ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
1865 ruleset.add(PermissionRule::allow(RuleSource::User, "read", "*"));
1866
1867 assert!(ruleset.has_deny_for("bash"));
1868 assert!(!ruleset.has_deny_for("read"));
1869 assert!(!ruleset.has_deny_for("edit"));
1870 }
1871
1872 #[test]
1875 fn test_permission_request_builder() {
1876 let req = PermissionRequest::new("bash", "rm -rf /tmp")
1877 .with_tool_name("bash")
1878 .with_call_id(ToolCallId::new("call_1"))
1879 .with_metadata(json!({"cwd": "/home/user"}))
1880 .with_always_allow(["rm *"]);
1881
1882 assert_eq!(req.permission, "bash");
1883 assert_eq!(req.patterns, vec!["rm -rf /tmp"]);
1884 assert_eq!(req.tool_name, Some("bash".into()));
1885 assert_eq!(req.call_id.unwrap().as_str(), "call_1");
1886 assert_eq!(req.always_allow_patterns, vec!["rm *"]);
1887 }
1888
1889 #[test]
1890 fn test_permission_request_multi_pattern() {
1891 let req = PermissionRequest::with_patterns("edit", ["src/main.rs", "src/lib.rs"]);
1892 assert_eq!(req.patterns.len(), 2);
1893 }
1894
1895 #[test]
1898 fn test_decision_allow() {
1899 let d = PermissionDecision::allow(PermissionReason::Rule {
1900 source: RuleSource::User,
1901 });
1902 assert!(d.is_allow());
1903 assert_eq!(d.behavior(), PermissionBehavior::Allow);
1904 }
1905
1906 #[test]
1907 fn test_decision_deny() {
1908 let d = PermissionDecision::deny(
1909 PermissionReason::Rule {
1910 source: RuleSource::Policy,
1911 },
1912 "Blocked by policy",
1913 );
1914 assert!(d.is_deny());
1915 if let PermissionDecision::Deny { message, .. } = &d {
1916 assert_eq!(message, "Blocked by policy");
1917 }
1918 }
1919
1920 #[test]
1921 fn test_decision_ask() {
1922 let d = PermissionDecision::ask("Allow bash to run 'rm -rf /tmp'?");
1923 assert!(d.is_ask());
1924 }
1925
1926 #[test]
1927 fn test_decision_ask_with_suggestions() {
1928 let d = PermissionDecision::ask_with_suggestions(
1929 "Allow?",
1930 vec![PermissionUpdate::add_rule(
1931 RuleSource::Session,
1932 PermissionRule::allow(RuleSource::Session, "bash", "rm *"),
1933 )],
1934 );
1935 if let PermissionDecision::Ask { suggestions, .. } = &d {
1936 assert_eq!(suggestions.len(), 1);
1937 }
1938 }
1939
1940 #[test]
1943 fn test_permission_result_variants() {
1944 assert!(PermissionResult::Allow.is_allow());
1945 assert!(PermissionResult::deny("no").is_deny());
1946 assert!(PermissionResult::ask("confirm?").is_ask());
1947 assert!(PermissionResult::Passthrough.is_passthrough());
1948 }
1949
1950 #[test]
1953 fn test_reply_is_allow() {
1954 assert!(PermissionReply::AllowOnce.is_allow());
1955 assert!(PermissionReply::AllowAlways.is_allow());
1956 assert!(!PermissionReply::DenyOnce.is_allow());
1957 }
1958
1959 #[test]
1960 fn test_reply_is_deny() {
1961 assert!(PermissionReply::DenyOnce.is_deny());
1962 assert!(PermissionReply::DenyAlways.is_deny());
1963 assert!(PermissionReply::DenyWithFeedback {
1964 feedback: "use a safer command".into()
1965 }
1966 .is_deny());
1967 assert!(!PermissionReply::AllowOnce.is_deny());
1968 }
1969
1970 #[test]
1971 fn test_reply_is_always() {
1972 assert!(PermissionReply::AllowAlways.is_always());
1973 assert!(PermissionReply::DenyAlways.is_always());
1974 assert!(!PermissionReply::AllowOnce.is_always());
1975 assert!(!PermissionReply::DenyOnce.is_always());
1976 }
1977
1978 #[test]
1979 fn test_reply_feedback() {
1980 let reply = PermissionReply::DenyWithFeedback {
1981 feedback: "try ls instead".into(),
1982 };
1983 assert!(reply.has_feedback());
1984 assert_eq!(reply.feedback(), Some("try ls instead"));
1985
1986 assert!(!PermissionReply::DenyOnce.has_feedback());
1987 assert_eq!(PermissionReply::DenyOnce.feedback(), None);
1988 }
1989
1990 #[test]
1991 fn test_reply_serde_roundtrip() {
1992 for reply in [
1993 PermissionReply::AllowOnce,
1994 PermissionReply::AllowAlways,
1995 PermissionReply::DenyOnce,
1996 PermissionReply::DenyAlways,
1997 PermissionReply::DenyWithFeedback {
1998 feedback: "feedback".into(),
1999 },
2000 ] {
2001 let json_str = serde_json::to_string(&reply).unwrap();
2002 let restored: PermissionReply = serde_json::from_str(&json_str).unwrap();
2003 assert_eq!(reply, restored);
2004 }
2005 }
2006
2007 #[test]
2010 fn test_cache_empty() {
2011 let cache = SessionPermissionCache::new();
2012 assert!(cache.is_empty());
2013 assert_eq!(cache.check("bash", "ls"), None);
2014 }
2015
2016 #[test]
2017 fn test_cache_allow_always() {
2018 let mut cache = SessionPermissionCache::new();
2019 cache.allow_always("bash", "git *");
2020
2021 assert_eq!(cache.check("bash", "git pull"), Some(PermissionBehavior::Allow));
2022 assert_eq!(cache.check("bash", "git push"), Some(PermissionBehavior::Allow));
2023 assert_eq!(cache.check("bash", "rm -rf /"), None);
2024 }
2025
2026 #[test]
2027 fn test_cache_deny_always() {
2028 let mut cache = SessionPermissionCache::new();
2029 cache.deny_always("bash", "rm *");
2030
2031 assert_eq!(cache.check("bash", "rm file.txt"), Some(PermissionBehavior::Deny));
2032 assert_eq!(cache.check("bash", "ls"), None);
2033 }
2034
2035 #[test]
2036 fn test_cache_last_match_wins() {
2037 let mut cache = SessionPermissionCache::new();
2038 cache.allow_always("bash", "*");
2039 cache.deny_always("bash", "rm *");
2040
2041 assert_eq!(cache.check("bash", "rm file"), Some(PermissionBehavior::Deny));
2043 assert_eq!(cache.check("bash", "ls"), Some(PermissionBehavior::Allow));
2045 }
2046
2047 #[test]
2048 fn test_cache_clear() {
2049 let mut cache = SessionPermissionCache::new();
2050 cache.allow_always("bash", "*");
2051 assert!(!cache.is_empty());
2052
2053 cache.clear();
2054 assert!(cache.is_empty());
2055 assert_eq!(cache.check("bash", "ls"), None);
2056 }
2057
2058 #[test]
2059 fn test_cache_to_ruleset() {
2060 let mut cache = SessionPermissionCache::new();
2061 cache.allow_always("bash", "git *");
2062 cache.deny_always("bash", "rm *");
2063
2064 let ruleset = cache.to_ruleset();
2065 assert_eq!(ruleset.len(), 2);
2066 assert_eq!(ruleset.evaluate("bash", "git pull"), Some(PermissionBehavior::Allow));
2067 assert_eq!(ruleset.evaluate("bash", "rm file"), Some(PermissionBehavior::Deny));
2068 }
2069
2070 #[test]
2073 fn test_denial_tracker_basic() {
2074 let tracker = DenialTracker::new(3);
2075 assert!(!tracker.is_tripped());
2076 assert_eq!(tracker.consecutive_count(), 0);
2077 assert_eq!(tracker.total_count(), 0);
2078 }
2079
2080 #[test]
2081 fn test_denial_tracker_trips_at_threshold() {
2082 let mut tracker = DenialTracker::new(3);
2083 tracker.record_denial();
2084 tracker.record_denial();
2085 assert!(!tracker.is_tripped());
2086
2087 tracker.record_denial();
2088 assert!(tracker.is_tripped());
2089 assert_eq!(tracker.consecutive_count(), 3);
2090 assert_eq!(tracker.total_count(), 3);
2091 }
2092
2093 #[test]
2094 fn test_denial_tracker_allow_resets() {
2095 let mut tracker = DenialTracker::new(3);
2096 tracker.record_denial();
2097 tracker.record_denial();
2098 tracker.record_allow(); assert!(!tracker.is_tripped());
2101 assert_eq!(tracker.consecutive_count(), 0);
2102 assert_eq!(tracker.total_count(), 2); }
2104
2105 #[test]
2106 fn test_denial_tracker_reset() {
2107 let mut tracker = DenialTracker::new(2);
2108 tracker.record_denial();
2109 tracker.record_denial();
2110 assert!(tracker.is_tripped());
2111
2112 tracker.reset();
2113 assert!(!tracker.is_tripped());
2114 }
2115
2116 #[test]
2119 fn test_wildcard_exact() {
2120 assert!(wildcard_match("hello", "hello"));
2121 assert!(!wildcard_match("hello", "world"));
2122 }
2123
2124 #[test]
2125 fn test_wildcard_star_all() {
2126 assert!(wildcard_match("anything", "*"));
2127 assert!(wildcard_match("", "*"));
2128 }
2129
2130 #[test]
2131 fn test_wildcard_prefix() {
2132 assert!(wildcard_match("hello world", "hello *"));
2133 assert!(wildcard_match("hello", "hello*"));
2134 assert!(!wildcard_match("world hello", "hello *"));
2135 }
2136
2137 #[test]
2138 fn test_wildcard_suffix() {
2139 assert!(wildcard_match("file.rs", "*.rs"));
2140 assert!(wildcard_match("test.rs", "*.rs"));
2141 assert!(!wildcard_match("file.ts", "*.rs"));
2142 }
2143
2144 #[test]
2145 fn test_wildcard_middle() {
2146 assert!(wildcard_match("/etc/shadow", "/etc/*"));
2147 assert!(wildcard_match("/home/user/file.txt", "/home/*/file.txt"));
2148 assert!(!wildcard_match("/tmp/file.txt", "/home/*/file.txt"));
2149 }
2150
2151 #[test]
2152 fn test_wildcard_multiple_stars() {
2153 assert!(wildcard_match("/home/user/project/src/main.rs", "/home/*/project/*.rs"));
2154 assert!(!wildcard_match("/home/user/other/src/main.rs", "/home/*/project/*.rs"));
2155 }
2156
2157 #[test]
2158 fn test_wildcard_empty_pattern_segment() {
2159 assert!(wildcard_match("anything", "**"));
2161 }
2162
2163 #[test]
2166 fn test_permission_update_add_rule() {
2167 let update = PermissionUpdate::add_rule(
2168 RuleSource::Session,
2169 PermissionRule::allow(RuleSource::Session, "bash", "git *"),
2170 );
2171 assert!(matches!(update, PermissionUpdate::AddRule { .. }));
2172 }
2173
2174 #[test]
2175 fn test_permission_update_serde_roundtrip() {
2176 let update = PermissionUpdate::set_mode(PermissionMode::Bypass);
2177 let json_str = serde_json::to_string(&update).unwrap();
2178 let restored: PermissionUpdate = serde_json::from_str(&json_str).unwrap();
2179 assert_eq!(update, restored);
2180 }
2181
2182 #[test]
2211 fn test_pipeline_policy_deny_beats_everything() {
2212 let mut ruleset = Ruleset::new();
2213 ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
2214
2215 let cache = SessionPermissionCache::new();
2216
2217 let input = PermissionCheckInput::new(
2219 PermissionRequest::new("bash", "rm -rf /"),
2220 PermissionMode::Bypass,
2221 )
2222 .with_hook_decision(HookPermission::Allow);
2223
2224 let decision = evaluate_permission(&ruleset, &cache, &input);
2225 assert!(decision.is_deny());
2226 if let PermissionDecision::Deny { reason, .. } = &decision {
2227 assert!(matches!(reason, PermissionReason::Rule { source: RuleSource::Policy }));
2228 }
2229 }
2230
2231 #[test]
2232 fn test_pipeline_hook_deny_beats_rule_allow() {
2233 let mut ruleset = Ruleset::new();
2234 ruleset.add(PermissionRule::allow(RuleSource::User, "bash", "*"));
2235
2236 let cache = SessionPermissionCache::new();
2237
2238 let input = PermissionCheckInput::new(
2240 PermissionRequest::new("bash", "curl evil.com"),
2241 PermissionMode::Default,
2242 )
2243 .with_hook_decision(HookPermission::Deny {
2244 reason: Some("Suspicious URL detected".into()),
2245 });
2246
2247 let decision = evaluate_permission(&ruleset, &cache, &input);
2248 assert!(decision.is_deny());
2249 if let PermissionDecision::Deny { reason, .. } = &decision {
2250 assert!(matches!(reason, PermissionReason::Hook));
2251 }
2252 }
2253
2254 #[test]
2255 fn test_pipeline_hook_allow_cannot_bypass_rule_deny() {
2256 let mut ruleset = Ruleset::new();
2257 ruleset.add(PermissionRule::deny(RuleSource::User, "bash", "rm *"));
2258
2259 let cache = SessionPermissionCache::new();
2260
2261 let input = PermissionCheckInput::new(
2263 PermissionRequest::new("bash", "rm file.txt"),
2264 PermissionMode::Default,
2265 )
2266 .with_hook_decision(HookPermission::Allow);
2267
2268 let decision = evaluate_permission(&ruleset, &cache, &input);
2269 assert!(decision.is_deny());
2270 }
2271
2272 #[test]
2273 fn test_pipeline_tool_deny_is_immediate() {
2274 let ruleset = Ruleset::new(); let cache = SessionPermissionCache::new();
2276
2277 let input = PermissionCheckInput::new(
2279 PermissionRequest::new("edit", "/etc/shadow"),
2280 PermissionMode::Bypass, )
2282 .with_tool_check(PermissionResult::deny("Cannot edit system files"));
2283
2284 let decision = evaluate_permission(&ruleset, &cache, &input);
2285 assert!(decision.is_deny());
2286 if let PermissionDecision::Deny { reason, .. } = &decision {
2287 assert!(matches!(reason, PermissionReason::ToolCheck));
2288 }
2289 }
2290
2291 #[test]
2292 fn test_pipeline_hook_ask_overrides_rule_allow() {
2293 let mut ruleset = Ruleset::new();
2294 ruleset.add(PermissionRule::allow(RuleSource::User, "bash", "*"));
2295
2296 let cache = SessionPermissionCache::new();
2297
2298 let input = PermissionCheckInput::new(
2300 PermissionRequest::new("bash", "git push --force"),
2301 PermissionMode::Default,
2302 )
2303 .with_hook_decision(HookPermission::Ask {
2304 message: Some("Force push detected, confirm?".into()),
2305 });
2306
2307 let decision = evaluate_permission(&ruleset, &cache, &input);
2308 assert!(decision.is_ask());
2309 }
2310
2311 #[test]
2312 fn test_pipeline_cache_allows_after_user_always() {
2313 let ruleset = Ruleset::new(); let mut cache = SessionPermissionCache::new();
2315 cache.allow_always("bash", "git *");
2316
2317 let input = PermissionCheckInput::new(
2318 PermissionRequest::new("bash", "git pull"),
2319 PermissionMode::Default,
2320 );
2321
2322 let decision = evaluate_permission(&ruleset, &cache, &input);
2323 assert!(decision.is_allow());
2324 }
2325
2326 #[test]
2327 fn test_pipeline_bypass_mode_auto_allows() {
2328 let ruleset = Ruleset::new();
2329 let cache = SessionPermissionCache::new();
2330
2331 let input = PermissionCheckInput::new(
2332 PermissionRequest::new("bash", "anything"),
2333 PermissionMode::Bypass,
2334 );
2335
2336 let decision = evaluate_permission(&ruleset, &cache, &input);
2337 assert!(decision.is_allow());
2338 }
2339
2340 #[test]
2341 fn test_pipeline_non_interactive_auto_denies() {
2342 let ruleset = Ruleset::new();
2343 let cache = SessionPermissionCache::new();
2344
2345 let input = PermissionCheckInput::new(
2346 PermissionRequest::new("bash", "anything"),
2347 PermissionMode::NonInteractive,
2348 );
2349
2350 let decision = evaluate_permission(&ruleset, &cache, &input);
2351 assert!(decision.is_deny());
2352 if let PermissionDecision::Deny { reason, .. } = &decision {
2353 assert!(matches!(reason, PermissionReason::Mode));
2354 }
2355 }
2356
2357 #[test]
2358 fn test_pipeline_no_rules_no_hook_defaults_to_ask() {
2359 let ruleset = Ruleset::new();
2360 let cache = SessionPermissionCache::new();
2361
2362 let input = PermissionCheckInput::new(
2363 PermissionRequest::new("bash", "ls -la"),
2364 PermissionMode::Default,
2365 );
2366
2367 let decision = evaluate_permission(&ruleset, &cache, &input);
2368 assert!(decision.is_ask());
2369 }
2370
2371 #[test]
2372 fn test_pipeline_hook_allow_works_when_no_rule_conflicts() {
2373 let ruleset = Ruleset::new(); let cache = SessionPermissionCache::new();
2375
2376 let input = PermissionCheckInput::new(
2378 PermissionRequest::new("read", "file.txt"),
2379 PermissionMode::Default,
2380 )
2381 .with_hook_decision(HookPermission::Allow);
2382
2383 let decision = evaluate_permission(&ruleset, &cache, &input);
2384 assert!(decision.is_allow());
2385 }
2386
2387 #[test]
2388 fn test_pipeline_tool_ask_overrides_rule_allow() {
2389 let mut ruleset = Ruleset::new();
2390 ruleset.add(PermissionRule::allow(RuleSource::User, "edit", "*"));
2391
2392 let cache = SessionPermissionCache::new();
2393
2394 let input = PermissionCheckInput::new(
2396 PermissionRequest::new("edit", ".git/config"),
2397 PermissionMode::Default,
2398 )
2399 .with_tool_check(PermissionResult::ask("Editing .git/ files requires confirmation"));
2400
2401 let decision = evaluate_permission(&ruleset, &cache, &input);
2402 assert!(decision.is_ask());
2403 }
2404
2405 #[test]
2408 fn test_hook_permission_to_behavior() {
2409 assert_eq!(
2410 PermissionBehavior::from(HookPermission::Allow),
2411 PermissionBehavior::Allow
2412 );
2413 assert_eq!(
2414 PermissionBehavior::from(HookPermission::Deny { reason: None }),
2415 PermissionBehavior::Deny
2416 );
2417 assert_eq!(
2418 PermissionBehavior::from(HookPermission::Ask { message: None }),
2419 PermissionBehavior::Ask
2420 );
2421 }
2422
2423 #[test]
2424 fn test_behavior_to_hook_permission() {
2425 let perm: HookPermission = PermissionBehavior::Allow.into();
2426 assert!(perm.is_allow());
2427
2428 let perm: HookPermission = PermissionBehavior::Deny.into();
2429 assert!(perm.is_deny());
2430 }
2431}