use std::collections::HashSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Permission {
ToolsIntercept,
ToolsOverride,
LlmContent,
SessionLifecycle,
ToolsRegister,
ProvidersRegister,
MemoryRead,
MemoryWrite,
ConfigWrite,
ConfigSubscribe,
AudioInput,
AudioOutput,
}
impl Permission {
pub fn as_str(&self) -> &'static str {
match self {
Self::ToolsIntercept => "tools.intercept",
Self::ToolsOverride => "tools.override",
Self::LlmContent => "privacy.llm_content",
Self::SessionLifecycle => "session.lifecycle",
Self::ToolsRegister => "tools.register",
Self::ProvidersRegister => "providers.register",
Self::MemoryRead => "memory.read",
Self::MemoryWrite => "memory.write",
Self::ConfigWrite => "config.write",
Self::ConfigSubscribe => "config.subscribe",
Self::AudioInput => "audio.input",
Self::AudioOutput => "audio.output",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"tools.intercept" => Some(Self::ToolsIntercept),
"tools.override" => Some(Self::ToolsOverride),
"privacy.llm_content" => Some(Self::LlmContent),
"session.lifecycle" => Some(Self::SessionLifecycle),
"tools.register" => Some(Self::ToolsRegister),
"providers.register" => Some(Self::ProvidersRegister),
"memory.read" => Some(Self::MemoryRead),
"memory.write" => Some(Self::MemoryWrite),
"config.write" => Some(Self::ConfigWrite),
"config.subscribe" => Some(Self::ConfigSubscribe),
"audio.input" => Some(Self::AudioInput),
"audio.output" => Some(Self::AudioOutput),
_ => None,
}
}
pub fn is_reserved(&self) -> bool {
matches!(
self,
Self::ToolsOverride
)
}
}
#[derive(Debug, Clone, Default)]
pub struct PermissionSet {
permissions: HashSet<Permission>,
}
impl PermissionSet {
pub fn new() -> Self {
Self::default()
}
pub fn from_strings(perms: &[String]) -> Self {
let permissions = perms.iter().filter_map(|s| Permission::parse(s)).collect();
Self { permissions }
}
pub fn try_from_strings(perms: &[String]) -> Result<Self, String> {
let mut permissions = HashSet::new();
for perm in perms {
let parsed = Permission::parse(perm)
.ok_or_else(|| format!("Unknown extension permission: {perm}"))?;
if parsed.is_reserved() {
return Err(format!(
"Reserved extension permission is not implemented yet: {perm}"
));
}
permissions.insert(parsed);
}
Ok(Self { permissions })
}
pub fn has(&self, perm: Permission) -> bool {
self.permissions.contains(&perm)
}
pub fn grant(&mut self, perm: Permission) {
self.permissions.insert(perm);
}
pub fn allows_hook(&self, kind: crate::extensions::hooks::events::HookKind) -> bool {
self.has(kind.required_permission())
}
pub fn len(&self) -> usize {
self.permissions.len()
}
pub fn is_empty(&self) -> bool {
self.permissions.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extensions::hooks::events::HookKind;
#[test]
fn parse_valid_permissions() {
assert_eq!(Permission::parse("tools.intercept"), Some(Permission::ToolsIntercept));
assert_eq!(Permission::parse("privacy.llm_content"), Some(Permission::LlmContent));
assert_eq!(Permission::parse("session.lifecycle"), Some(Permission::SessionLifecycle));
}
#[test]
fn parse_invalid_returns_none() {
assert_eq!(Permission::parse("invalid"), None);
assert_eq!(Permission::parse(""), None);
}
#[test]
fn from_strings_skips_invalid() {
let perms = PermissionSet::from_strings(&[
"tools.intercept".into(),
"bogus".into(),
"session.lifecycle".into(),
]);
assert_eq!(perms.len(), 2);
assert!(perms.has(Permission::ToolsIntercept));
assert!(perms.has(Permission::SessionLifecycle));
assert!(!perms.has(Permission::LlmContent));
}
#[test]
fn allows_hook_checks_required_permission() {
let mut perms = PermissionSet::new();
assert!(!perms.allows_hook(HookKind::BeforeToolCall));
perms.grant(Permission::ToolsIntercept);
assert!(perms.allows_hook(HookKind::BeforeToolCall));
assert!(perms.allows_hook(HookKind::AfterToolCall));
assert!(!perms.allows_hook(HookKind::BeforeMessage)); }
#[test]
fn empty_set() {
let perms = PermissionSet::new();
assert!(perms.is_empty());
assert_eq!(perms.len(), 0);
}
#[test]
fn providers_register_is_active_but_tools_override_remains_reserved() {
let perms = PermissionSet::try_from_strings(&["providers.register".to_string()]).unwrap();
assert!(perms.has(Permission::ProvidersRegister));
let err = PermissionSet::try_from_strings(&["tools.override".to_string()]).unwrap_err();
assert!(err.contains("Reserved extension permission"));
}
#[test]
fn memory_permissions_parse_and_are_not_reserved() {
assert_eq!(Permission::parse("memory.read"), Some(Permission::MemoryRead));
assert_eq!(Permission::parse("memory.write"), Some(Permission::MemoryWrite));
assert!(!Permission::MemoryRead.is_reserved());
assert!(!Permission::MemoryWrite.is_reserved());
let perms = PermissionSet::try_from_strings(&[
"memory.read".to_string(),
"memory.write".to_string(),
])
.unwrap();
assert!(perms.has(Permission::MemoryRead));
assert!(perms.has(Permission::MemoryWrite));
}
#[test]
fn audio_permissions_parse_and_are_not_reserved() {
assert_eq!(Permission::parse("audio.input"), Some(Permission::AudioInput));
assert_eq!(Permission::parse("audio.output"), Some(Permission::AudioOutput));
assert!(!Permission::AudioInput.is_reserved());
assert!(!Permission::AudioOutput.is_reserved());
let perms = PermissionSet::try_from_strings(&[
"audio.input".to_string(),
"audio.output".to_string(),
])
.unwrap();
assert!(perms.has(Permission::AudioInput));
assert!(perms.has(Permission::AudioOutput));
}
#[test]
fn round_trip_as_str() {
for perm in [
Permission::ToolsIntercept,
Permission::ToolsOverride,
Permission::LlmContent,
Permission::SessionLifecycle,
Permission::ToolsRegister,
Permission::ProvidersRegister,
Permission::MemoryRead,
Permission::MemoryWrite,
Permission::AudioInput,
Permission::AudioOutput,
] {
assert_eq!(Permission::parse(perm.as_str()), Some(perm));
}
}
}