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}