agent_engine/extensions/
permissions.rs1use std::collections::HashSet;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum Permission {
15 ToolsIntercept,
17 ToolsTransformOutput,
22 ToolsOverride,
24 LlmContent,
26 SessionLifecycle,
28 ToolsRegister,
30 ProvidersRegister,
32 MemoryRead,
34 MemoryWrite,
36 ConfigWrite,
38 ConfigSubscribe,
40 AudioInput,
42 AudioOutput,
44}
45
46impl Permission {
47 pub fn as_str(&self) -> &'static str {
49 match self {
50 Self::ToolsIntercept => "tools.intercept",
51 Self::ToolsTransformOutput => "tools.transform_output",
52 Self::ToolsOverride => "tools.override",
53 Self::LlmContent => "privacy.llm_content",
54 Self::SessionLifecycle => "session.lifecycle",
55 Self::ToolsRegister => "tools.register",
56 Self::ProvidersRegister => "providers.register",
57 Self::MemoryRead => "memory.read",
58 Self::MemoryWrite => "memory.write",
59 Self::ConfigWrite => "config.write",
60 Self::ConfigSubscribe => "config.subscribe",
61 Self::AudioInput => "audio.input",
62 Self::AudioOutput => "audio.output",
63 }
64 }
65
66 pub fn parse(s: &str) -> Option<Self> {
68 match s {
69 "tools.intercept" => Some(Self::ToolsIntercept),
70 "tools.transform_output" => Some(Self::ToolsTransformOutput),
71 "tools.override" => Some(Self::ToolsOverride),
72 "privacy.llm_content" => Some(Self::LlmContent),
73 "session.lifecycle" => Some(Self::SessionLifecycle),
74 "tools.register" => Some(Self::ToolsRegister),
75 "providers.register" => Some(Self::ProvidersRegister),
76 "memory.read" => Some(Self::MemoryRead),
77 "memory.write" => Some(Self::MemoryWrite),
78 "config.write" => Some(Self::ConfigWrite),
79 "config.subscribe" => Some(Self::ConfigSubscribe),
80 "audio.input" => Some(Self::AudioInput),
81 "audio.output" => Some(Self::AudioOutput),
82 _ => None,
83 }
84 }
85 pub fn is_reserved(&self) -> bool {
87 matches!(
88 self,
89 Self::ToolsOverride
90 )
91 }
92}
93
94#[derive(Debug, Clone, Default)]
96pub struct PermissionSet {
97 permissions: HashSet<Permission>,
98}
99
100impl PermissionSet {
101 pub fn new() -> Self {
103 Self::default()
104 }
105
106 pub fn from_strings(perms: &[String]) -> Self {
112 let permissions = perms.iter().filter_map(|s| Permission::parse(s)).collect();
113 Self { permissions }
114 }
115
116 pub fn try_from_strings(perms: &[String]) -> Result<Self, String> {
118 let mut permissions = HashSet::new();
119 for perm in perms {
120 let parsed = Permission::parse(perm)
121 .ok_or_else(|| format!("Unknown extension permission: {perm}"))?;
122 if parsed.is_reserved() {
123 return Err(format!(
124 "Reserved extension permission is not implemented yet: {perm}"
125 ));
126 }
127 permissions.insert(parsed);
128 }
129 Ok(Self { permissions })
130 }
131
132 pub fn has(&self, perm: Permission) -> bool {
134 self.permissions.contains(&perm)
135 }
136
137 pub fn grant(&mut self, perm: Permission) {
139 self.permissions.insert(perm);
140 }
141
142 pub fn allows_hook(&self, kind: crate::extensions::hooks::events::HookKind) -> bool {
144 self.has(kind.required_permission())
145 }
146
147 pub fn len(&self) -> usize {
149 self.permissions.len()
150 }
151
152 pub fn is_empty(&self) -> bool {
154 self.permissions.is_empty()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::extensions::hooks::events::HookKind;
162
163 #[test]
164 fn parse_valid_permissions() {
165 assert_eq!(Permission::parse("tools.intercept"), Some(Permission::ToolsIntercept));
166 assert_eq!(Permission::parse("privacy.llm_content"), Some(Permission::LlmContent));
167 assert_eq!(Permission::parse("session.lifecycle"), Some(Permission::SessionLifecycle));
168 }
169
170 #[test]
171 fn parse_invalid_returns_none() {
172 assert_eq!(Permission::parse("invalid"), None);
173 assert_eq!(Permission::parse(""), None);
174 }
175
176 #[test]
177 fn from_strings_skips_invalid() {
178 let perms = PermissionSet::from_strings(&[
179 "tools.intercept".into(),
180 "bogus".into(),
181 "session.lifecycle".into(),
182 ]);
183 assert_eq!(perms.len(), 2);
184 assert!(perms.has(Permission::ToolsIntercept));
185 assert!(perms.has(Permission::SessionLifecycle));
186 assert!(!perms.has(Permission::LlmContent));
187 }
188
189 #[test]
190 fn allows_hook_checks_required_permission() {
191 let mut perms = PermissionSet::new();
192 assert!(!perms.allows_hook(HookKind::BeforeToolCall));
193
194 perms.grant(Permission::ToolsIntercept);
195 assert!(perms.allows_hook(HookKind::BeforeToolCall));
196 assert!(perms.allows_hook(HookKind::AfterToolCall));
197 assert!(!perms.allows_hook(HookKind::BeforeMessage)); }
199
200 #[test]
201 fn empty_set() {
202 let perms = PermissionSet::new();
203 assert!(perms.is_empty());
204 assert_eq!(perms.len(), 0);
205 }
206
207 #[test]
208 fn providers_register_is_active_but_tools_override_remains_reserved() {
209 let perms = PermissionSet::try_from_strings(&["providers.register".to_string()]).unwrap();
210 assert!(perms.has(Permission::ProvidersRegister));
211
212 let err = PermissionSet::try_from_strings(&["tools.override".to_string()]).unwrap_err();
213 assert!(err.contains("Reserved extension permission"));
214 }
215
216 #[test]
217 fn memory_permissions_parse_and_are_not_reserved() {
218 assert_eq!(Permission::parse("memory.read"), Some(Permission::MemoryRead));
219 assert_eq!(Permission::parse("memory.write"), Some(Permission::MemoryWrite));
220 assert!(!Permission::MemoryRead.is_reserved());
221 assert!(!Permission::MemoryWrite.is_reserved());
222 let perms = PermissionSet::try_from_strings(&[
223 "memory.read".to_string(),
224 "memory.write".to_string(),
225 ])
226 .unwrap();
227 assert!(perms.has(Permission::MemoryRead));
228 assert!(perms.has(Permission::MemoryWrite));
229 }
230
231 #[test]
232 fn audio_permissions_parse_and_are_not_reserved() {
233 assert_eq!(Permission::parse("audio.input"), Some(Permission::AudioInput));
234 assert_eq!(Permission::parse("audio.output"), Some(Permission::AudioOutput));
235 assert!(!Permission::AudioInput.is_reserved());
236 assert!(!Permission::AudioOutput.is_reserved());
237 let perms = PermissionSet::try_from_strings(&[
238 "audio.input".to_string(),
239 "audio.output".to_string(),
240 ])
241 .unwrap();
242 assert!(perms.has(Permission::AudioInput));
243 assert!(perms.has(Permission::AudioOutput));
244 }
245
246 #[test]
247 fn round_trip_as_str() {
248 for perm in [
249 Permission::ToolsIntercept,
250 Permission::ToolsOverride,
251 Permission::LlmContent,
252 Permission::SessionLifecycle,
253 Permission::ToolsRegister,
254 Permission::ProvidersRegister,
255 Permission::MemoryRead,
256 Permission::MemoryWrite,
257 Permission::AudioInput,
258 Permission::AudioOutput,
259 ] {
260 assert_eq!(Permission::parse(perm.as_str()), Some(perm));
261 }
262 }
263}