Skip to main content

origin_mcp_core/
permission.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeSet;
3
4/// What an external AI may do with this application.
5///
6/// A third permission level, separate from product permissions (what the app may do at
7/// a service) and platform permissions (what it may do on this machine). It is never
8/// wider than the rights of the signed-in user: MCP must not be a privilege escalation.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum AiPermission {
12    /// Read a specific thing it already knows the identity of.
13    Read,
14    /// Search across content.
15    Search,
16    /// Prepare a change for a human to confirm. Nothing takes effect.
17    Propose,
18    /// Make a change take effect without further confirmation.
19    Commit,
20    /// Remove content.
21    Delete,
22}
23
24impl AiPermission {
25    /// Whether this permission can change anything.
26    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/// What this application actually grants.
42#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(transparent)]
44pub struct AiPermissions {
45    granted: BTreeSet<AiPermission>,
46}
47
48impl AiPermissions {
49    /// Nothing granted. MCP is effectively off.
50    pub fn none() -> Self {
51        Self::default()
52    }
53
54    /// The default: an external AI may look at things and prepare changes, but nothing
55    /// it does takes effect without a human.
56    ///
57    /// This is the whole safety story in one line. The caller is a model reacting to
58    /// content it read somewhere; prompt injection in a document must not be able to
59    /// delete anything.
60    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    /// Whether anything granted can change data. Worth surfacing in the settings UI.
83    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}