turboclaude_protocol/
permissions.rs

1//! Permission update types and definitions
2//!
3//! Provides types for dynamic permission changes during agent sessions.
4//! Matches the Python SDK implementation (types.py:56-108).
5
6use serde::{Deserialize, Serialize};
7
8/// Permission behavior for rule-based updates
9#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "lowercase")]
11pub enum PermissionBehavior {
12    /// Allow the action
13    Allow,
14
15    /// Deny the action
16    Deny,
17
18    /// Ask for permission
19    Ask,
20}
21
22/// Destination for permission updates
23#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(rename_all = "camelCase")]
25pub enum PermissionUpdateDestination {
26    /// Update user settings
27    UserSettings,
28
29    /// Update project settings
30    ProjectSettings,
31
32    /// Update local settings
33    LocalSettings,
34
35    /// Update session only (non-persistent)
36    Session,
37}
38
39/// Permission rule value
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41pub struct PermissionRuleValue {
42    /// Tool name for the rule
43    #[serde(rename = "toolName")]
44    pub tool_name: String,
45
46    /// Optional rule content (regex pattern or other constraint)
47    #[serde(rename = "ruleContent", skip_serializing_if = "Option::is_none")]
48    pub rule_content: Option<String>,
49}
50
51impl PermissionRuleValue {
52    /// Create a new permission rule
53    pub fn new(tool_name: impl Into<String>) -> Self {
54        Self {
55            tool_name: tool_name.into(),
56            rule_content: None,
57        }
58    }
59
60    /// Set the rule content
61    pub fn with_rule_content(mut self, content: impl Into<String>) -> Self {
62        self.rule_content = Some(content.into());
63        self
64    }
65}
66
67/// Add rules update
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct AddRulesUpdate {
70    /// Rules to add
71    pub rules: Vec<PermissionRuleValue>,
72
73    /// Behavior for the rules
74    pub behavior: PermissionBehavior,
75
76    /// Optional destination
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub destination: Option<PermissionUpdateDestination>,
79}
80
81/// Replace rules update
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct ReplaceRulesUpdate {
84    /// Rules to replace with
85    pub rules: Vec<PermissionRuleValue>,
86
87    /// Behavior for the rules
88    pub behavior: PermissionBehavior,
89
90    /// Optional destination
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub destination: Option<PermissionUpdateDestination>,
93}
94
95/// Remove rules update
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct RemoveRulesUpdate {
98    /// Rules to remove
99    pub rules: Vec<PermissionRuleValue>,
100
101    /// Optional destination
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub destination: Option<PermissionUpdateDestination>,
104}
105
106/// Set mode update
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
108pub struct SetModeUpdate {
109    /// Permission mode to set
110    pub mode: crate::types::PermissionMode,
111
112    /// Optional destination
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub destination: Option<PermissionUpdateDestination>,
115}
116
117/// Add directories update
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119pub struct AddDirectoriesUpdate {
120    /// Directories to add
121    pub directories: Vec<String>,
122
123    /// Optional destination
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub destination: Option<PermissionUpdateDestination>,
126}
127
128/// Remove directories update
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
130pub struct RemoveDirectoriesUpdate {
131    /// Directories to remove
132    pub directories: Vec<String>,
133
134    /// Optional destination
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub destination: Option<PermissionUpdateDestination>,
137}
138
139/// Permission update type
140///
141/// Represents dynamic permission changes during an agent session.
142/// Matches Python SDK implementation (types.py:56-108).
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
144#[serde(tag = "type", rename_all = "camelCase")]
145pub enum PermissionUpdate {
146    /// Add permission rules
147    AddRules(AddRulesUpdate),
148
149    /// Replace permission rules
150    ReplaceRules(ReplaceRulesUpdate),
151
152    /// Remove permission rules
153    RemoveRules(RemoveRulesUpdate),
154
155    /// Set permission mode
156    SetMode(SetModeUpdate),
157
158    /// Add allowed directories
159    AddDirectories(AddDirectoriesUpdate),
160
161    /// Remove allowed directories
162    RemoveDirectories(RemoveDirectoriesUpdate),
163}
164
165impl PermissionUpdate {
166    /// Create an add rules update
167    pub fn add_rules(rules: Vec<PermissionRuleValue>, behavior: PermissionBehavior) -> Self {
168        Self::AddRules(AddRulesUpdate {
169            rules,
170            behavior,
171            destination: None,
172        })
173    }
174
175    /// Create a replace rules update
176    pub fn replace_rules(rules: Vec<PermissionRuleValue>, behavior: PermissionBehavior) -> Self {
177        Self::ReplaceRules(ReplaceRulesUpdate {
178            rules,
179            behavior,
180            destination: None,
181        })
182    }
183
184    /// Create a remove rules update
185    pub fn remove_rules(rules: Vec<PermissionRuleValue>) -> Self {
186        Self::RemoveRules(RemoveRulesUpdate {
187            rules,
188            destination: None,
189        })
190    }
191
192    /// Create a set mode update
193    pub fn set_mode(mode: crate::types::PermissionMode) -> Self {
194        Self::SetMode(SetModeUpdate {
195            mode,
196            destination: None,
197        })
198    }
199
200    /// Create an add directories update
201    pub fn add_directories(directories: Vec<String>) -> Self {
202        Self::AddDirectories(AddDirectoriesUpdate {
203            directories,
204            destination: None,
205        })
206    }
207
208    /// Create a remove directories update
209    pub fn remove_directories(directories: Vec<String>) -> Self {
210        Self::RemoveDirectories(RemoveDirectoriesUpdate {
211            directories,
212            destination: None,
213        })
214    }
215
216    /// Set the destination for this update
217    pub fn with_destination(mut self, destination: PermissionUpdateDestination) -> Self {
218        match &mut self {
219            Self::AddRules(u) => u.destination = Some(destination),
220            Self::ReplaceRules(u) => u.destination = Some(destination),
221            Self::RemoveRules(u) => u.destination = Some(destination),
222            Self::SetMode(u) => u.destination = Some(destination),
223            Self::AddDirectories(u) => u.destination = Some(destination),
224            Self::RemoveDirectories(u) => u.destination = Some(destination),
225        }
226        self
227    }
228
229    /// Get the destination for this update
230    pub fn destination(&self) -> Option<PermissionUpdateDestination> {
231        match self {
232            Self::AddRules(u) => u.destination,
233            Self::ReplaceRules(u) => u.destination,
234            Self::RemoveRules(u) => u.destination,
235            Self::SetMode(u) => u.destination,
236            Self::AddDirectories(u) => u.destination,
237            Self::RemoveDirectories(u) => u.destination,
238        }
239    }
240
241    /// Validate the update
242    ///
243    /// Returns `Ok(())` if the update is valid, or an error message if not.
244    pub fn validate(&self) -> Result<(), String> {
245        match self {
246            Self::AddRules(u) => {
247                if u.rules.is_empty() {
248                    return Err("AddRules update must have at least one rule".to_string());
249                }
250                for rule in &u.rules {
251                    if rule.tool_name.is_empty() {
252                        return Err("Rule tool_name cannot be empty".to_string());
253                    }
254                }
255                Ok(())
256            }
257            Self::ReplaceRules(u) => {
258                if u.rules.is_empty() {
259                    return Err("ReplaceRules update must have at least one rule".to_string());
260                }
261                for rule in &u.rules {
262                    if rule.tool_name.is_empty() {
263                        return Err("Rule tool_name cannot be empty".to_string());
264                    }
265                }
266                Ok(())
267            }
268            Self::RemoveRules(u) => {
269                if u.rules.is_empty() {
270                    return Err("RemoveRules update must have at least one rule".to_string());
271                }
272                for rule in &u.rules {
273                    if rule.tool_name.is_empty() {
274                        return Err("Rule tool_name cannot be empty".to_string());
275                    }
276                }
277                Ok(())
278            }
279            Self::SetMode(_) => Ok(()),
280            Self::AddDirectories(u) => {
281                if u.directories.is_empty() {
282                    return Err(
283                        "AddDirectories update must have at least one directory".to_string()
284                    );
285                }
286                for dir in &u.directories {
287                    if dir.is_empty() {
288                        return Err("Directory path cannot be empty".to_string());
289                    }
290                }
291                Ok(())
292            }
293            Self::RemoveDirectories(u) => {
294                if u.directories.is_empty() {
295                    return Err(
296                        "RemoveDirectories update must have at least one directory".to_string()
297                    );
298                }
299                for dir in &u.directories {
300                    if dir.is_empty() {
301                        return Err("Directory path cannot be empty".to_string());
302                    }
303                }
304                Ok(())
305            }
306        }
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use crate::types::PermissionMode;
314
315    #[test]
316    fn test_permission_rule_value() {
317        let rule = PermissionRuleValue::new("bash").with_rule_content(".*");
318
319        assert_eq!(rule.tool_name, "bash");
320        assert_eq!(rule.rule_content, Some(".*".to_string()));
321    }
322
323    #[test]
324    fn test_add_rules_update() {
325        let rules = vec![
326            PermissionRuleValue::new("bash"),
327            PermissionRuleValue::new("file_editor"),
328        ];
329
330        let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow)
331            .with_destination(PermissionUpdateDestination::Session);
332
333        assert!(matches!(update, PermissionUpdate::AddRules(_)));
334        assert_eq!(
335            update.destination(),
336            Some(PermissionUpdateDestination::Session)
337        );
338        assert!(update.validate().is_ok());
339    }
340
341    #[test]
342    fn test_set_mode_update() {
343        let update = PermissionUpdate::set_mode(PermissionMode::BypassPermissions)
344            .with_destination(PermissionUpdateDestination::ProjectSettings);
345
346        assert!(matches!(update, PermissionUpdate::SetMode(_)));
347        assert!(update.validate().is_ok());
348    }
349
350    #[test]
351    fn test_add_directories_update() {
352        let dirs = vec!["/home/user/project".to_string()];
353        let update = PermissionUpdate::add_directories(dirs);
354
355        assert!(matches!(update, PermissionUpdate::AddDirectories(_)));
356        assert!(update.validate().is_ok());
357    }
358
359    #[test]
360    fn test_validation_empty_rules() {
361        let update = PermissionUpdate::add_rules(vec![], PermissionBehavior::Allow);
362        assert!(update.validate().is_err());
363    }
364
365    #[test]
366    fn test_validation_empty_directories() {
367        let update = PermissionUpdate::add_directories(vec![]);
368        assert!(update.validate().is_err());
369    }
370
371    #[test]
372    fn test_validation_empty_tool_name() {
373        let rules = vec![PermissionRuleValue::new("")];
374        let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow);
375        assert!(update.validate().is_err());
376    }
377
378    #[test]
379    fn test_serialization_add_rules() {
380        let rules = vec![PermissionRuleValue::new("bash")];
381        let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow);
382
383        let json = serde_json::to_string(&update).unwrap();
384        assert!(json.contains("addRules"));
385        assert!(json.contains("bash"));
386
387        let deserialized: PermissionUpdate = serde_json::from_str(&json).unwrap();
388        assert_eq!(update, deserialized);
389    }
390
391    #[test]
392    fn test_serialization_set_mode() {
393        let update = PermissionUpdate::set_mode(PermissionMode::AcceptEdits);
394
395        let json = serde_json::to_string(&update).unwrap();
396        assert!(json.contains("setMode"));
397
398        let deserialized: PermissionUpdate = serde_json::from_str(&json).unwrap();
399        assert_eq!(update, deserialized);
400    }
401
402    #[test]
403    fn test_serialization_with_destination() {
404        let rules = vec![PermissionRuleValue::new("bash")];
405        let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow)
406            .with_destination(PermissionUpdateDestination::Session);
407
408        let json = serde_json::to_string(&update).unwrap();
409        assert!(json.contains("destination"));
410        assert!(json.contains("session"));
411
412        let deserialized: PermissionUpdate = serde_json::from_str(&json).unwrap();
413        assert_eq!(update, deserialized);
414    }
415}