deepstrike_core/governance/
permission.rs1use compact_str::CompactString;
2
3use crate::types::message::ToolCall;
4use crate::types::policy::{CallerContext, 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 rule_count(&self) -> usize {
56 self.rules.len()
57 }
58
59 pub fn default_action(&self) -> &PermissionAction {
60 &self.default
61 }
62
63 pub fn check(&self, call: &ToolCall, _caller: &CallerContext) -> Option<GovernanceVerdict> {
64 for rule in &self.rules {
65 if rule.matches(&call.name) {
66 return match rule.action {
67 PermissionAction::Allow => None,
68 PermissionAction::Deny => Some(GovernanceVerdict::Deny {
69 stage: "permission",
70 reason: format!(
71 "tool '{}' denied by rule '{}'",
72 call.name, rule.tool_pattern
73 ),
74 }),
75 PermissionAction::AskUser => Some(GovernanceVerdict::AskUser {
76 reason: format!("tool '{}' requires user approval", call.name),
77 }),
78 };
79 }
80 }
81 match self.default {
82 PermissionAction::Allow => None,
83 PermissionAction::AskUser => Some(GovernanceVerdict::AskUser {
84 reason: format!("tool '{}' requires user approval", call.name),
85 }),
86 PermissionAction::Deny => Some(GovernanceVerdict::Deny {
87 stage: "permission",
88 reason: format!("tool '{}' denied by default policy", call.name),
89 }),
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use compact_str::CompactString;
98
99 fn test_call(name: &str) -> ToolCall {
100 ToolCall {
101 id: CompactString::new("call-1"),
102 name: CompactString::new(name),
103 arguments: serde_json::Value::Null,
104 }
105 }
106
107 fn test_caller() -> CallerContext {
108 CallerContext {
109 agent_id: "test".into(),
110 session_id: "s1".into(),
111 is_sub_agent: false,
112 parent_session_id: None,
113 }
114 }
115
116 #[test]
117 fn allow_by_default() {
118 let pm = PermissionManager::new(PermissionAction::Allow);
119 assert!(pm.check(&test_call("anything"), &test_caller()).is_none());
120 }
121
122 #[test]
123 fn deny_by_pattern() {
124 let mut pm = PermissionManager::new(PermissionAction::Allow);
125 pm.add_rule(PermissionRule {
126 tool_pattern: "db.*".into(),
127 action: PermissionAction::Deny,
128 });
129 assert!(pm.check(&test_call("db.drop"), &test_caller()).is_some());
130 assert!(pm.check(&test_call("file.read"), &test_caller()).is_none());
131 }
132}