Skip to main content

deepstrike_sdk/
safety.rs

1use std::collections::HashMap;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum PermissionMode {
5    Default,
6    Plan,
7    Auto,
8}
9
10#[derive(Debug, Clone)]
11pub struct Permission {
12    pub tool: String,
13    pub action: String,
14    pub allowed: bool,
15    pub requires_approval: bool,
16    pub note: String,
17}
18
19#[derive(Debug, Clone)]
20pub struct PermissionDecision {
21    pub allowed: bool,
22    pub reason: String,
23    pub requires_approval: bool,
24    pub matched_permission: Option<Permission>,
25}
26
27pub struct PermissionManager {
28    mode: PermissionMode,
29    permissions: HashMap<String, Permission>,
30}
31
32impl PermissionManager {
33    pub fn new(mode: PermissionMode) -> Self {
34        Self {
35            mode,
36            permissions: Default::default(),
37        }
38    }
39
40    pub fn grant(&mut self, resource: impl Into<String>, action: impl Into<String>) {
41        let tool = resource.into();
42        let action = action.into();
43        let key = format!("{tool}:{action}");
44        self.permissions.insert(
45            key,
46            Permission {
47                tool,
48                action,
49                allowed: true,
50                requires_approval: false,
51                note: String::new(),
52            },
53        );
54    }
55
56    pub fn grant_with_approval(
57        &mut self,
58        resource: impl Into<String>,
59        action: impl Into<String>,
60        note: impl Into<String>,
61    ) {
62        let tool = resource.into();
63        let action = action.into();
64        let key = format!("{tool}:{action}");
65        self.permissions.insert(
66            key,
67            Permission {
68                tool,
69                action,
70                allowed: true,
71                requires_approval: true,
72                note: note.into(),
73            },
74        );
75    }
76
77    pub fn revoke(&mut self, resource: &str, action: &str) {
78        let key = format!("{resource}:{action}");
79        self.permissions.insert(
80            key,
81            Permission {
82                tool: resource.to_string(),
83                action: action.to_string(),
84                allowed: false,
85                requires_approval: false,
86                note: String::new(),
87            },
88        );
89    }
90
91    fn match_permission(&self, tool: &str, action: &str) -> Option<Permission> {
92        for key in [
93            format!("{tool}:{action}"),
94            format!("{tool}:*"),
95            format!("*:{action}"),
96            format!("*:*"),
97        ] {
98            if let Some(p) = self.permissions.get(&key) {
99                return Some(p.clone());
100            }
101        }
102        None
103    }
104
105    pub fn evaluate(&self, resource: &str, action: &str) -> PermissionDecision {
106        match self.mode {
107            PermissionMode::Auto => PermissionDecision {
108                allowed: true,
109                reason: "AUTO mode".into(),
110                requires_approval: false,
111                matched_permission: None,
112            },
113            PermissionMode::Plan => PermissionDecision {
114                allowed: false,
115                reason: "PLAN mode blocks all".into(),
116                requires_approval: false,
117                matched_permission: None,
118            },
119            PermissionMode::Default => match self.match_permission(resource, action) {
120                None => PermissionDecision {
121                    allowed: false,
122                    reason: "not granted".into(),
123                    requires_approval: false,
124                    matched_permission: None,
125                },
126                Some(p) if !p.allowed => PermissionDecision {
127                    allowed: false,
128                    reason: if p.note.is_empty() {
129                        "permission denied".into()
130                    } else {
131                        p.note.clone()
132                    },
133                    requires_approval: false,
134                    matched_permission: Some(p),
135                },
136                Some(p) if p.requires_approval => PermissionDecision {
137                    allowed: false,
138                    reason: if p.note.is_empty() {
139                        "requires approval".into()
140                    } else {
141                        p.note.clone()
142                    },
143                    requires_approval: true,
144                    matched_permission: Some(p),
145                },
146                Some(p) => PermissionDecision {
147                    allowed: true,
148                    reason: "granted".into(),
149                    requires_approval: false,
150                    matched_permission: Some(p),
151                },
152            },
153        }
154    }
155}