Skip to main content

artifactize_core/
remote.rs

1//! Shared remote review store records: what leaves this machine and how it is mirrored back.
2
3mod 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
15/// Record schema version: 2 since records carry the 0.5 reuse key.
16pub const SCHEMA: u32 = 2;
17/// Bound network/storage cost of reduced records, which omit large captured execution output.
18pub const MAX_SUMMARY_BYTES: usize = 256 * 1024;
19/// Full records must fit the same per-entry budget as the local reusable cache.
20pub const MAX_FULL_BYTES: usize = crate::store::history::MAX_ENTRY_BYTES;
21
22/// One record of a reuse key's history; the store keeps every record and returns the latest.
23///
24/// A summary carries no argv, captured output or repository path. A full record additionally
25/// carries the saved `execution` as is; one an earlier artifactize published may still hold
26/// its `toolCalls`, which readers ignore. The server stamps `publisher`
27/// and `publishedAt`.
28#[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    /// Each Artifact the key covers, with its fingerprint.
35    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    /// An Agent result's tool `executionPaths` pins.
47    #[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    /// Project a completed local GREEN/RED result with a key; `full` keeps the execution.
67    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    /// Check shape, the key against its fingerprints, and size limits; never trusts the
116    /// record's verdict.
117    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    /// Rebuild a self-contained execution for the local cache, marked with its origin.
165    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                // The target is the eval id's Artifact part.
177                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    // SHA-256's published wire width is independent of the implementation's limit.
262    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
275/// A producer's session reference, if any, names its conversation in bounded, printable
276/// fields. It is a pointer back to the producing machine; no conversation is ever published.
277fn 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}