metrale_bench/
serve_env.rs1use std::collections::BTreeMap;
26
27use anyhow::{Result, bail};
28use sha2::{Digest, Sha256};
29
30pub use metrale_config::levers::PREFIX;
32
33pub fn is_lever(name: &str) -> bool {
42 name.starts_with(PREFIX) && !metrale_config::levers::is_harness(name)
43}
44
45pub fn levers<I, K, V>(env: I) -> BTreeMap<String, String>
48where
49 I: IntoIterator<Item = (K, V)>,
50 K: AsRef<str>,
51 V: AsRef<str>,
52{
53 env.into_iter()
54 .filter(|(k, _)| is_lever(k.as_ref()))
55 .map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string()))
56 .collect()
57}
58
59pub fn process_levers() -> BTreeMap<String, String> {
65 levers(std::env::vars_os().map(|(k, v)| {
66 (
67 k.to_string_lossy().into_owned(),
68 v.to_string_lossy().into_owned(),
69 )
70 }))
71}
72
73#[must_use]
77pub fn fingerprint(levers: &BTreeMap<String, String>) -> String {
78 let mut h = Sha256::new();
79 for (k, v) in levers {
80 h.update(k.as_bytes());
81 h.update(b"=");
82 h.update(v.as_bytes());
83 h.update([0u8]);
84 }
85 metrale_closure::hex_lower(&h.finalize())
86}
87
88pub fn declared(owner: &str, raw: &BTreeMap<String, String>) -> Result<BTreeMap<String, String>> {
98 let mut out = BTreeMap::new();
99 for (name, value) in raw {
100 if !name.starts_with(PREFIX) {
101 bail!(
102 "{owner} declares env {name}, which is not a METRALE_* serve lever. The gate \
103 applies exactly the levers a recipe declares and nothing else, so a variable \
104 the server does not read cannot be declared here."
105 );
106 }
107 match metrale_config::levers::lookup(name).map(|l| l.class) {
108 Some(metrale_config::levers::Class::Runtime) => {}
109 Some(metrale_config::levers::Class::Harness) => bail!(
110 "{owner} declares env {name}, which places, logs or builds rather than \
111 measuring ({name} is a harness variable, not a serve lever) — it is the \
112 box's to set, never the recipe's."
113 ),
114 Some(class) => bail!(
115 "{owner} declares env {name}, which the server never reads (it is a {} \
116 variable), so declaring it here would change nothing that is measured.",
117 class.as_str()
118 ),
119 None => bail!(
120 "{owner} declares env {name}, which is not a declared lever: nothing reads it, \
121 so it is a typo or a lever that no longer exists, and `met serve` refuses \
122 to start with it set. `met dump-serve-options` lists every declared lever."
123 ),
124 }
125 if value.trim().is_empty() {
126 bail!(
127 "{owner} declares env {name} with an empty value. An exported-but-empty variable \
128 is how a shell spells \"unset\": declare a value or drop the key."
129 );
130 }
131 out.insert(name.clone(), value.clone());
132 }
133 Ok(out)
134}
135
136#[must_use]
139pub fn merge_declared(
140 recipe: BTreeMap<String, String>,
141 baseline: BTreeMap<String, String>,
142) -> BTreeMap<String, String> {
143 let mut out = recipe;
144 out.extend(baseline);
145 out
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct Reconciled {
152 pub env: BTreeMap<String, String>,
155 pub missing: BTreeMap<String, String>,
159}
160
161pub fn reconcile(
170 owner: &str,
171 declared: &BTreeMap<String, String>,
172 present: &BTreeMap<String, String>,
173) -> Result<Reconciled> {
174 let undeclared: Vec<String> = present
175 .iter()
176 .filter(|(k, _)| !declared.contains_key(*k))
177 .map(|(k, v)| format!("{k}={v}"))
178 .collect();
179 if !undeclared.is_empty() {
180 bail!(
181 "the harness environment carries {} serve lever(s) that {owner} does not declare: \
182 {}. A lever the recipe did not ask for changes what the gate measures \
183 (METRALE_FP8_ROWWISE=1 builds FP8 twin weights that a 0.70-util serve cannot \
184 boot with — #1242), so it is refused rather than inherited. Unset it — on a node \
185 agent, remove it from bench.yaml `env:`, which reaches EVERY gate on the box — or \
186 declare it in the recipe's `env:` (or the gate's [benchmarks.serve_env]) if the \
187 gate is measured under it.",
188 undeclared.len(),
189 undeclared.join(", ")
190 );
191 }
192 let mut missing = BTreeMap::new();
193 let mut contradicted = Vec::new();
194 for (k, want) in declared {
195 match present.get(k) {
196 Some(got) if got == want => {}
197 Some(got) => {
198 contradicted.push(format!("{k}: harness {got:?}, {owner} declares {want:?}"))
199 }
200 None => {
201 missing.insert(k.clone(), want.clone());
202 }
203 }
204 }
205 if !contradicted.is_empty() {
206 bail!(
207 "the harness environment contradicts {owner} on {} serve lever(s): {}. Neither value \
208 wins silently: unset the variable so the declaration applies, or change the \
209 declaration.",
210 contradicted.len(),
211 contradicted.join("; ")
212 );
213 }
214 Ok(Reconciled {
215 env: declared.clone(),
216 missing,
217 })
218}
219
220#[cfg(test)]
221#[path = "serve_env_tests.rs"]
222mod tests;