1mod push;
4mod session;
5pub use push::{Push, push};
6pub(crate) use session::REFRESH as REFRESH_INTERVAL;
7pub use session::Session;
8
9use std::{collections::BTreeMap, path::PathBuf};
10
11use serde::{Deserialize, Serialize};
12
13use crate::store::{Execution, ExecutionOptions, Origin, Producer, Provenance};
14
15pub const SCHEMA: u32 = 2;
17pub const MAX_SUMMARY_BYTES: usize = 256 * 1024;
19pub const MAX_FULL_BYTES: usize = crate::store::history::MAX_ENTRY_BYTES;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase", deny_unknown_fields)]
30pub struct Record {
31 pub schema: u32,
32 pub key: crate::types::ReuseKey,
33 pub eval_def_hash: crate::types::DefinitionHash,
34 pub fingerprints: BTreeMap<crate::types::ArtifactName, crate::types::Fingerprint>,
36 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
37 pub artifact_kinds: BTreeMap<crate::types::ArtifactName, crate::config::ArtifactKind>,
38 pub verdict: crate::types::ExecutionStatus,
39 pub eval_id: crate::types::EvalId,
40 pub run_id: crate::types::RunId,
41 pub request_id: crate::types::RequestId,
42 pub execution_id: crate::types::ExecutionId,
43 pub profile: crate::config::StoredProfile,
44 #[serde(default)]
45 pub options: ExecutionOptions,
46 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
48 pub execution_paths: crate::tools::pins::Pins,
49 pub result: crate::store::ExecutionResult,
50 pub usage: Option<Vec<crate::llm::Attempt>>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub reviewer: Option<crate::types::ReviewerId>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub producer: Option<Producer>,
55 pub started_at: crate::types::Timestamp,
56 pub completed_at: crate::types::Timestamp,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub execution: Option<Box<Execution>>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub publisher: Option<crate::types::TokenName>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub published_at: Option<crate::types::Timestamp>,
63}
64
65impl Record {
66 pub fn new(execution: &Execution, full: bool) -> Result<Self, String> {
68 let (Some(key), Some(_), Some(completed_at), Some(result)) = (
69 &execution.key,
70 execution.verdict(),
71 &execution.completed_at(),
72 execution.result(),
73 ) else {
74 return Err("Only completed GREEN/RED results with a reuse key are shared.".into());
75 };
76 if execution.origin.is_some() {
77 return Err("Mirrored results are never published again.".into());
78 }
79 let (result, usage) = if full {
80 (result.clone(), execution.usage.clone())
81 } else {
82 (
83 summary_result(&execution.profile, result),
84 summary_usage(execution.usage.as_ref()),
85 )
86 };
87 let record = Self {
88 schema: SCHEMA,
89 key: key.clone(),
90 eval_def_hash: execution.eval_def_hash.clone(),
91 fingerprints: execution.fingerprints.clone(),
92 artifact_kinds: execution.artifact_kinds.clone(),
93 verdict: execution.status(),
94 eval_id: execution.provenance.eval_id.clone(),
95 run_id: execution.provenance.run_id.clone(),
96 request_id: execution.provenance.request_id.clone(),
97 execution_id: execution.id.clone(),
98 profile: execution.profile.clone(),
99 options: execution.options.clone(),
100 execution_paths: execution.provenance.execution_paths.clone(),
101 result,
102 usage,
103 reviewer: execution.reviewer.clone(),
104 producer: execution.producer.clone(),
105 started_at: execution.started_at,
106 completed_at: *completed_at,
107 execution: full.then(|| Box::new(execution.clone())),
108 publisher: None,
109 published_at: None,
110 };
111 record.validate()?;
112 Ok(record)
113 }
114
115 pub fn validate(&self) -> Result<(), String> {
118 let valid = self.schema == SCHEMA
119 && valid_hash(&self.key)
120 && valid_hash(&self.eval_def_hash)
121 && !self.fingerprints.is_empty()
122 && self.fingerprints.iter().all(|(name, fingerprint)| {
123 crate::config::identifier(name, "Artifact name").is_ok()
124 && valid_fingerprint(fingerprint)
125 })
126 && (self.artifact_kinds.is_empty()
127 || self.artifact_kinds.keys().eq(self.fingerprints.keys()))
128 && crate::cache::key_with_kinds(
129 &self.eval_def_hash,
130 &self.fingerprints,
131 &self.artifact_kinds,
132 ) == self.key
133 && matches!(
134 self.verdict,
135 crate::types::ExecutionStatus::Green | crate::types::ExecutionStatus::Red
136 )
137 && self.result.verdict().as_str() == self.verdict.as_str()
138 && self.execution_id.valid_wire()
139 && valid_session(self.producer.as_ref())
140 && self.execution.as_ref().is_none_or(|execution| {
141 valid_session(execution.producer.as_ref())
142 && execution.key.as_ref() == Some(&self.key)
143 && execution.eval_def_hash == self.eval_def_hash
144 && execution.fingerprints == self.fingerprints
145 && execution.artifact_kinds == self.artifact_kinds
146 && execution.status() == self.verdict
147 && execution.profile == self.profile
148 && execution.origin.is_none()
149 });
150 if !valid {
151 return Err("Invalid remote review record.".into());
152 }
153 let limit = if self.execution.is_some() {
154 MAX_FULL_BYTES
155 } else {
156 MAX_SUMMARY_BYTES
157 };
158 if serde_json::to_vec(self).map_err(|e| e.to_string())?.len() > limit {
159 return Err(format!("Remote review record exceeds {limit} bytes."));
160 }
161 Ok(())
162 }
163
164 pub fn mirror(self, store: &str) -> Result<Execution, String> {
166 self.validate()?;
167 let (Some(publisher), Some(published_at)) = (self.publisher, self.published_at) else {
168 return Err("Remote review record has no server publisher.".into());
169 };
170 let id = self.execution_id.remote_mirror()?;
171 let mut execution = match self.execution {
172 Some(execution) => *execution,
173 None => Execution {
174 id: id.clone(),
175 key: Some(self.key),
176 fingerprint: self
178 .eval_id
179 .rsplit_once('/')
180 .and_then(|(target, _)| self.fingerprints.get(target))
181 .cloned(),
182 fingerprints: self.fingerprints,
183 artifact_kinds: self.artifact_kinds,
184 eval_def_hash: self.eval_def_hash.clone(),
185 owner: None,
186 state: crate::store::ExecutionState::Completed {
187 result: self.result,
188 at: self.completed_at,
189 },
190 profile: self.profile,
191 options: self.options,
192 usage: self.usage,
193 provenance: Provenance {
194 repo_path: PathBuf::new(),
195 run_id: self.run_id,
196 request_id: self.request_id,
197 eval_id: self.eval_id,
198 eval_def_hash: self.eval_def_hash,
199 completed_at: Some(self.completed_at),
200 execution_paths: self.execution_paths,
201 },
202 started_at: self.started_at,
203 producer: self.producer,
204 reviewer: self.reviewer,
205 origin: None,
206 manifest: None,
207 },
208 };
209 execution.id = id;
210 execution.origin = Some(Origin {
211 store: store.parse()?,
212 publisher,
213 published_at,
214 });
215 Ok(execution)
216 }
217}
218
219fn summary_result(
220 profile: &crate::config::StoredProfile,
221 result: &crate::store::ExecutionResult,
222) -> crate::store::ExecutionResult {
223 if profile.kind() == crate::config::ProfileKind::Runtime
224 && let crate::store::ExecutionResult::Runtime(runtime) = result
225 {
226 return crate::store::ExecutionResult::Runtime(runtime.summary());
227 }
228 result.clone()
229}
230
231fn summary_usage(usage: Option<&Vec<crate::llm::Attempt>>) -> Option<Vec<crate::llm::Attempt>> {
232 let attempts = usage?;
233 Some(
234 attempts
235 .iter()
236 .map(|attempt| crate::llm::Attempt {
237 turn: attempt.turn,
238 attempt: attempt.attempt,
239 usage: attempt.usage.clone(),
240 error: None,
241 error_code: None,
242 })
243 .collect(),
244 )
245}
246
247pub(crate) fn valid_fingerprint(value: &str) -> bool {
248 value.parse::<crate::types::Fingerprint>().is_ok()
249}
250
251pub(crate) fn valid_hash(value: &str) -> bool {
252 value.len() == crate::types::SHA256_HEX_BYTES
253 && value
254 .bytes()
255 .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
256}
257
258#[cfg(test)]
259#[test]
260fn sha256_hash_wire_format_keeps_exact_width_and_lowercase() {
261 assert!(valid_hash(
263 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
264 ));
265 for hash in [
266 "a".repeat(63),
267 "a".repeat(65),
268 "A".repeat(64),
269 "g".repeat(64),
270 ] {
271 assert!(!valid_hash(&hash), "{hash}");
272 }
273}
274
275fn valid_session(producer: Option<&Producer>) -> bool {
278 producer
279 .and_then(|producer| producer.session.as_ref())
280 .is_none_or(crate::agent::session::SessionRef::valid)
281}