systemprompt_security/policy/
types.rs1use std::fmt;
11use std::str::FromStr;
12use std::sync::Arc;
13
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::{McpToolName, PolicyId, SessionId, UserId};
16
17use crate::authz::error::AuthzError;
18use crate::authz::types::Decision;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct SecretLocation {
26 pub kind: String,
27 pub path: String,
28}
29
30impl SecretLocation {
31 pub fn new(kind: impl Into<String>, path: impl Into<String>) -> Self {
32 Self {
33 kind: kind.into(),
34 path: path.into(),
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct RateLimitWindow {
41 pub name: String,
42 pub seconds: u64,
43 pub limit: u64,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(tag = "kind", rename_all = "snake_case")]
51pub enum AgentScope {
52 User { user_id: UserId },
53 System,
54}
55
56impl AgentScope {
57 #[must_use]
58 pub const fn user_id(&self) -> Option<&UserId> {
59 match self {
60 Self::User { user_id } => Some(user_id),
61 Self::System => None,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
75#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
76#[serde(rename_all = "lowercase")]
77pub enum AccessScope {
78 Admin,
79 User,
80 Unknown,
81}
82
83impl AccessScope {
84 #[must_use]
85 pub const fn as_str(self) -> &'static str {
86 match self {
87 Self::Admin => "admin",
88 Self::User => "user",
89 Self::Unknown => "unknown",
90 }
91 }
92}
93
94impl fmt::Display for AccessScope {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100impl FromStr for AccessScope {
101 type Err = AuthzError;
102
103 fn from_str(s: &str) -> Result<Self, Self::Err> {
104 match s {
105 "admin" => Ok(Self::Admin),
106 "user" => Ok(Self::User),
107 "unknown" | "" => Ok(Self::Unknown),
108 other => Err(AuthzError::Validation(format!(
109 "unknown access scope: {other}"
110 ))),
111 }
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(transparent)]
123pub struct McpToolInput(
124 serde_json::Value,
127);
128
129impl McpToolInput {
130 #[must_use]
131 pub const fn new(value: serde_json::Value) -> Self {
132 Self(value)
133 }
134
135 #[must_use]
136 pub const fn as_value(&self) -> &serde_json::Value {
137 &self.0
138 }
139
140 #[must_use]
141 pub fn as_str(&self, field: &str) -> Option<&str> {
142 self.0.get(field).and_then(serde_json::Value::as_str)
143 }
144
145 #[must_use]
146 pub fn as_path(&self, field: &str) -> Option<&str> {
147 self.as_str(field)
148 }
149}
150
151#[derive(Debug)]
152pub struct PolicyContext<'a> {
153 pub tool: McpToolName,
154 pub agent_scope: AgentScope,
155 pub access_scope: AccessScope,
156 pub session_id: &'a SessionId,
157 pub user_id: &'a UserId,
158 pub tool_input: &'a McpToolInput,
159}
160
161pub trait GovernancePolicy: Send + Sync + fmt::Debug {
167 fn id(&self) -> PolicyId;
168 fn name(&self) -> &'static str;
169 fn description(&self) -> &'static str;
170 fn evaluate(&self, ctx: &PolicyContext<'_>) -> Decision;
171}
172
173#[derive(Debug, Clone, Default)]
175pub struct GovernanceChain {
176 entries: Vec<Arc<dyn GovernancePolicy>>,
177}
178
179impl GovernanceChain {
180 #[must_use]
181 pub const fn new(entries: Vec<Arc<dyn GovernancePolicy>>) -> Self {
182 Self { entries }
183 }
184
185 pub fn push(&mut self, policy: Arc<dyn GovernancePolicy>) {
186 self.entries.push(policy);
187 }
188
189 #[must_use]
190 pub fn entries(&self) -> &[Arc<dyn GovernancePolicy>] {
191 &self.entries
192 }
193
194 #[must_use]
195 pub fn evaluate(&self, ctx: &PolicyContext<'_>) -> Decision {
196 for policy in &self.entries {
197 if let deny @ Decision::Deny { .. } = policy.evaluate(ctx) {
198 return deny;
199 }
200 }
201 Decision::Allow {
202 matched_by: crate::authz::types::MatchedBy::DefaultIncluded,
203 }
204 }
205}