1use 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 ContextProvidersRegister,
38 MemoryRead,
40 MemoryWrite,
42 ConfigWrite,
44 ConfigSubscribe,
46 AudioInput,
48 AudioOutput,
50}
51
52impl Permission {
53 pub fn as_str(&self) -> &'static str {
55 match self {
56 Self::ToolsIntercept => "tools.intercept",
57 Self::ToolsTransformOutput => "tools.transform_output",
58 Self::ToolsOverride => "tools.override",
59 Self::LlmContent => "privacy.llm_content",
60 Self::SessionLifecycle => "session.lifecycle",
61 Self::ToolsRegister => "tools.register",
62 Self::ProvidersRegister => "providers.register",
63 Self::ContextProvidersRegister => "context_providers.register",
64 Self::MemoryRead => "memory.read",
65 Self::MemoryWrite => "memory.write",
66 Self::ConfigWrite => "config.write",
67 Self::ConfigSubscribe => "config.subscribe",
68 Self::AudioInput => "audio.input",
69 Self::AudioOutput => "audio.output",
70 }
71 }
72
73 pub fn parse(s: &str) -> Option<Self> {
75 match s {
76 "tools.intercept" => Some(Self::ToolsIntercept),
77 "tools.transform_output" => Some(Self::ToolsTransformOutput),
78 "tools.override" => Some(Self::ToolsOverride),
79 "privacy.llm_content" => Some(Self::LlmContent),
80 "session.lifecycle" => Some(Self::SessionLifecycle),
81 "tools.register" => Some(Self::ToolsRegister),
82 "providers.register" => Some(Self::ProvidersRegister),
83 "context_providers.register" => Some(Self::ContextProvidersRegister),
84 "memory.read" => Some(Self::MemoryRead),
85 "memory.write" => Some(Self::MemoryWrite),
86 "config.write" => Some(Self::ConfigWrite),
87 "config.subscribe" => Some(Self::ConfigSubscribe),
88 "audio.input" => Some(Self::AudioInput),
89 "audio.output" => Some(Self::AudioOutput),
90 _ => None,
91 }
92 }
93 pub fn is_reserved(&self) -> bool {
95 matches!(self, Self::ToolsOverride)
96 }
97}
98
99#[derive(Debug, Clone, Default)]
101pub struct PermissionSet {
102 permissions: HashSet<Permission>,
103}
104
105impl PermissionSet {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn from_strings(perms: &[String]) -> Self {
117 let permissions = perms.iter().filter_map(|s| Permission::parse(s)).collect();
118 Self { permissions }
119 }
120
121 pub fn try_from_strings(perms: &[String]) -> Result<Self, String> {
123 let mut permissions = HashSet::new();
124 for perm in perms {
125 let parsed = Permission::parse(perm)
126 .ok_or_else(|| format!("Unknown extension permission: {perm}"))?;
127 if parsed.is_reserved() {
128 return Err(format!(
129 "Reserved extension permission is not implemented yet: {perm}"
130 ));
131 }
132 permissions.insert(parsed);
133 }
134 Ok(Self { permissions })
135 }
136
137 pub fn has(&self, perm: Permission) -> bool {
139 self.permissions.contains(&perm)
140 }
141
142 pub fn grant(&mut self, perm: Permission) {
144 self.permissions.insert(perm);
145 }
146
147 pub fn allows_hook(&self, kind: crate::extensions::hooks::events::HookKind) -> bool {
149 self.has(kind.required_permission())
150 }
151
152 pub fn len(&self) -> usize {
154 self.permissions.len()
155 }
156
157 pub fn is_empty(&self) -> bool {
159 self.permissions.is_empty()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::extensions::hooks::events::HookKind;
167
168 #[test]
169 fn parse_valid_permissions() {
170 assert_eq!(
171 Permission::parse("tools.intercept"),
172 Some(Permission::ToolsIntercept)
173 );
174 assert_eq!(
175 Permission::parse("privacy.llm_content"),
176 Some(Permission::LlmContent)
177 );
178 assert_eq!(
179 Permission::parse("session.lifecycle"),
180 Some(Permission::SessionLifecycle)
181 );
182 }
183
184 #[test]
185 fn parse_invalid_returns_none() {
186 assert_eq!(Permission::parse("invalid"), None);
187 assert_eq!(Permission::parse(""), None);
188 }
189
190 #[test]
191 fn from_strings_skips_invalid() {
192 let perms = PermissionSet::from_strings(&[
193 "tools.intercept".into(),
194 "bogus".into(),
195 "session.lifecycle".into(),
196 ]);
197 assert_eq!(perms.len(), 2);
198 assert!(perms.has(Permission::ToolsIntercept));
199 assert!(perms.has(Permission::SessionLifecycle));
200 assert!(!perms.has(Permission::LlmContent));
201 }
202
203 #[test]
204 fn allows_hook_checks_required_permission() {
205 let mut perms = PermissionSet::new();
206 assert!(!perms.allows_hook(HookKind::BeforeToolCall));
207
208 perms.grant(Permission::ToolsIntercept);
209 assert!(perms.allows_hook(HookKind::BeforeToolCall));
210 assert!(perms.allows_hook(HookKind::AfterToolCall));
211 assert!(!perms.allows_hook(HookKind::BeforeMessage)); }
213
214 #[test]
215 fn empty_set() {
216 let perms = PermissionSet::new();
217 assert!(perms.is_empty());
218 assert_eq!(perms.len(), 0);
219 }
220
221 #[test]
222 fn providers_register_is_active_but_tools_override_remains_reserved() {
223 let perms = PermissionSet::try_from_strings(&["providers.register".to_string()]).unwrap();
224 assert!(perms.has(Permission::ProvidersRegister));
225
226 let err = PermissionSet::try_from_strings(&["tools.override".to_string()]).unwrap_err();
227 assert!(err.contains("Reserved extension permission"));
228 }
229
230 #[test]
231 fn memory_permissions_parse_and_are_not_reserved() {
232 assert_eq!(
233 Permission::parse("memory.read"),
234 Some(Permission::MemoryRead)
235 );
236 assert_eq!(
237 Permission::parse("memory.write"),
238 Some(Permission::MemoryWrite)
239 );
240 assert!(!Permission::MemoryRead.is_reserved());
241 assert!(!Permission::MemoryWrite.is_reserved());
242 let perms = PermissionSet::try_from_strings(&[
243 "memory.read".to_string(),
244 "memory.write".to_string(),
245 ])
246 .unwrap();
247 assert!(perms.has(Permission::MemoryRead));
248 assert!(perms.has(Permission::MemoryWrite));
249 }
250
251 #[test]
252 fn audio_permissions_parse_and_are_not_reserved() {
253 assert_eq!(
254 Permission::parse("audio.input"),
255 Some(Permission::AudioInput)
256 );
257 assert_eq!(
258 Permission::parse("audio.output"),
259 Some(Permission::AudioOutput)
260 );
261 assert!(!Permission::AudioInput.is_reserved());
262 assert!(!Permission::AudioOutput.is_reserved());
263 let perms = PermissionSet::try_from_strings(&[
264 "audio.input".to_string(),
265 "audio.output".to_string(),
266 ])
267 .unwrap();
268 assert!(perms.has(Permission::AudioInput));
269 assert!(perms.has(Permission::AudioOutput));
270 }
271
272 #[test]
273 fn context_providers_register_parses_and_is_not_reserved() {
274 assert_eq!(
275 Permission::parse("context_providers.register"),
276 Some(Permission::ContextProvidersRegister)
277 );
278 assert!(!Permission::ContextProvidersRegister.is_reserved());
279 let perms =
280 PermissionSet::try_from_strings(&["context_providers.register".to_string()]).unwrap();
281 assert!(perms.has(Permission::ContextProvidersRegister));
282 assert!(!perms.has(Permission::ProvidersRegister));
284 }
285
286 #[test]
287 fn round_trip_as_str() {
288 for perm in [
289 Permission::ToolsIntercept,
290 Permission::ToolsOverride,
291 Permission::LlmContent,
292 Permission::SessionLifecycle,
293 Permission::ToolsRegister,
294 Permission::ProvidersRegister,
295 Permission::ContextProvidersRegister,
296 Permission::MemoryRead,
297 Permission::MemoryWrite,
298 Permission::AudioInput,
299 Permission::AudioOutput,
300 ] {
301 assert_eq!(Permission::parse(perm.as_str()), Some(perm));
302 }
303 }
304}