Skip to main content

smolvm_protocol/
secrets.rs

1//! Secret reference types shared across smolvm surfaces.
2//!
3//! A [`SecretRef`] is a *pointer* to a secret on the host: either a host
4//! environment variable or a host file. Refs travel across trust
5//! boundaries (HTTP request bodies, persisted VM records, `.smolmachine`
6//! pack manifests); resolved plaintext values do not. smolvm itself does
7//! not store secret material — it only references whatever secrets
8//! manager already holds it (your shell env, Vault/1Password/etc. rendered
9//! to an env var or file).
10//!
11//! This crate carries only the *shape* of a ref — the on-the-wire and
12//! on-disk representation plus trivial introspection. The validation
13//! policy (which source kinds are allowed at which trust boundary)
14//! lives in the host crate alongside the code that enforces it. See
15//! `smolvm::secrets` for `ResolutionScope` and `validate_ref`.
16
17use serde::{Deserialize, Serialize};
18use std::path::PathBuf;
19
20/// Which source a [`SecretRef`] points at, independent of the data
21/// inside. Used by audit logging so the logger never sees the path or
22/// env-var name (which can themselves be revealing).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum SecretSourceKind {
25    /// The ref points at a host environment variable.
26    Env,
27    /// The ref points at a host file path.
28    File,
29}
30
31impl SecretSourceKind {
32    /// Human-readable label.
33    pub fn as_str(self) -> &'static str {
34        match self {
35            Self::Env => "env",
36            Self::File => "file",
37        }
38    }
39}
40
41/// A reference to a secret. Exactly one of the two sources must be
42/// populated; validation is performed by the host crate's
43/// `validate_ref` (policy lives where it's enforced).
44///
45/// Round-trips through `serde_json` for persistence in the VM record DB
46/// and in `.smolmachine` pack manifests. Refs are not sensitive; the
47/// resolved plaintext is produced only at the workload launch site and
48/// never touches any of these stores.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct SecretRef {
52    /// Read the secret from a host environment variable.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub from_env: Option<String>,
55
56    /// Read the secret from a host file path (must be absolute).
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub from_file: Option<PathBuf>,
59}
60
61impl SecretRef {
62    /// Return the source kind for this ref, if exactly one source is set.
63    ///
64    /// Returns `None` for structurally invalid refs (0 or >1 sources).
65    /// Callers are expected to have already validated with the host
66    /// crate's `validate_ref` before calling this; this function is
67    /// primarily for audit logging of a known-good ref.
68    pub fn source_kind(&self) -> Option<SecretSourceKind> {
69        match (self.from_env.is_some(), self.from_file.is_some()) {
70            (true, false) => Some(SecretSourceKind::Env),
71            (false, true) => Some(SecretSourceKind::File),
72            _ => None,
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn source_kind_reports_variant() {
83        assert_eq!(
84            SecretRef {
85                from_env: Some("Y".into()),
86                from_file: None,
87            }
88            .source_kind(),
89            Some(SecretSourceKind::Env)
90        );
91        assert_eq!(
92            SecretRef {
93                from_env: None,
94                from_file: Some(PathBuf::from("/z")),
95            }
96            .source_kind(),
97            Some(SecretSourceKind::File)
98        );
99        let empty = SecretRef {
100            from_env: None,
101            from_file: None,
102        };
103        assert_eq!(empty.source_kind(), None);
104        let both = SecretRef {
105            from_env: Some("Y".into()),
106            from_file: Some(PathBuf::from("/z")),
107        };
108        assert_eq!(both.source_kind(), None);
109    }
110
111    #[test]
112    fn deny_unknown_fields() {
113        // `from_store` was removed; it must now be rejected like any typo.
114        let bad = r#"{ "from_store": "gone" }"#;
115        let res: Result<SecretRef, _> = serde_json::from_str(bad);
116        assert!(res.is_err());
117    }
118
119    #[test]
120    fn roundtrip_json() {
121        let r = SecretRef {
122            from_env: Some("API_KEY".into()),
123            from_file: None,
124        };
125        let json = serde_json::to_string(&r).unwrap();
126        let back: SecretRef = serde_json::from_str(&json).unwrap();
127        assert_eq!(r, back);
128    }
129
130    #[test]
131    fn serialization_omits_empty_fields() {
132        let r = SecretRef {
133            from_env: Some("X".into()),
134            from_file: None,
135        };
136        let json = serde_json::to_string(&r).unwrap();
137        assert!(json.contains("from_env"));
138        assert!(!json.contains("from_file"));
139    }
140}