origin_mcp_core/
permission.rs1use serde::{Deserialize, Serialize};
2use std::collections::BTreeSet;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum AiPermission {
12 Read,
14 Search,
16 Propose,
18 Commit,
20 Delete,
22}
23
24impl AiPermission {
25 pub fn is_mutating(self) -> bool {
27 matches!(self, Self::Commit | Self::Delete)
28 }
29
30 pub fn as_str(self) -> &'static str {
31 match self {
32 Self::Read => "read",
33 Self::Search => "search",
34 Self::Propose => "propose",
35 Self::Commit => "commit",
36 Self::Delete => "delete",
37 }
38 }
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(transparent)]
44pub struct AiPermissions {
45 granted: BTreeSet<AiPermission>,
46}
47
48impl AiPermissions {
49 pub fn none() -> Self {
51 Self::default()
52 }
53
54 pub fn read_and_propose() -> Self {
61 Self::from([
62 AiPermission::Read,
63 AiPermission::Search,
64 AiPermission::Propose,
65 ])
66 }
67
68 pub fn from(permissions: impl IntoIterator<Item = AiPermission>) -> Self {
69 Self {
70 granted: permissions.into_iter().collect(),
71 }
72 }
73
74 pub fn allows(&self, permission: AiPermission) -> bool {
75 self.granted.contains(&permission)
76 }
77
78 pub fn is_empty(&self) -> bool {
79 self.granted.is_empty()
80 }
81
82 pub fn grants_mutation(&self) -> bool {
84 self.granted
85 .iter()
86 .any(|permission| permission.is_mutating())
87 }
88
89 pub fn granted(&self) -> impl Iterator<Item = AiPermission> + '_ {
90 self.granted.iter().copied()
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn the_default_grant_cannot_change_anything() {
100 let permissions = AiPermissions::read_and_propose();
101
102 assert!(permissions.allows(AiPermission::Read));
103 assert!(permissions.allows(AiPermission::Propose));
104 assert!(!permissions.allows(AiPermission::Commit));
105 assert!(!permissions.allows(AiPermission::Delete));
106 assert!(!permissions.grants_mutation());
107 }
108
109 #[test]
110 fn proposing_is_not_mutating_but_committing_is() {
111 assert!(!AiPermission::Propose.is_mutating());
112 assert!(AiPermission::Commit.is_mutating());
113 assert!(AiPermission::Delete.is_mutating());
114 }
115
116 #[test]
117 fn granting_commit_is_visible_as_such() {
118 let permissions = AiPermissions::from([AiPermission::Read, AiPermission::Commit]);
119
120 assert!(
121 permissions.grants_mutation(),
122 "a settings screen must be able to warn about this"
123 );
124 }
125}