Skip to main content

harn_vm/
runtime_content.rs

1//! Linked runtime content identity for embedding hosts.
2//!
3//! This module owns the digest of Harn's embedded standard library and the
4//! composite identity of the runtime content compiled into this crate. Hosts
5//! project the typed receipt instead of reconstructing version, stdlib, or
6//! compatibility facts from their own build environment.
7
8use std::sync::OnceLock;
9
10use serde::{Deserialize, Serialize};
11use sha2::{Digest as _, Sha256};
12
13const FINGERPRINT_SCHEMA: &str = "harn.runtime_content_fingerprint.v1";
14const CONTENT_DIGEST_DOMAIN: &[u8] = b"harn.runtime-content.v1\0";
15
16/// Compatibility identities that decide whether linked Harn artifacts can be
17/// interpreted by this VM.
18#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
19#[serde(deny_unknown_fields)]
20pub struct RuntimeCompatibilityFingerprint {
21    pub codegen_fingerprint: String,
22    pub bytecode_schema_version: u32,
23    pub linked_program_schema_version: u32,
24    pub linker_algorithm_version: u32,
25    pub build_features: RuntimeBuildFeatures,
26}
27
28macro_rules! define_runtime_build_features {
29    ($( $field:ident => $cargo_name:literal ),+ $(,)?) => {
30        /// Cargo feature set compiled into the linked VM.
31        ///
32        /// Every declared `harn-vm` feature is represented explicitly so a lean
33        /// embedding cannot share an identity with the distributed full runtime.
34        #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
35        #[serde(deny_unknown_fields)]
36        pub struct RuntimeBuildFeatures {
37            $(pub $field: bool),+
38        }
39
40        impl RuntimeBuildFeatures {
41            fn linked() -> Self {
42                Self {$($field: cfg!(feature = $cargo_name)),+}
43            }
44
45            fn update_digest(&self, hasher: &mut Sha256) {
46                $(update_bool_field(hasher, concat!("feature-", $cargo_name), self.$field);)+
47            }
48
49            #[cfg(test)]
50            const CARGO_NAMES: &'static [&'static str] = &[$($cargo_name),+];
51
52            #[cfg(test)]
53            fn none() -> Self {
54                Self {$($field: false),+}
55            }
56
57            #[cfg(test)]
58            fn enable(&mut self, cargo_name: &str) {
59                match cargo_name {
60                    $($cargo_name => self.$field = true),+,
61                    _ => unreachable!("feature list and typed fields share one declaration"),
62                }
63            }
64        }
65    };
66}
67
68define_runtime_build_features! {
69    default => "default",
70    full => "full",
71    llm_bench_internals => "llm-bench-internals",
72    vm_bench_internals => "vm-bench-internals",
73    content => "content",
74    compression => "compression",
75    http_compression => "http-compression",
76    cloud_aws => "cloud-aws",
77    native_keyring => "native-keyring",
78    postgres => "postgres",
79    sqlite => "sqlite",
80    otel => "otel",
81    testbench_wasi => "testbench-wasi",
82}
83
84/// Typed identity of the Harn runtime content linked into an embedding host.
85///
86/// `content_sha256` is derived only from compiled/runtime content. The optional
87/// source revision is provenance metadata and deliberately cannot change that
88/// digest by itself.
89#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
90#[serde(deny_unknown_fields)]
91pub struct RuntimeContentFingerprint {
92    pub schema: String,
93    pub content_sha256: String,
94    pub harn_version: String,
95    pub embedded_stdlib_sha256: String,
96    pub compatibility: RuntimeCompatibilityFingerprint,
97    pub source_revision: Option<String>,
98}
99
100/// Return the content fingerprint of the Harn VM linked into this process.
101#[must_use]
102pub fn runtime_content_fingerprint() -> &'static RuntimeContentFingerprint {
103    static FINGERPRINT: OnceLock<RuntimeContentFingerprint> = OnceLock::new();
104    FINGERPRINT.get_or_init(|| {
105        let compatibility = RuntimeCompatibilityFingerprint {
106            codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT.to_string(),
107            bytecode_schema_version: crate::bytecode_cache::SCHEMA_VERSION,
108            linked_program_schema_version: crate::linked_program::LINKED_PROGRAM_SCHEMA_VERSION,
109            linker_algorithm_version: crate::linked_program::LINKER_ALGORITHM_VERSION,
110            build_features: RuntimeBuildFeatures::linked(),
111        };
112        fingerprint_from_parts(
113            crate::bytecode_cache::HARN_VERSION,
114            embedded_stdlib_digest_bytes(),
115            compatibility,
116            linked_source_revision(),
117        )
118    })
119}
120
121pub(crate) fn embedded_stdlib_digest_bytes() -> &'static [u8; 32] {
122    static DIGEST: OnceLock<[u8; 32]> = OnceLock::new();
123    DIGEST.get_or_init(|| {
124        embedded_stdlib_digest_from_sources(
125            harn_stdlib::STDLIB_SOURCES
126                .iter()
127                .map(|source| (source.module, source.source)),
128        )
129    })
130}
131
132fn embedded_stdlib_digest_from_sources<'a>(
133    sources: impl IntoIterator<Item = (&'a str, &'a str)>,
134) -> [u8; 32] {
135    let mut entries: Vec<(&str, &str)> = sources.into_iter().collect();
136    entries.sort_by(|left, right| left.0.cmp(right.0));
137    let mut hasher = Sha256::new();
138    for (module, source) in entries {
139        hasher.update(module.as_bytes());
140        hasher.update(b"\0");
141        hasher.update(source.as_bytes());
142        hasher.update(b"\0");
143    }
144    hasher.finalize().into()
145}
146
147fn fingerprint_from_parts(
148    harn_version: &str,
149    embedded_stdlib_digest: &[u8; 32],
150    compatibility: RuntimeCompatibilityFingerprint,
151    source_revision: Option<String>,
152) -> RuntimeContentFingerprint {
153    let mut hasher = Sha256::new();
154    hasher.update(CONTENT_DIGEST_DOMAIN);
155    update_field(&mut hasher, "harn-version", harn_version.as_bytes());
156    update_field(
157        &mut hasher,
158        "embedded-stdlib-sha256",
159        embedded_stdlib_digest,
160    );
161    update_field(
162        &mut hasher,
163        "codegen-fingerprint",
164        compatibility.codegen_fingerprint.as_bytes(),
165    );
166    update_field(
167        &mut hasher,
168        "bytecode-schema-version",
169        &compatibility.bytecode_schema_version.to_le_bytes(),
170    );
171    update_field(
172        &mut hasher,
173        "linked-program-schema-version",
174        &compatibility.linked_program_schema_version.to_le_bytes(),
175    );
176    update_field(
177        &mut hasher,
178        "linker-algorithm-version",
179        &compatibility.linker_algorithm_version.to_le_bytes(),
180    );
181    compatibility.build_features.update_digest(&mut hasher);
182    RuntimeContentFingerprint {
183        schema: FINGERPRINT_SCHEMA.to_string(),
184        content_sha256: hex(&hasher.finalize()),
185        harn_version: harn_version.to_string(),
186        embedded_stdlib_sha256: hex(embedded_stdlib_digest),
187        compatibility,
188        source_revision,
189    }
190}
191
192fn update_bool_field(hasher: &mut Sha256, name: &str, enabled: bool) {
193    update_field(hasher, name, &[u8::from(enabled)]);
194}
195
196fn update_field(hasher: &mut Sha256, name: &str, value: &[u8]) {
197    hasher.update(name.as_bytes());
198    hasher.update(b"\0");
199    hasher.update((value.len() as u64).to_le_bytes());
200    hasher.update(value);
201}
202
203fn linked_source_revision() -> Option<String> {
204    normalize_source_revision(option_env!("HARN_BUILD_REVISION")).map(str::to_string)
205}
206
207fn normalize_source_revision(raw: Option<&str>) -> Option<&str> {
208    let revision = raw?.trim();
209    if matches!(revision.len(), 40 | 64)
210        && revision
211            .bytes()
212            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
213    {
214        Some(revision)
215    } else {
216        None
217    }
218}
219
220fn hex(bytes: &[u8]) -> String {
221    let mut output = String::with_capacity(bytes.len() * 2);
222    for byte in bytes {
223        use std::fmt::Write as _;
224        write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
225    }
226    output
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    fn lean_features() -> RuntimeBuildFeatures {
234        RuntimeBuildFeatures::none()
235    }
236
237    fn compatibility() -> RuntimeCompatibilityFingerprint {
238        RuntimeCompatibilityFingerprint {
239            codegen_fingerprint: "codegen-a".to_string(),
240            bytecode_schema_version: 14,
241            linked_program_schema_version: 1,
242            linker_algorithm_version: 1,
243            build_features: lean_features(),
244        }
245    }
246
247    #[test]
248    fn embedded_stdlib_digest_changes_when_one_source_byte_changes() {
249        let original = embedded_stdlib_digest_from_sources([("agent", "pub fn run() {}")]);
250        let changed = embedded_stdlib_digest_from_sources([("agent", "pub fn run() { }")]);
251        assert_ne!(original, changed);
252    }
253
254    #[test]
255    fn source_stamp_cannot_change_content_digest() {
256        let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
257        let first = fingerprint_from_parts(
258            "0.10.123-dev",
259            &stdlib,
260            compatibility(),
261            Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()),
262        );
263        let second = fingerprint_from_parts(
264            "0.10.123-dev",
265            &stdlib,
266            compatibility(),
267            Some("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string()),
268        );
269        assert_eq!(first.content_sha256, second.content_sha256);
270        assert_ne!(first.source_revision, second.source_revision);
271    }
272
273    #[test]
274    fn compatibility_change_changes_content_digest() {
275        let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
276        let first = fingerprint_from_parts("0.10.123-dev", &stdlib, compatibility(), None);
277        let mut changed = compatibility();
278        changed.bytecode_schema_version += 1;
279        let second = fingerprint_from_parts("0.10.123-dev", &stdlib, changed, None);
280        assert_ne!(first.content_sha256, second.content_sha256);
281    }
282
283    #[test]
284    fn full_and_lean_builds_have_distinct_content_digests() {
285        let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
286        let lean = fingerprint_from_parts("0.10.123-dev", &stdlib, compatibility(), None);
287        let mut full_compatibility = compatibility();
288        for name in [
289            "default",
290            "full",
291            "postgres",
292            "sqlite",
293            "content",
294            "compression",
295            "http-compression",
296            "cloud-aws",
297            "native-keyring",
298        ] {
299            full_compatibility.build_features.enable(name);
300        }
301        let full = fingerprint_from_parts("0.10.123-dev", &stdlib, full_compatibility, None);
302        assert_ne!(lean.content_sha256, full.content_sha256);
303    }
304
305    #[test]
306    fn every_typed_build_feature_changes_content_digest() {
307        let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
308        let baseline = fingerprint_from_parts("0.10.123-dev", &stdlib, compatibility(), None);
309        for name in RuntimeBuildFeatures::CARGO_NAMES {
310            let mut changed = compatibility();
311            changed.build_features.enable(name);
312            let fingerprint = fingerprint_from_parts("0.10.123-dev", &stdlib, changed, None);
313            assert_ne!(
314                baseline.content_sha256, fingerprint.content_sha256,
315                "feature {name} must affect runtime content identity"
316            );
317        }
318    }
319
320    #[test]
321    fn typed_build_features_cover_every_declared_cargo_feature() {
322        let manifest: toml::Value =
323            toml::from_str(include_str!("../Cargo.toml")).expect("harn-vm Cargo.toml parses");
324        let mut declared: Vec<&str> = manifest["features"]
325            .as_table()
326            .expect("features table")
327            .keys()
328            .map(String::as_str)
329            .collect();
330        declared.sort_unstable();
331        let mut represented = RuntimeBuildFeatures::CARGO_NAMES.to_vec();
332        represented.sort_unstable();
333        assert_eq!(declared, represented);
334    }
335
336    #[test]
337    fn missing_source_revision_remains_absent() {
338        let stdlib = embedded_stdlib_digest_from_sources([("agent", "source")]);
339        let fingerprint = fingerprint_from_parts("0.10.123-dev", &stdlib, compatibility(), None);
340        assert_eq!(fingerprint.source_revision, None);
341        assert_eq!(normalize_source_revision(Some("")), None);
342        assert_eq!(normalize_source_revision(Some("not-a-revision")), None);
343    }
344
345    #[test]
346    fn public_fingerprint_matches_current_binary_inputs() {
347        let actual = runtime_content_fingerprint();
348        assert_eq!(actual.schema, FINGERPRINT_SCHEMA);
349        assert_eq!(actual.harn_version, crate::bytecode_cache::HARN_VERSION);
350        assert_eq!(
351            actual.embedded_stdlib_sha256,
352            hex(embedded_stdlib_digest_bytes())
353        );
354        assert_eq!(
355            actual.compatibility.codegen_fingerprint,
356            crate::bytecode_cache::CODEGEN_FINGERPRINT
357        );
358        assert_eq!(
359            actual.compatibility.build_features,
360            RuntimeBuildFeatures::linked()
361        );
362        assert_eq!(actual.content_sha256.len(), 64);
363    }
364}