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::UnknownAccessScope(other.to_owned())),
141 }
142 }
143}
144
145#[derive(Debug)]
146pub struct PolicyContext<'a> {
147 pub target: GovernedTarget,
148 pub agent_scope: AgentScope,
149 pub access_scope: AccessScope,
150 pub session_id: &'a SessionId,
151 pub user_id: &'a UserId,
152 pub input: &'a GovernedInput,
153 pub call_id: &'a CallId,
154}
155
156impl PolicyContext<'_> {
157 #[must_use]
158 pub fn with_input<'b>(&'b self, input: &'b GovernedInput) -> PolicyContext<'b> {
159 PolicyContext {
160 target: self.target.clone(),
161 agent_scope: self.agent_scope.clone(),
162 access_scope: self.access_scope,
163 session_id: self.session_id,
164 user_id: self.user_id,
165 input,
166 call_id: self.call_id,
167 }
168 }
169}
170
171pub trait GovernancePolicy: Send + Sync + fmt::Debug {
188 fn secret_scanner(&self) -> Option<&SecretScanner> {
189 None
190 }
191 fn id(&self) -> PolicyId;
192 fn name(&self) -> &'static str;
193 fn description(&self) -> &'static str;
194 fn evaluate(&self, ctx: &PolicyContext<'_>) -> Decision;
195 fn prompt_secret_findings(&self, _input: &GovernedInput) -> Option<Vec<SecretFinding>> {
196 None
197 }
198}