Skip to main content

ci_engine/
env.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Hermetic check environment construction.
3
4use std::collections::BTreeMap;
5
6/// Host variables needed to locate ordinary POSIX/Rust tooling.
7pub const BASE_ALLOWLIST: &[&str] = &[
8    "PATH",
9    "HOME",
10    "USER",
11    "SHELL",
12    "TERM",
13    "LANG",
14    "LC_ALL",
15    "CARGO_HOME",
16    "RUSTUP_HOME",
17    "TMPDIR",
18    "TEMP",
19];
20/// Deterministic Git author/committer name.
21pub const GIT_IDENTITY_NAME: &str = "heddle ci";
22/// Deterministic Git author/committer email.
23pub const GIT_IDENTITY_EMAIL: &str = "ci@heddle.invalid";
24
25/// Environment fields permitted in signed verdicts. None are needed: the
26/// definition digest identifies authored inputs, and the command, services and
27/// outcome identify the check and its result. Host, service, cache and literal
28/// check values may contain private paths or identities; keep them runtime-only.
29pub(crate) const VERDICT_ENV_ALLOWLIST: &[&str] = &[];
30
31/// Builder for the runtime environment, separate from signed evidence.
32#[derive(Debug, Clone)]
33pub struct HermeticEnv {
34    git_hermetic: bool,
35    host: BTreeMap<String, String>,
36}
37
38impl HermeticEnv {
39    /// Capture the allowed variables from the current process.
40    #[must_use]
41    pub fn new() -> Self {
42        let host = BASE_ALLOWLIST
43            .iter()
44            .filter_map(|name| {
45                std::env::var(name)
46                    .ok()
47                    .map(|value| ((*name).to_string(), value))
48            })
49            .collect();
50        Self {
51            git_hermetic: true,
52            host,
53        }
54    }
55
56    /// Construct from an explicit host map, primarily for tests.
57    #[must_use]
58    pub fn with_host(host: BTreeMap<String, String>) -> Self {
59        Self {
60            git_hermetic: true,
61            host,
62        }
63    }
64
65    /// Enable or disable deterministic Git configuration.
66    #[must_use]
67    pub fn git_hermetic(mut self, enabled: bool) -> Self {
68        self.git_hermetic = enabled;
69        self
70    }
71
72    /// Produce the sorted effective environment.
73    #[must_use]
74    pub fn build(
75        &self,
76        check: &BTreeMap<String, String>,
77        services: &BTreeMap<String, String>,
78        caches: &BTreeMap<String, String>,
79    ) -> BTreeMap<String, String> {
80        let mut output = self.host.clone();
81        if self.git_hermetic {
82            output.insert("GIT_CONFIG_GLOBAL".into(), "/dev/null".into());
83            output.insert("GIT_CONFIG_SYSTEM".into(), "/dev/null".into());
84            output.insert("GIT_AUTHOR_NAME".into(), GIT_IDENTITY_NAME.into());
85            output.insert("GIT_AUTHOR_EMAIL".into(), GIT_IDENTITY_EMAIL.into());
86            output.insert("GIT_COMMITTER_NAME".into(), GIT_IDENTITY_NAME.into());
87            output.insert("GIT_COMMITTER_EMAIL".into(), GIT_IDENTITY_EMAIL.into());
88        }
89        for source in [services, caches, check] {
90            output.extend(
91                source
92                    .iter()
93                    .map(|(key, value)| (key.clone(), value.clone())),
94            );
95        }
96        output
97    }
98}
99
100impl Default for HermeticEnv {
101    fn default() -> Self {
102        Self::new()
103    }
104}