deepstrike_core/governance/
permission.rs1use compact_str::CompactString;
2
3use crate::types::message::ToolCall;
4use crate::types::policy::GovernanceVerdict;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum PermissionAction {
9 Allow,
10 Deny,
11 AskUser,
12}
13
14#[derive(Debug, Clone)]
16pub struct PermissionRule {
17 pub tool_pattern: CompactString,
18 pub action: PermissionAction,
19}
20
21impl PermissionRule {
22 fn matches(&self, tool_name: &str) -> bool {
23 let p = self.tool_pattern.as_str();
24 if p == "*" {
25 return true;
26 }
27 if let Some(prefix) = p.strip_suffix('*') {
28 return tool_name.starts_with(prefix);
29 }
30 if let Some(suffix) = p.strip_prefix('*') {
31 return tool_name.ends_with(suffix);
32 }
33 p == tool_name
34 }
35}
36
37pub struct PermissionManager {
39 rules: Vec<PermissionRule>,
40 default: PermissionAction,
41}
42
43impl PermissionManager {
44 pub fn new(default: PermissionAction) -> Self {
45 Self {
46 rules: Vec::new(),
47 default,
48 }
49 }
50
51 pub fn add_rule(&mut self, rule: PermissionRule) {
52 self.rules.push(rule);
53 }
54
55 pub fn default_action(&self) -> &PermissionAction {
56 &self.default
57 }
58
59 pub fn check(&self, call: &ToolCall) -> Option<GovernanceVerdict> {
60 for rule in &self.rules {
61 if rule.matches(&call.name) {
62 return match rule.action {
63 PermissionAction::Allow => None,
64 PermissionAction::Deny => Some(GovernanceVerdict::Deny {
65 stage: "permission",
66 reason: format!(
67 "tool '{}' denied by rule '{}'",
68 call.name, rule.tool_pattern
69 ),
70 }),
71 PermissionAction::AskUser => Some(GovernanceVerdict::AskUser {
72 reason: format!("tool '{}' requires user approval", call.name),
73 }),
74 };
75 }
76 }
77 match self.default {
78 PermissionAction::Allow => None,
79 PermissionAction::AskUser => Some(GovernanceVerdict::AskUser {
80 reason: format!("tool '{}' requires user approval", call.name),
81 }),
82 PermissionAction::Deny => Some(GovernanceVerdict::Deny {
83 stage: "permission",
84 reason: format!("tool '{}' denied by default policy", call.name),
85 }),
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use compact_str::CompactString;
94
95 fn test_call(name: &str) -> ToolCall {
96 ToolCall {
97 id: CompactString::new("call-1"),
98 name: CompactString::new(name),
99 arguments: serde_json::Value::Null,
100 }
101 }
102
103
104 #[test]
105 fn allow_by_default() {
106 let pm = PermissionManager::new(PermissionAction::Allow);
107 assert!(pm.check(&test_call("anything")).is_none());
108 }
109
110 #[test]
111 fn deny_by_pattern() {
112 let mut pm = PermissionManager::new(PermissionAction::Allow);
113 pm.add_rule(PermissionRule {
114 tool_pattern: "db.*".into(),
115 action: PermissionAction::Deny,
116 });
117 assert!(pm.check(&test_call("db.drop")).is_some());
118 assert!(pm.check(&test_call("file.read")).is_none());
119 }
120}