a3s_code_core/
completion_attestor.rs1use std::sync::Arc;
19
20use crate::harness_loop::MutationLedger;
21use crate::read_only_verifier::{accept_report, ReportAuthor};
22use crate::verification::VerificationReport;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct MutatedPathRecord {
27 pub path: String,
29 pub content_digest: String,
31}
32
33pub trait CompletionAttestor: Send + Sync {
39 fn attest(
41 &self,
42 effect_digest: &str,
43 paths: &[MutatedPathRecord],
44 ) -> Option<VerificationReport>;
45}
46
47pub fn merge_attested_report(
52 attestor: Option<&Arc<dyn CompletionAttestor>>,
53 ledger: &MutationLedger,
54 reports: &mut Vec<VerificationReport>,
55) {
56 let Some(attestor) = attestor else {
57 return;
58 };
59 if ledger.is_empty() {
60 return;
61 }
62 let digest = ledger.digest();
63 if digest.trim().is_empty() {
64 return;
65 }
66 let paths: Vec<MutatedPathRecord> = ledger
67 .paths()
68 .map(|path| MutatedPathRecord {
69 path: path.to_string(),
70 content_digest: ledger
71 .content_digest_for_path(path)
72 .unwrap_or("")
73 .to_string(),
74 })
75 .collect();
76 let Some(report) = attestor.attest(digest, &paths) else {
77 return;
78 };
79 let Some(report) = accept_report(ReportAuthor::Host, report) else {
80 return;
81 };
82 reports.push(report);
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::harness_loop::{decide_with_observations, MutationLedger};
89 use crate::verification::{VerificationCheck, VerificationStatus};
90
91 struct BindingAttestor;
92
93 impl CompletionAttestor for BindingAttestor {
94 fn attest(
95 &self,
96 effect_digest: &str,
97 paths: &[MutatedPathRecord],
98 ) -> Option<VerificationReport> {
99 assert!(!paths.is_empty());
100 assert!(paths.iter().any(|p| !p.content_digest.is_empty()));
101 Some(
102 VerificationReport::new(
103 "host:attestor",
104 vec![VerificationCheck::required(
105 "host:effect",
106 "host_attestation",
107 "host reconciled mutated paths",
108 )
109 .with_status(VerificationStatus::Passed)],
110 )
111 .with_effect_digest(effect_digest.to_string()),
112 )
113 }
114 }
115
116 struct SilentAttestor;
117
118 impl CompletionAttestor for SilentAttestor {
119 fn attest(
120 &self,
121 _effect_digest: &str,
122 _paths: &[MutatedPathRecord],
123 ) -> Option<VerificationReport> {
124 None
125 }
126 }
127
128 fn ledger_with_write(path: &str) -> MutationLedger {
129 let mut ledger = MutationLedger::default();
130 ledger.observe_tool(
131 "write",
132 0,
133 Some(&serde_json::json!({"file_path": path, "after": "fn main() {}"})),
134 );
135 ledger
136 }
137
138 #[test]
139 fn attestor_binding_report_closes_gate() {
140 let ledger = ledger_with_write("out.txt");
141 let mut reports = Vec::new();
142 merge_attested_report(
143 Some(&(Arc::new(BindingAttestor) as Arc<dyn CompletionAttestor>)),
144 &ledger,
145 &mut reports,
146 );
147 assert_eq!(reports.len(), 1);
148 let gate = decide_with_observations(&ledger, &reports, &[], false, &[]);
149 assert!(matches!(
150 gate,
151 crate::harness_loop::CompletionGate::Allow(
152 crate::harness_loop::CompletionTerminal::Verified { .. }
153 )
154 ));
155 }
156
157 #[test]
158 fn silent_attestor_leaves_gate_incomplete() {
159 let ledger = ledger_with_write("out.txt");
160 let mut reports = Vec::new();
161 merge_attested_report(
162 Some(&(Arc::new(SilentAttestor) as Arc<dyn CompletionAttestor>)),
163 &ledger,
164 &mut reports,
165 );
166 assert!(reports.is_empty());
167 let gate = decide_with_observations(&ledger, &reports, &[], false, &[]);
168 assert!(matches!(
169 gate,
170 crate::harness_loop::CompletionGate::Incomplete { .. }
171 ));
172 }
173
174 #[test]
175 fn empty_ledger_skips_attestor() {
176 struct PanicAttestor;
177 impl CompletionAttestor for PanicAttestor {
178 fn attest(&self, _: &str, _: &[MutatedPathRecord]) -> Option<VerificationReport> {
179 panic!("must not run on empty ledger");
180 }
181 }
182 let mut reports = Vec::new();
183 merge_attested_report(
184 Some(&(Arc::new(PanicAttestor) as Arc<dyn CompletionAttestor>)),
185 &MutationLedger::default(),
186 &mut reports,
187 );
188 assert!(reports.is_empty());
189 }
190
191 #[test]
192 fn mismatched_digest_report_cannot_bypass_gate() {
193 struct WrongDigestAttestor;
194 impl CompletionAttestor for WrongDigestAttestor {
195 fn attest(
196 &self,
197 _effect_digest: &str,
198 paths: &[MutatedPathRecord],
199 ) -> Option<VerificationReport> {
200 assert!(!paths.is_empty());
201 Some(
202 VerificationReport::new(
203 "host:attestor",
204 vec![VerificationCheck::required(
205 "host:effect",
206 "host_attestation",
207 "wrong digest on purpose",
208 )
209 .with_status(VerificationStatus::Passed)],
210 )
211 .with_effect_digest("sha256:not-the-ledger-digest".to_string()),
212 )
213 }
214 }
215 let ledger = ledger_with_write("out.txt");
216 let mut reports = Vec::new();
217 merge_attested_report(
218 Some(&(Arc::new(WrongDigestAttestor) as Arc<dyn CompletionAttestor>)),
219 &ledger,
220 &mut reports,
221 );
222 assert_eq!(reports.len(), 1);
223 let gate = decide_with_observations(&ledger, &reports, &[], false, &[]);
224 assert!(matches!(
225 gate,
226 crate::harness_loop::CompletionGate::Incomplete { .. }
227 ));
228 }
229}