Skip to main content

varve_core/
bazel.rs

1//! Bazel checksum-registry compilation (REQ-BAZEL-001).
2//!
3//! rules_wasm_component pins tools with per-tool JSON checksum registries
4//! and a pure sha256 download path. This module compiles those registries
5//! FROM a verified layer manifest: the hashes Bazel enforces become
6//! transcriptions from a signed, counter-protected document instead of
7//! trust-on-first-use hashes of whatever a release page served. Bazel's
8//! fetch path does not change; its trust anchor does.
9//!
10//! The digests exported are the SOURCE-ASSET digests (the bytes Bazel
11//! downloads), recorded inside the signed payload at deposit time — the
12//! layer's own entry digests cover the extracted binaries, which Bazel
13//! never sees.
14
15use std::collections::BTreeMap;
16
17use crate::manifest::LayerManifest;
18
19pub const ANN_SRC_REPO: &str = "eu.pulseengine.source.repo";
20pub const ANN_SRC_RELEASE: &str = "eu.pulseengine.source.release";
21pub const ANN_SRC_ASSET: &str = "eu.pulseengine.source.asset";
22pub const ANN_SRC_SHA256: &str = "eu.pulseengine.source.sha256";
23
24pub const ANN_RUNNER: &str = "eu.pulseengine.runner";
25pub const ANN_RUNNER_ARGS: &str = "eu.pulseengine.runner-args";
26pub const ANN_RUNNER_ARG_PREFIX: &str = "eu.pulseengine.runner-arg-prefix";
27
28/// Map a target triple to rules_wasm_component's platform-key vocabulary.
29pub fn bazel_platform_key(triple: &str) -> Option<&'static str> {
30    match triple {
31        "aarch64-apple-darwin" => Some("darwin_arm64"),
32        "x86_64-apple-darwin" => Some("darwin_amd64"),
33        "aarch64-unknown-linux-gnu" => Some("linux_arm64"),
34        "x86_64-unknown-linux-gnu" => Some("linux_amd64"),
35        "x86_64-pc-windows-msvc" => Some("windows_amd64"),
36        _ => None,
37    }
38}
39
40/// One compiled registry per tool, plus what could not be compiled and why.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct BazelExport {
43    /// tool name → registry JSON (rules_wasm_component schema).
44    pub registries: BTreeMap<String, serde_json::Value>,
45    /// (tool, platform, reason) skipped — loud, never silent.
46    pub skipped: Vec<(String, String, String)>,
47}
48
49/// Compile the registries from a (verified-by-the-caller) layer manifest.
50pub fn export(manifest: &LayerManifest) -> BazelExport {
51    let mut skipped = Vec::new();
52    // tool → version → bazel platform key → {sha256, url_suffix}
53    let mut tools: BTreeMap<String, (String, String, BTreeMap<String, serde_json::Value>)> =
54        BTreeMap::new();
55
56    for entry in &manifest.entries {
57        let ann = &entry.annotations;
58        let Some(tool) = ann.get("eu.pulseengine.tool") else {
59            continue;
60        };
61        let version = ann
62            .get("eu.pulseengine.tool.version")
63            .cloned()
64            .unwrap_or_default();
65        let platform = ann
66            .get(crate::platform::ANN_PLATFORM)
67            .cloned()
68            .unwrap_or_default();
69        let Some(key) = bazel_platform_key(&platform) else {
70            skipped.push((
71                tool.clone(),
72                platform.clone(),
73                "platform has no Bazel key".into(),
74            ));
75            continue;
76        };
77        let (Some(repo), Some(asset), Some(src_sha)) = (
78            ann.get(ANN_SRC_REPO),
79            ann.get(ANN_SRC_ASSET),
80            ann.get(ANN_SRC_SHA256),
81        ) else {
82            skipped.push((
83                tool.clone(),
84                platform.clone(),
85                "no source provenance recorded at deposit".into(),
86            ));
87            continue;
88        };
89        let hex = src_sha
90            .strip_prefix("sha256:")
91            .unwrap_or(src_sha)
92            .to_string();
93        let slot = tools
94            .entry(tool.clone())
95            .or_insert_with(|| (repo.clone(), version.clone(), BTreeMap::new()));
96        slot.2.insert(
97            key.to_string(),
98            serde_json::json!({ "sha256": hex, "url_suffix": asset }),
99        );
100    }
101
102    let registries = tools
103        .into_iter()
104        .map(|(tool, (repo, version, platforms))| {
105            let json = serde_json::json!({
106                // The header states exactly what varve vouched for, and stops
107                // there. `sha256` below is the SOURCE-ASSET hash: recorded
108                // verbatim from the deposit spec into the signed payload, and
109                // never checked against the asset it names — varve never
110                // fetches that asset. A release engineer deposited all zeros
111                // and nothing objected anywhere in the chain. Being inside a
112                // signature makes a value ATTRIBUTABLE, not true, and a header
113                // that reads as though varve had verified it turns a
114                // transcription into a warrant.
115                "_generated_by": format!(
116                    "varve export-bazel — layer {} (counter {}); digests transcribed from the \
117                     signed layer manifest. The source-asset sha256 is transcribed from the \
118                     deposit spec and is NOT verified by varve against the asset it names. \
119                     Do not hand-edit.",
120                    manifest.layer, manifest.counter
121                ),
122                "tool_name": tool,
123                "github_repo": repo,
124                "latest_version": version,
125                "versions": {
126                    version.clone(): { "platforms": platforms }
127                }
128            });
129            (tool, json)
130        })
131        .collect();
132    BazelExport {
133        registries,
134        skipped,
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    fn manifest_with_sources() -> LayerManifest {
143        let payload = r#"{
144  "schemaVersion": 2,
145  "mediaType": "application/vnd.oci.image.index.v1+json",
146  "annotations": {
147    "eu.pulseengine.varve.layer": "2026.08.1",
148    "eu.pulseengine.varve.channel": "rolling",
149    "eu.pulseengine.varve.counter": "2",
150    "org.opencontainers.image.created": "2026-08-07T00:00:00Z"
151  },
152  "manifests": [
153    {
154      "digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
155      "annotations": {
156        "eu.pulseengine.tool": "rivet",
157        "eu.pulseengine.tool.version": "0.32.0",
158        "eu.pulseengine.platform": "aarch64-apple-darwin",
159        "eu.pulseengine.source.repo": "pulseengine/rivet",
160        "eu.pulseengine.source.release": "v0.32.0",
161        "eu.pulseengine.source.asset": "rivet-v0.32.0-aarch64-apple-darwin.tar.gz",
162        "eu.pulseengine.source.sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
163      }
164    },
165    {
166      "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
167      "annotations": {
168        "eu.pulseengine.tool": "rivet",
169        "eu.pulseengine.tool.version": "0.32.0",
170        "eu.pulseengine.platform": "x86_64-unknown-linux-gnu",
171        "eu.pulseengine.source.repo": "pulseengine/rivet",
172        "eu.pulseengine.source.release": "v0.32.0",
173        "eu.pulseengine.source.asset": "rivet-v0.32.0-x86_64-unknown-linux-gnu.tar.gz",
174        "eu.pulseengine.source.sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
175      }
176    },
177    {
178      "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
179      "annotations": {
180        "eu.pulseengine.tool": "wsc",
181        "eu.pulseengine.tool.version": "0.10.0",
182        "eu.pulseengine.platform": "aarch64-apple-darwin"
183      }
184    }
185  ]
186}"#;
187        LayerManifest::parse(payload.as_bytes()).unwrap()
188    }
189
190    // rivet: verifies REQ-BAZEL-001
191    #[test]
192    fn registries_compile_in_the_rules_schema_with_source_digests() {
193        let export = export(&manifest_with_sources());
194        let rivet = &export.registries["rivet"];
195        assert_eq!(rivet["tool_name"], "rivet");
196        assert_eq!(rivet["github_repo"], "pulseengine/rivet");
197        assert_eq!(rivet["latest_version"], "0.32.0");
198        let platforms = &rivet["versions"]["0.32.0"]["platforms"];
199        assert_eq!(
200            platforms["darwin_arm64"]["sha256"],
201            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
202        );
203        assert_eq!(
204            platforms["darwin_arm64"]["url_suffix"],
205            "rivet-v0.32.0-aarch64-apple-darwin.tar.gz"
206        );
207        // sha256: prefix normalized to bare hex, per the rules schema.
208        assert_eq!(
209            platforms["linux_amd64"]["sha256"],
210            "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
211        );
212        // Provenance header names the layer and forbids hand-editing.
213        let header = rivet["_generated_by"].as_str().unwrap();
214        assert!(header.contains("2026.08.1") && header.contains("Do not hand-edit"));
215        // …and it must not overclaim. The `sha256` asserted above is the
216        // SOURCE-ASSET hash, transcribed from the deposit spec and never
217        // checked against the asset it names — varve never fetches it. This
218        // very fixture proves it: `aaaa…`/`bbbb…` are obviously not any real
219        // release's digest, and they travel through deposit, signature and
220        // export without a word. A header that reads as though varve had
221        // verified them turns a transcription into a warrant.
222        assert!(
223            header.contains("NOT verified by varve"),
224            "the header must not claim a check varve does not perform: {header}"
225        );
226    }
227
228    // rivet: verifies REQ-BAZEL-001
229    #[test]
230    fn a_tool_without_source_provenance_is_skipped_loudly() {
231        let export = export(&manifest_with_sources());
232        assert!(!export.registries.contains_key("wsc"));
233        assert!(
234            export
235                .skipped
236                .iter()
237                .any(|(tool, _, reason)| tool == "wsc" && reason.contains("no source provenance")),
238            "skips must be reported: {:?}",
239            export.skipped
240        );
241    }
242
243    // rivet: verifies REQ-BAZEL-001
244    #[test]
245    fn platform_keys_map_the_rules_vocabulary() {
246        assert_eq!(
247            bazel_platform_key("aarch64-apple-darwin"),
248            Some("darwin_arm64")
249        );
250        assert_eq!(
251            bazel_platform_key("x86_64-apple-darwin"),
252            Some("darwin_amd64")
253        );
254        assert_eq!(
255            bazel_platform_key("aarch64-unknown-linux-gnu"),
256            Some("linux_arm64")
257        );
258        assert_eq!(
259            bazel_platform_key("x86_64-unknown-linux-gnu"),
260            Some("linux_amd64")
261        );
262        assert_eq!(
263            bazel_platform_key("x86_64-pc-windows-msvc"),
264            Some("windows_amd64")
265        );
266        assert_eq!(bazel_platform_key("wasm32-wasip2"), None);
267    }
268}