Skip to main content

systemprompt_models/documents/
access.rs

1//! Document access levels, the actions they gate, and the provenance
2//! vocabulary recorded on every revision.
3//!
4//! [`AccessLevel`] is totally ordered: holding a level grants every level
5//! below it, so a grant check is a single comparison against
6//! [`DocumentAction::required_level`].
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::fmt;
12use std::str::FromStr;
13
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
18#[error("unknown {kind} `{value}`")]
19pub struct UnknownDocumentValue {
20    pub kind: &'static str,
21    pub value: String,
22}
23
24macro_rules! wire_enum {
25    ($name:ident, $kind:literal, { $($variant:ident => $wire:literal),+ $(,)? }) => {
26        impl $name {
27            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
28
29            #[must_use]
30            pub const fn as_str(self) -> &'static str {
31                match self {
32                    $(Self::$variant => $wire),+
33                }
34            }
35        }
36
37        impl fmt::Display for $name {
38            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39                f.write_str(self.as_str())
40            }
41        }
42
43        impl FromStr for $name {
44            type Err = UnknownDocumentValue;
45
46            fn from_str(s: &str) -> Result<Self, Self::Err> {
47                match s {
48                    $($wire => Ok(Self::$variant),)+
49                    other => Err(UnknownDocumentValue {
50                        kind: $kind,
51                        value: other.to_owned(),
52                    }),
53                }
54            }
55        }
56    };
57}
58
59#[derive(
60    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
61)]
62#[serde(rename_all = "snake_case")]
63pub enum AccessLevel {
64    View,
65    Comment,
66    Edit,
67    Share,
68    Own,
69}
70
71wire_enum!(AccessLevel, "access level", {
72    View => "view",
73    Comment => "comment",
74    Edit => "edit",
75    Share => "share",
76    Own => "own",
77});
78
79/// An operation on a document or folder, as named in an authz request.
80///
81/// `Create` is checked against the destination folder; every other action
82/// is checked against the document itself.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
84#[serde(rename_all = "snake_case")]
85pub enum DocumentAction {
86    View,
87    Comment,
88    Edit,
89    Share,
90    Own,
91    Create,
92    Delete,
93}
94
95wire_enum!(DocumentAction, "document action", {
96    View => "view",
97    Comment => "comment",
98    Edit => "edit",
99    Share => "share",
100    Own => "own",
101    Create => "create",
102    Delete => "delete",
103});
104
105impl DocumentAction {
106    #[must_use]
107    pub const fn required_level(self) -> AccessLevel {
108        match self {
109            Self::View => AccessLevel::View,
110            Self::Comment => AccessLevel::Comment,
111            Self::Edit | Self::Create => AccessLevel::Edit,
112            Self::Share => AccessLevel::Share,
113            Self::Own | Self::Delete => AccessLevel::Own,
114        }
115    }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
119#[serde(rename_all = "snake_case")]
120pub enum ActorKind {
121    User,
122    Agent,
123    Bridge,
124    Import,
125    System,
126}
127
128wire_enum!(ActorKind, "document actor kind", {
129    User => "user",
130    Agent => "agent",
131    Bridge => "bridge",
132    Import => "import",
133    System => "system",
134});
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
137#[serde(rename_all = "snake_case")]
138pub enum RevisionOriginKind {
139    McpExecution,
140    BridgeDevice,
141    WebSession,
142    EditSession,
143    Api,
144}
145
146wire_enum!(RevisionOriginKind, "revision origin kind", {
147    McpExecution => "mcp_execution",
148    BridgeDevice => "bridge_device",
149    WebSession => "web_session",
150    EditSession => "edit_session",
151    Api => "api",
152});
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
155#[serde(rename_all = "snake_case")]
156pub enum ProposalStatus {
157    Open,
158    Accepted,
159    Rejected,
160    Superseded,
161}
162
163wire_enum!(ProposalStatus, "proposal status", {
164    Open => "open",
165    Accepted => "accepted",
166    Rejected => "rejected",
167    Superseded => "superseded",
168});
169
170/// How an agent's edit under a folder lands: straight onto the head, or as
171/// a proposal a reviewer accepts. A folder inherits `propose` from the
172/// nearest ancestor that sets it.
173#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
174#[serde(rename_all = "snake_case")]
175pub enum AgentEditPolicy {
176    #[default]
177    Direct,
178    Propose,
179}
180
181wire_enum!(AgentEditPolicy, "agent edit policy", {
182    Direct => "direct",
183    Propose => "propose",
184});
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
187#[serde(rename_all = "snake_case")]
188pub enum EditMode {
189    Land,
190    Propose,
191}
192
193wire_enum!(EditMode, "edit mode", {
194    Land => "land",
195    Propose => "propose",
196});