systemprompt_security/policy/
types.rs1use std::fmt;
14use std::str::FromStr;
15
16use serde::{Deserialize, Serialize};
17use systemprompt_identifiers::{CallId, PolicyId, SessionId, UserId};
18
19use super::governed::{GovernedInput, GovernedTarget};
20use super::secrets::{SecretFinding, SecretScanner};
21use crate::authz::error::AuthzError;
22use crate::authz::types::Decision;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct SecretLocation {
31 pub kind: String,
32 pub path: String,
33 pub redacted: String,
34}
35
36impl SecretLocation {
37 pub fn new(
38 kind: impl Into<String>,
39 path: impl Into<String>,
40 redacted: impl Into<String>,
41 ) -> Self {
42 Self {
43 kind: kind.into(),
44 path: path.into(),
45 redacted: redacted.into(),
46 }
47 }
48}
49
50impl fmt::Display for SecretLocation {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 if self.path.is_empty() {
53 write!(f, "{} ({})", self.kind, self.redacted)
54 } else {
55 write!(f, "{}.{} ({})", self.kind, self.path, self.redacted)
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct RateLimitWindow {
62 pub name: String,
63 pub seconds: u64,
64 pub limit: u64,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(tag = "kind", rename_all = "snake_case")]
72pub enum AgentScope {
73 User { user_id: UserId },
74 System,
75}
76
77impl AgentScope {
78 #[must_use]
79 pub const fn user_id(&self) -> Option<&UserId> {
80 match self {
81 Self::User { user_id } => Some(user_id),
82 Self::System => None,
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
96#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
97#[serde(rename_all = "lowercase")]
98pub enum AccessScope {
99 Admin,
100 User,
101 Unknown,
102}
103
104impl AccessScope {
105 #[must_use]
106 pub fn from_roles<S: AsRef<str>>(roles: &[S]) -> Self {
107 if roles.iter().any(|r| r.as_ref() == "admin") {
108 Self::Admin
109 } else if roles.iter().any(|r| r.as_ref() == "user") {
110 Self::User
111 } else {
112 Self::Unknown
113 }
114 }
115
116 #[must_use]
117 pub const fn as_str(self) -> &'static str {
118 match self {
119 Self::Admin => "admin",
120 Self::User => "user",
121 Self::Unknown => "unknown",
122 }
123 }
124}
125
126impl fmt::Display for AccessScope {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 f.write_str(self.as_str())
129 }
130}
131
132impl FromStr for AccessScope {
133 type Err = AuthzError;
134
135 fn from_str(s: &str) -> Result<Self, Self::Err> {
136 match s {
137 "admin" => Ok(Self::Admin),
138 "user" => Ok(Self::User),
139 "unknown" | "" => Ok(Self::Unknown),
140 other => Err(AuthzError::Validation(format!(
141 "unknown access scope: {other}"
142 ))),
143 }
144 }
145}
146
147#[derive(Debug)]
148pub struct PolicyContext<'a> {
149 pub target: GovernedTarget,
150 pub agent_scope: AgentScope,
151 pub access_scope: AccessScope,
152 pub session_id: &'a SessionId,
153 pub user_id: &'a UserId,
154 pub input: &'a GovernedInput,
155 pub call_id: &'a CallId,
156}
157
158impl PolicyContext<'_> {
159 #[must_use]
160 pub fn with_input<'b>(&'b self, input: &'b GovernedInput) -> PolicyContext<'b> {
161 PolicyContext {
162 target: self.target.clone(),
163 agent_scope: self.agent_scope.clone(),
164 access_scope: self.access_scope,
165 session_id: self.session_id,
166 user_id: self.user_id,
167 input,
168 call_id: self.call_id,
169 }
170 }
171}
172
173pub trait GovernancePolicy: Send + Sync + fmt::Debug {
190 fn secret_scanner(&self) -> Option<&SecretScanner> {
191 None
192 }
193 fn id(&self) -> PolicyId;
194 fn name(&self) -> &'static str;
195 fn description(&self) -> &'static str;
196 fn evaluate(&self, ctx: &PolicyContext<'_>) -> Decision;
197 fn prompt_secret_findings(&self, _input: &GovernedInput) -> Option<Vec<SecretFinding>> {
198 None
199 }
200}