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