1use super::SecurityProvider;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum TrustLevel {
10 Untrusted,
11 Derived,
12 Trusted,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum TaintLabel {
19 Unknown,
20 Sensitive,
21 Secret,
22 PromptInjection,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum SanitizationState {
31 Pending,
32 Applied,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct SecurityLabel {
38 pub trust: TrustLevel,
39 pub taint: TaintLabel,
40 pub sanitization: SanitizationState,
41}
42
43impl SecurityLabel {
44 pub const fn untrusted() -> Self {
45 Self {
46 trust: TrustLevel::Untrusted,
47 taint: TaintLabel::Unknown,
48 sanitization: SanitizationState::Pending,
49 }
50 }
51
52 pub const fn trusted() -> Self {
53 Self {
54 trust: TrustLevel::Trusted,
55 ..Self::untrusted()
56 }
57 }
58
59 pub const fn derived(taint: TaintLabel) -> Self {
60 Self {
61 trust: TrustLevel::Derived,
62 taint,
63 sanitization: SanitizationState::Pending,
64 }
65 }
66
67 fn sanitized(self) -> Self {
68 Self {
69 sanitization: SanitizationState::Applied,
70 ..self
71 }
72 }
73}
74
75#[must_use]
78#[derive(Clone, PartialEq, Eq)]
79pub struct TaintedValue<T> {
80 value: T,
81 label: SecurityLabel,
82}
83
84impl<T> std::fmt::Debug for TaintedValue<T> {
85 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 formatter
87 .debug_struct("TaintedValue")
88 .field("label", &self.label)
89 .finish_non_exhaustive()
90 }
91}
92
93impl<T> TaintedValue<T> {
94 pub fn new(value: T, label: SecurityLabel) -> Self {
95 Self { value, label }
96 }
97
98 pub fn untrusted(value: T) -> Self {
99 Self::new(value, SecurityLabel::untrusted())
100 }
101
102 pub fn trusted(value: T) -> Self {
103 Self::new(value, SecurityLabel::trusted())
104 }
105
106 pub fn label(&self) -> SecurityLabel {
107 self.label
108 }
109
110 pub fn value(&self) -> &T {
111 &self.value
112 }
113
114 pub fn into_parts(self) -> (T, SecurityLabel) {
115 (self.value, self.label)
116 }
117}
118
119pub fn sanitize_tainted_text(
121 provider: &dyn SecurityProvider,
122 value: TaintedValue<String>,
123) -> TaintedValue<String> {
124 let (value, label) = value.into_parts();
125 TaintedValue::new(provider.sanitize_output(&value), label.sanitized())
126}
127
128pub fn sanitize_text(provider: &dyn SecurityProvider, value: &str) -> String {
131 sanitize_tainted_text(provider, TaintedValue::untrusted(value.to_owned()))
132 .into_parts()
133 .0
134}
135
136pub fn sanitize_tainted_json(
139 provider: &dyn SecurityProvider,
140 value: TaintedValue<serde_json::Value>,
141) -> TaintedValue<serde_json::Value> {
142 fn sanitize(provider: &dyn SecurityProvider, value: serde_json::Value) -> serde_json::Value {
143 match value {
144 serde_json::Value::String(value) => {
145 serde_json::Value::String(provider.sanitize_output(&value))
146 }
147 serde_json::Value::Array(values) => serde_json::Value::Array(
148 values
149 .into_iter()
150 .map(|value| sanitize(provider, value))
151 .collect(),
152 ),
153 serde_json::Value::Object(values) => serde_json::Value::Object(
154 values
155 .into_iter()
156 .map(|(key, value)| (key, sanitize(provider, value)))
157 .collect(),
158 ),
159 value => value,
160 }
161 }
162
163 let (value, label) = value.into_parts();
164 TaintedValue::new(sanitize(provider, value), label.sanitized())
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::security::DefaultSecurityProvider;
171
172 #[test]
173 fn typed_value_boundary_preserves_provenance_and_redacts_nested_json() {
174 let provider = DefaultSecurityProvider::new();
175 let value = TaintedValue::untrusted(serde_json::json!({
176 "email": "user@example.com",
177 "nested": ["123-45-6789"],
178 }));
179 assert_eq!(value.label(), SecurityLabel::untrusted());
180
181 let (sanitized, label) = sanitize_tainted_json(&provider, value).into_parts();
182 assert_eq!(label.trust, TrustLevel::Untrusted);
183 assert_eq!(label.taint, TaintLabel::Unknown);
184 assert_eq!(label.sanitization, SanitizationState::Applied);
185 assert_eq!(sanitized["email"], "[REDACTED:EMAIL]");
186 assert_eq!(sanitized["nested"][0], "[REDACTED:SSN]");
187 }
188
189 #[test]
190 fn redaction_does_not_declassify_secrets_or_grant_instruction_authority() {
191 let value = TaintedValue::new(
192 "user@example.com".to_string(),
193 SecurityLabel {
194 taint: TaintLabel::Secret,
195 ..SecurityLabel::untrusted()
196 },
197 );
198 let sanitized = sanitize_tainted_text(&DefaultSecurityProvider::new(), value);
199 assert_eq!(sanitized.label().trust, TrustLevel::Untrusted);
200 assert_eq!(sanitized.label().taint, TaintLabel::Secret);
201 assert_eq!(sanitized.label().sanitization, SanitizationState::Applied);
202 assert_eq!(sanitized.value(), "[REDACTED:EMAIL]");
203 }
204
205 #[test]
206 fn value_debug_never_exposes_content() {
207 let value = TaintedValue::trusted("private-canary".to_string());
208 assert_eq!(value.label(), SecurityLabel::trusted());
209 assert!(!format!("{value:?}").contains("private-canary"));
210 }
211}