Skip to main content

a3s_code_core/security/
value.rs

1//! Typed provenance for values crossing runtime security boundaries.
2
3use super::SecurityProvider;
4use serde::{Deserialize, Serialize};
5
6/// Trust provenance assigned by the owning adapter, never inferred from text.
7#[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/// Taint classification is independent of instruction authority and redaction.
16#[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/// Whether the configured provider has processed the complete value.
26///
27/// `Applied` is not a guarantee that the value is public or safe to execute.
28#[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/// A label travels with its value until an explicit boundary consumes it.
76/// These labels describe provenance; they never grant execution permission.
77#[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
119/// Apply output sanitization without promoting trust or declassifying taint.
120pub 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
128/// Convenience adapter for text values that enter the egress boundary without
129/// an existing wrapper.
130pub 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
136/// Process complete JSON string values while retaining keys and protocol shape.
137/// Keys are protocol field names, not a channel for arbitrary output text.
138pub 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}