speccoder_runtime/attest/
policy.rs1use serde_json::{Value, json};
4
5const JUDGE: &[&str] = &[
7 "checks",
8 "acceptance",
9 "tests",
10 "mutation",
11 "monitor",
12 "sandbox",
13 "executor",
14];
15
16pub fn judge_config_sha256(config: &str) -> Result<String, String> {
20 judge_config_sha256_as(config, env!("CARGO_PKG_VERSION"))
21}
22
23pub fn judge_config_sha256_as(config: &str, version: &str) -> Result<String, String> {
31 let table: toml::Table = toml::from_str(config).map_err(|e| e.to_string())?;
32 let section = |k: &str| {
33 table
34 .get(k)
35 .and_then(|v| serde_json::to_value(v).ok())
36 .unwrap_or(Value::Null)
37 };
38 let mut judge = serde_json::Map::new();
39 for k in JUDGE {
40 judge.insert((*k).to_owned(), section(k));
41 }
42 let gates = crate::hooks::gate_tables(&table);
44 if !gates.is_null() {
45 judge.insert("hooks".to_owned(), gates);
46 }
47 let model = table.get("model");
48 let field = |t: Option<&toml::Value>, k: &str| {
49 t.and_then(|m| m.get(k))
50 .and_then(toml::Value::as_str)
51 .map(str::to_owned)
52 };
53 judge.insert(
54 "model".to_owned(),
55 json!({ "provider": field(model, "provider"), "model": field(model, "model") }),
56 );
57 judge.insert(
58 "attest".to_owned(),
59 json!({ "signer": field(table.get("attest"), "signer").unwrap_or_else(|| "git".to_owned()) }),
60 );
61 judge.insert("speccoder_version".to_owned(), json!(version));
62 Ok(crate::integrity::sha256_hex(&super::statement::canonical(
63 &Value::Object(judge),
64 )))
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 const CONFIG: &str = "[model]\nprovider = \"openai\"\nmodel = \"m\"\nprice_input_per_mtok = 1.0\n\n[checks.test]\nargv = [\"cargo\", \"test\"]\n";
72
73 #[test]
74 fn comments_and_prices_keep_the_hash_and_a_check_changes_it() {
75 let base = judge_config_sha256(CONFIG).unwrap();
76 let commented = format!("# a note\n{}", CONFIG.replace("1.0", "2.5"));
77 assert_eq!(judge_config_sha256(&commented).unwrap(), base);
78 let keyed = CONFIG.replace(
79 "model = \"m\"\n",
80 "model = \"m\"\napi_key_env = \"MY_KEY\"\n",
81 );
82 assert_eq!(judge_config_sha256(&keyed).unwrap(), base);
83 let other = CONFIG.replace("\"test\"]", "\"test\", \"--all\"]");
84 assert_ne!(judge_config_sha256(&other).unwrap(), base);
85 let model = CONFIG.replace("model = \"m\"", "model = \"n\"");
86 assert_ne!(judge_config_sha256(&model).unwrap(), base);
87 }
88
89 #[test]
90 fn a_gate_changes_the_hash_and_an_observer_does_not() {
91 let base = judge_config_sha256(CONFIG).unwrap();
92 let observed = format!("{CONFIG}\n[hooks.chat]\non = [\"*\"]\nurl = \"https://x.dev/h\"\n");
93 assert_eq!(judge_config_sha256(&observed).unwrap(), base);
94 let gated = format!("{CONFIG}\n[hooks.licence]\nat = \"tick\"\nargv = [\"true\"]\n");
95 assert_ne!(judge_config_sha256(&gated).unwrap(), base);
96 }
97
98 #[test]
102 fn a_tick_is_recomputed_as_the_version_that_made_it() {
103 let ours = judge_config_sha256(CONFIG).unwrap();
104 assert_eq!(
105 judge_config_sha256_as(CONFIG, env!("CARGO_PKG_VERSION")).unwrap(),
106 ours
107 );
108 let older = judge_config_sha256_as(CONFIG, "0.9.7").unwrap();
109 assert_ne!(older, ours);
110 assert_eq!(judge_config_sha256_as(CONFIG, "0.9.7").unwrap(), older);
111 }
112}