1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
10#[serde(rename_all = "lowercase")]
11pub enum PermissionBehavior {
12 Allow,
14 Deny,
16 #[default]
18 Ask,
19}
20
21impl PermissionBehavior {
22 pub fn as_str(&self) -> &'static str {
24 match self {
25 PermissionBehavior::Allow => "allow",
26 PermissionBehavior::Deny => "deny",
27 PermissionBehavior::Ask => "ask",
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
34#[serde(rename_all = "lowercase")]
35pub enum PermissionMode {
36 #[default]
38 Default,
39 AcceptEdits,
41 Bypass,
43 DontAsk,
45 Plan,
47 Auto,
49 Bubble,
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum PermissionRuleSource {
57 UserSettings,
59 ProjectSettings,
61 LocalSettings,
63 CliArg,
65 Session,
67 Policy,
69 FlagSettings,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct PermissionRule {
76 pub source: PermissionRuleSource,
78 pub behavior: PermissionBehavior,
80 pub tool_name: String,
82 pub rule_content: Option<String>,
84}
85
86impl PermissionRule {
87 pub fn new(tool_name: &str, behavior: PermissionBehavior) -> Self {
89 Self {
90 source: PermissionRuleSource::UserSettings,
91 behavior,
92 tool_name: tool_name.to_string(),
93 rule_content: None,
94 }
95 }
96
97 pub fn with_content(tool_name: &str, behavior: PermissionBehavior, content: &str) -> Self {
99 Self {
100 source: PermissionRuleSource::UserSettings,
101 behavior,
102 tool_name: tool_name.to_string(),
103 rule_content: Some(content.to_string()),
104 }
105 }
106
107 pub fn allow(tool_name: &str) -> Self {
109 Self::new(tool_name, PermissionBehavior::Allow)
110 }
111
112 pub fn deny(tool_name: &str) -> Self {
114 Self::new(tool_name, PermissionBehavior::Deny)
115 }
116
117 pub fn ask(tool_name: &str) -> Self {
119 Self::new(tool_name, PermissionBehavior::Ask)
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct PermissionMetadata {
126 pub tool_name: String,
128 pub description: Option<String>,
130 pub input: Option<serde_json::Value>,
132 pub cwd: Option<String>,
134}
135
136impl PermissionMetadata {
137 pub fn new(tool_name: &str) -> Self {
139 Self {
140 tool_name: tool_name.to_string(),
141 description: None,
142 input: None,
143 cwd: None,
144 }
145 }
146
147 pub fn with_description(mut self, description: &str) -> Self {
149 self.description = Some(description.to_string());
150 self
151 }
152
153 pub fn with_input(mut self, input: serde_json::Value) -> Self {
155 self.input = Some(input);
156 self
157 }
158
159 pub fn with_cwd(mut self, cwd: &str) -> Self {
161 self.cwd = Some(cwd.to_string());
162 self
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168#[serde(tag = "type", rename_all = "snake_case")]
169pub enum PermissionDecisionReason {
170 Rule { rule: PermissionRule },
172 Mode { mode: PermissionMode },
174 Hook {
176 hook_name: String,
177 reason: Option<String>,
178 },
179 SandboxOverride { reason: String },
181 SafetyCheck { reason: String },
183 Other { reason: String },
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct PermissionAllowDecision {
190 pub behavior: PermissionBehavior,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub updated_input: Option<serde_json::Value>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub user_modified: Option<bool>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub decision_reason: Option<PermissionDecisionReason>,
197}
198
199impl PermissionAllowDecision {
200 pub fn new() -> Self {
202 Self {
203 behavior: PermissionBehavior::Allow,
204 updated_input: None,
205 user_modified: None,
206 decision_reason: None,
207 }
208 }
209
210 pub fn with_reason(mut self, reason: PermissionDecisionReason) -> Self {
212 self.decision_reason = Some(reason);
213 self
214 }
215}
216
217impl Default for PermissionAllowDecision {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct PermissionAskDecision {
226 pub behavior: PermissionBehavior,
227 pub message: String,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub updated_input: Option<serde_json::Value>,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub decision_reason: Option<PermissionDecisionReason>,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub blocked_path: Option<String>,
234}
235
236impl PermissionAskDecision {
237 pub fn new(message: &str) -> Self {
239 Self {
240 behavior: PermissionBehavior::Ask,
241 message: message.to_string(),
242 updated_input: None,
243 decision_reason: None,
244 blocked_path: None,
245 }
246 }
247
248 pub fn with_reason(mut self, reason: PermissionDecisionReason) -> Self {
250 self.decision_reason = Some(reason);
251 self
252 }
253
254 pub fn with_blocked_path(mut self, path: &str) -> Self {
256 self.blocked_path = Some(path.to_string());
257 self
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct PermissionDenyDecision {
264 pub behavior: PermissionBehavior,
265 pub message: String,
266 pub decision_reason: PermissionDecisionReason,
267}
268
269impl PermissionDenyDecision {
270 pub fn new(message: &str, reason: PermissionDecisionReason) -> Self {
272 Self {
273 behavior: PermissionBehavior::Deny,
274 message: message.to_string(),
275 decision_reason: reason,
276 }
277 }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282#[serde(tag = "behavior", rename_all = "lowercase")]
283pub enum PermissionDecision {
284 Allow(PermissionAllowDecision),
285 Ask(PermissionAskDecision),
286 Deny(PermissionDenyDecision),
287}
288
289impl PermissionDecision {
290 pub fn is_allowed(&self) -> bool {
292 matches!(self, PermissionDecision::Allow(_))
293 }
294
295 pub fn is_denied(&self) -> bool {
297 matches!(self, PermissionDecision::Deny(_))
298 }
299
300 pub fn is_ask(&self) -> bool {
302 matches!(self, PermissionDecision::Ask(_))
303 }
304
305 pub fn message(&self) -> Option<&str> {
307 match self {
308 PermissionDecision::Allow(_) => None,
309 PermissionDecision::Ask(d) => Some(&d.message),
310 PermissionDecision::Deny(d) => Some(&d.message),
311 }
312 }
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(tag = "behavior", rename_all = "lowercase")]
318pub enum PermissionResult {
319 Allow(PermissionAllowDecision),
320 Ask(PermissionAskDecision),
321 Deny(PermissionDenyDecision),
322 Passthrough {
324 message: String,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 decision_reason: Option<PermissionDecisionReason>,
327 },
328}
329
330impl PermissionResult {
331 pub fn to_decision(self) -> Option<PermissionDecision> {
333 match self {
334 PermissionResult::Allow(d) => Some(PermissionDecision::Allow(d)),
335 PermissionResult::Ask(d) => Some(PermissionDecision::Ask(d)),
336 PermissionResult::Deny(d) => Some(PermissionDecision::Deny(d)),
337 PermissionResult::Passthrough { .. } => None,
338 }
339 }
340
341 pub fn is_allowed(&self) -> bool {
343 matches!(
344 self,
345 PermissionResult::Allow(_) | PermissionResult::Passthrough { .. }
346 )
347 }
348
349 pub fn is_denied(&self) -> bool {
351 matches!(self, PermissionResult::Deny(_))
352 }
353
354 pub fn is_ask(&self) -> bool {
356 matches!(self, PermissionResult::Ask(_))
357 }
358
359 pub fn message(&self) -> Option<&str> {
361 match self {
362 PermissionResult::Allow(_) => None,
363 PermissionResult::Ask(d) => Some(&d.message),
364 PermissionResult::Deny(d) => Some(&d.message),
365 PermissionResult::Passthrough { message, .. } => Some(message),
366 }
367 }
368}
369
370pub struct PermissionContext {
372 pub mode: PermissionMode,
374 pub allow_rules: Vec<PermissionRule>,
376 pub deny_rules: Vec<PermissionRule>,
378 pub ask_rules: Vec<PermissionRule>,
380 pub denial_tracking: std::sync::RwLock<crate::utils::permissions::denial_tracking::DenialTrackingState>,
382}
383
384impl std::fmt::Debug for PermissionContext {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 f.debug_struct("PermissionContext")
387 .field("mode", &self.mode)
388 .field("allow_rules", &self.allow_rules)
389 .field("deny_rules", &self.deny_rules)
390 .field("ask_rules", &self.ask_rules)
391 .finish_non_exhaustive()
392 }
393}
394
395impl Clone for PermissionContext {
396 fn clone(&self) -> Self {
397 let dt = self.denial_tracking.read().map(|dt| *dt).unwrap_or_default();
398 Self {
399 mode: self.mode,
400 allow_rules: self.allow_rules.clone(),
401 deny_rules: self.deny_rules.clone(),
402 ask_rules: self.ask_rules.clone(),
403 denial_tracking: std::sync::RwLock::new(dt),
404 }
405 }
406}
407
408impl Default for PermissionContext {
409 fn default() -> Self {
410 Self {
411 mode: PermissionMode::default(),
412 allow_rules: Vec::new(),
413 deny_rules: Vec::new(),
414 ask_rules: Vec::new(),
415 denial_tracking: std::sync::RwLock::new(
416 crate::utils::permissions::denial_tracking::DenialTrackingState::default(),
417 ),
418 }
419 }
420}
421
422fn tool_name_matches_rule(tool_name: &str, rule: &PermissionRule) -> bool {
429 let rule_tool = &rule.tool_name;
430
431 if rule_tool == tool_name {
433 return true;
434 }
435 if rule_tool.ends_with("__") && tool_name.starts_with(rule_tool.as_str()) {
437 return true;
438 }
439 if rule_tool.ends_with('_') && tool_name.starts_with(rule_tool.as_str()) {
441 return true;
442 }
443 if rule_tool == "*" {
445 return true;
446 }
447 false
448}
449
450impl PermissionContext {
451 pub fn new() -> Self {
453 Self::default()
454 }
455
456 pub fn with_mode(mut self, mode: PermissionMode) -> Self {
458 self.mode = mode;
459 self
460 }
461
462 pub fn with_allow_rule(mut self, rule: PermissionRule) -> Self {
464 self.allow_rules.push(rule);
465 self
466 }
467
468 pub fn with_deny_rule(mut self, rule: PermissionRule) -> Self {
470 self.deny_rules.push(rule);
471 self
472 }
473
474 pub fn with_ask_rule(mut self, rule: PermissionRule) -> Self {
476 self.ask_rules.push(rule);
477 self
478 }
479
480 pub fn with_denial_tracking(
482 mut self,
483 state: crate::utils::permissions::denial_tracking::DenialTrackingState,
484 ) -> Self {
485 let guard = self.denial_tracking.get_mut().unwrap();
486 *guard = state;
487 self
488 }
489
490 fn deny_rule_matches(&self, tool_name: &str, rule: &PermissionRule) -> bool {
493 if rule.rule_content.is_some() {
494 return false;
495 }
496 tool_name_matches_rule(tool_name, rule)
497 }
498
499 fn allow_rule_matches(
501 &self,
502 tool_name: &str,
503 input: Option<&serde_json::Value>,
504 rule: &PermissionRule,
505 ) -> bool {
506 if !tool_name_matches_rule(tool_name, rule) {
507 return false;
508 }
509 if let Some(content) = &rule.rule_content {
511 if let Some(input) = input {
512 let input_str = input.to_string();
513 return input_str.contains(content);
514 }
515 return false;
516 }
517 true
518 }
519
520 pub fn check_tool(
522 &self,
523 tool_name: &str,
524 input: Option<&serde_json::Value>,
525 ) -> PermissionResult {
526 for rule in &self.deny_rules {
528 if self.deny_rule_matches(tool_name, rule) {
529 return PermissionResult::Deny(PermissionDenyDecision::new(
530 &format!("Tool '{}' is denied by rule", tool_name),
531 PermissionDecisionReason::Rule { rule: rule.clone() },
532 ));
533 }
534 }
535
536 for rule in &self.allow_rules {
538 if self.allow_rule_matches(tool_name, input, rule) {
539 return PermissionResult::Allow(
540 PermissionAllowDecision::new()
541 .with_reason(PermissionDecisionReason::Rule { rule: rule.clone() }),
542 );
543 }
544 }
545
546 for rule in &self.ask_rules {
548 if self.deny_rule_matches(tool_name, rule) {
549 return PermissionResult::Ask(
550 PermissionAskDecision::new(&format!(
551 "Tool '{}' requires permission",
552 tool_name
553 ))
554 .with_reason(PermissionDecisionReason::Rule { rule: rule.clone() }),
555 );
556 }
557 }
558
559 match self.mode {
561 PermissionMode::Bypass => {
562 return PermissionResult::Allow(PermissionAllowDecision::new().with_reason(
563 PermissionDecisionReason::Mode {
564 mode: PermissionMode::Bypass,
565 },
566 ));
567 }
568 PermissionMode::DontAsk => {
569 return PermissionResult::Deny(PermissionDenyDecision::new(
570 "Permission mode is 'dontAsk'",
571 PermissionDecisionReason::Mode {
572 mode: PermissionMode::DontAsk,
573 },
574 ));
575 }
576 PermissionMode::AcceptEdits => {
577 if tool_name == "Write" || tool_name == "Edit" || tool_name == "Bash" {
579 return PermissionResult::Allow(PermissionAllowDecision::new().with_reason(
580 PermissionDecisionReason::Mode {
581 mode: PermissionMode::AcceptEdits,
582 },
583 ));
584 }
585 }
586 PermissionMode::Bubble => {
587 let safe_tools = ["Read", "Glob", "Grep", "Search", "WebFetch", "WebSearch"];
590 if safe_tools.iter().any(|&t| t == tool_name) {
591 return PermissionResult::Allow(PermissionAllowDecision::new().with_reason(
592 PermissionDecisionReason::Mode {
593 mode: PermissionMode::Bubble,
594 },
595 ));
596 }
597 if let Some(input_val) = input {
599 let input_str = input_val.to_string();
600 let dangerous_patterns = [
602 "rm -rf",
603 "rm /",
604 "del /",
605 "format",
606 "dd if=",
607 "> /dev/sd",
608 "chmod 777",
609 "chown -R",
610 ];
611 for pattern in dangerous_patterns {
612 if input_str.contains(pattern) {
613 return PermissionResult::Ask(
615 PermissionAskDecision::new(&format!(
616 "Tool '{}' contains potentially dangerous pattern: {}",
617 tool_name, pattern
618 ))
619 .with_reason(
620 PermissionDecisionReason::Mode {
621 mode: PermissionMode::Bubble,
622 },
623 ),
624 );
625 }
626 }
627 }
628 if tool_name == "Write"
630 || tool_name == "Edit"
631 || tool_name == "Bash"
632 || tool_name == "FileEdit"
633 || tool_name == "Write"
634 {
635 return PermissionResult::Allow(PermissionAllowDecision::new().with_reason(
636 PermissionDecisionReason::Mode {
637 mode: PermissionMode::Bubble,
638 },
639 ));
640 }
641 }
642 PermissionMode::Auto => {
643 if crate::utils::permissions::classifier_decision::is_auto_mode_allowlisted_tool(
644 tool_name,
645 ) {
646 if let Ok(mut dt) = self.denial_tracking.write() {
647 *dt = crate::utils::permissions::denial_tracking::record_success(*dt);
648 }
649 return PermissionResult::Allow(
650 PermissionAllowDecision::new()
651 .with_reason(PermissionDecisionReason::Mode {
652 mode: PermissionMode::Auto,
653 }),
654 );
655 }
656 if let Ok(mut dt) = self.denial_tracking.write() {
658 *dt = crate::utils::permissions::denial_tracking::record_denial(*dt);
659 }
660 let should_fallback = if let Ok(dt) = self.denial_tracking.read() {
661 crate::utils::permissions::denial_tracking::should_fallback_to_prompting(*dt)
662 } else {
663 false
664 };
665 let mut msg = format!("Tool '{}' requires auto-classification", tool_name);
666 if should_fallback {
667 msg = format!(
668 "{}. Auto mode has failed repeatedly — consider switching to a different permission mode.",
669 msg
670 );
671 }
672 return PermissionResult::Ask(
673 PermissionAskDecision::new(&msg).with_reason(PermissionDecisionReason::Mode {
674 mode: PermissionMode::Auto,
675 }),
676 );
677 }
678 _ => {}
679 }
680
681 PermissionResult::Ask(
683 PermissionAskDecision::new(&format!("Permission required to use {}", tool_name))
684 .with_reason(PermissionDecisionReason::Mode { mode: self.mode }),
685 )
686 }
687}
688
689pub type PermissionCallback =
691 Box<dyn Fn(PermissionMetadata, PermissionResult) -> PermissionResult + Send + Sync>;
692
693pub struct PermissionHandler {
695 context: PermissionContext,
696 callback: Option<PermissionCallback>,
697}
698
699impl PermissionHandler {
700 pub fn new(context: PermissionContext) -> Self {
702 Self {
703 context,
704 callback: None,
705 }
706 }
707
708 pub fn with_callback(context: PermissionContext, callback: PermissionCallback) -> Self {
710 Self {
711 context,
712 callback: Some(callback),
713 }
714 }
715
716 pub fn check(&self, metadata: PermissionMetadata) -> PermissionResult {
718 let result = self
719 .context
720 .check_tool(&metadata.tool_name, metadata.input.as_ref());
721
722 if let Some(callback) = &self.callback {
724 return callback(metadata, result);
725 }
726
727 result
728 }
729
730 pub fn is_allowed(&self, metadata: &PermissionMetadata) -> bool {
732 self.check(metadata.clone()).is_allowed()
733 }
734}
735
736impl PermissionHandler {
737 pub fn default() -> Self {
739 Self::new(PermissionContext::default())
740 }
741}