1use serde::{Deserialize, Serialize};
10
11use crate::evidence::EvidenceRecord;
12use crate::execution::FailureClass;
13use crate::ids::{ArtifactId, ExecutionPolicyId, PlanId};
14use crate::judgment::Unresolved;
15use crate::limits::RECORD_V1;
16use crate::optimization::{OptimizationOutcome, OptimizationRecord};
17use crate::remote::{MAX_TEXT_BYTES, RemoteCode, ServedIdentity};
18use crate::schema;
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
21#[serde(deny_unknown_fields)]
22pub struct RunRecord {
23 pub schema: String,
24 pub decision_id: String,
26 pub plan_id: PlanId,
27 pub execution: RecordedExecution,
28 pub termination: Termination,
29 pub core: CoreEvidence,
30 pub timing: Timing,
31 pub requests: Vec<RequestRecord>,
33 pub cost: CostSummary,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub remote_evidence: Option<Vec<RemoteEvidence>>,
39 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
41 pub remote_evidence_truncated: bool,
42 #[serde(
44 default,
45 skip_serializing_if = "Option::is_none",
46 deserialize_with = "crate::acquisition_record::list"
47 )]
48 #[schemars(
49 with = "Vec<crate::acquisition_record::AcquisitionRecord>",
50 length(min = 1, max = 4096)
51 )]
52 pub acquisitions: Option<Vec<crate::acquisition_record::AcquisitionRecord>>,
53}
54
55crate::document::versioned_document!(
59 RunRecord,
60 [schema::RUN, schema::RUN_V2, schema::RUN_V3],
61 "rustev.run/1 or rustev.run/2 or rustev.run/3",
62 RECORD_V1,
63 check = RunRecord::parsed_members_fit
64);
65
66pub const MAX_REMOTE_EVIDENCE: usize = 256;
68pub const MAX_REMOTE_EVIDENCE_ENTRY_BYTES: usize = 8 * 1024;
71pub const MAX_REMOTE_EVIDENCE_INSTANCE: usize = 8;
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum RunVersionError {
77 NotVersion2,
79 Members(&'static str),
82}
83
84impl std::fmt::Display for RunVersionError {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 RunVersionError::NotVersion2 => f.write_str("only rustev.run/2 projects to version 1"),
88 RunVersionError::Members(why) => write!(f, "run record members: {why}"),
89 }
90 }
91}
92
93impl std::error::Error for RunVersionError {}
94
95impl RunRecord {
96 pub fn check_version_members(&self) -> Result<(), RunVersionError> {
99 if self.schema != schema::RUN_V3
100 && self
101 .requests
102 .iter()
103 .any(|r| r.screen.is_some() || r.result == RequestResult::Withdrawn)
104 {
105 return Err(RunVersionError::Members("screening requires rustev.run/3"));
106 }
107 if let Some(pages) = &self.acquisitions {
108 if self.schema != schema::RUN_V3 {
109 return Err(RunVersionError::Members(
110 "acquisitions require rustev.run/3",
111 ));
112 }
113 if pages.iter().map(|p| p.attempts.len()).sum::<usize>() > 4096 {
114 return Err(RunVersionError::Members("too many acquisition attempts"));
115 }
116 }
117 let v2 = self.schema == schema::RUN_V2;
118 match &self.remote_evidence {
119 None if v2 => Err(RunVersionError::Members(
120 "rustev.run/2 needs remote_evidence",
121 )),
122 Some(_) if !v2 => Err(RunVersionError::Members(
123 "remote_evidence is a rustev.run/2 member",
124 )),
125 _ if self.remote_evidence_truncated && !v2 => Err(RunVersionError::Members(
126 "remote_evidence_truncated is a rustev.run/2 member",
127 )),
128 Some(list) if list.len() > MAX_REMOTE_EVIDENCE => {
129 Err(RunVersionError::Members("too many remote evidence entries"))
130 }
131 Some(list) if !list.iter().all(RemoteEvidence::is_bounded) => Err(
132 RunVersionError::Members("a remote evidence entry beyond its bounds"),
133 ),
134 _ => Ok(()),
135 }
136 }
137
138 fn parsed_members_fit(&self) -> Result<(), String> {
143 if let CoreEvidence::Recorded(e) = &self.core {
144 e.screening_version()?;
145 if self.schema != schema::RUN_V3
146 && (e.extraction.is_some()
147 || e.acceptance.is_some()
148 || e.inputs.iter().any(|i| i.producer.is_some())
149 || e.judgment.lineage.producers.is_some())
150 {
151 return Err("extraction evidence requires rustev.run/3".into());
152 }
153 }
154 if let CoreEvidence::Recorded(e) = &self.core
155 && e.generation.is_some()
156 && (self.schema != schema::RUN_V3 || e.schema != schema::EVIDENCE_V2)
157 {
158 return Err("generation evidence requires rustev.run/3 and rustev.evidence/2".into());
159 }
160 if let CoreEvidence::Recorded(e) = &self.core
161 && (self.schema != schema::RUN_V3 || e.schema != schema::EVIDENCE_V2)
162 && (e.acquisitions.is_some() || e.steps.iter().any(|s| s.constructed))
163 {
164 return Err("acquisition evidence requires rustev.evidence/2".into());
165 }
166
167 self.check_version_members().map_err(|e| e.to_string())
168 }
169
170 pub fn project_v1(&self) -> Result<RunRecord, RunVersionError> {
176 if self.schema != schema::RUN_V2 {
177 return Err(RunVersionError::NotVersion2);
178 }
179 self.check_version_members()?;
180 Ok(RunRecord {
181 schema: schema::RUN.into(),
182 remote_evidence: None,
183 remote_evidence_truncated: false,
184 ..self.clone()
185 })
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
194#[serde(deny_unknown_fields)]
195pub struct RemoteEvidence {
196 pub attempt_id: String,
197 pub step: String,
199 pub instance: Vec<String>,
200 pub exchange: ExchangeReference,
201 pub requested_identity: Option<String>,
203 pub served: ServedIdentity,
204 pub binding: ArtifactId,
207 pub protocol: String,
208 pub endpoint_class: String,
209 pub code: Option<RemoteCode>,
211 pub remote_state: RemoteState,
213 pub charge_provenance: ChargeProvenance,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub batch: Option<BatchMembership>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub principal_digest: Option<String>,
220}
221
222impl RemoteEvidence {
223 pub fn is_bounded(&self) -> bool {
227 self.strings_within(MAX_TEXT_BYTES)
228 && crate::canonical::canonical_bytes(self)
229 .is_ok_and(|b| b.len() <= MAX_REMOTE_EVIDENCE_ENTRY_BYTES)
230 }
231
232 fn strings_within(&self, max: usize) -> bool {
233 let t = |s: &str| s.len() <= max;
234 let o = |s: &Option<String>| s.as_deref().is_none_or(t);
235 t(&self.attempt_id)
236 && t(&self.step)
237 && self.instance.len() <= MAX_REMOTE_EVIDENCE_INSTANCE
238 && self.instance.iter().all(|i| t(i))
239 && self.exchange.is_bounded(max)
240 && o(&self.requested_identity)
241 && o(&self.served.identity)
242 && t(&self.protocol)
243 && t(&self.endpoint_class)
244 && self.batch.as_ref().is_none_or(|b| t(&b.batch_id))
245 && o(&self.principal_digest)
246 }
247
248 pub fn bounded(self) -> Self {
252 let e = self.cut(MAX_TEXT_BYTES);
253 if e.is_bounded() { e } else { e.cut(32) }
254 }
255
256 fn cut(mut self, max: usize) -> Self {
257 let c = |s: &mut String| {
258 if s.len() > max {
259 *s = crate::remote::cut_to(s, max);
260 }
261 };
262 c(&mut self.attempt_id);
263 c(&mut self.step);
264 self.instance.truncate(MAX_REMOTE_EVIDENCE_INSTANCE);
265 self.instance.iter_mut().for_each(c);
266 if let ExchangeReference::Retained {
267 digest,
268 schema,
269 locator,
270 ..
271 } = &mut self.exchange
272 {
273 c(digest);
274 c(schema);
275 c(locator);
276 }
277 if let Some(r) = &mut self.requested_identity {
278 c(r);
279 }
280 if let Some(s) = &mut self.served.identity {
281 c(s);
282 }
283 c(&mut self.protocol);
284 c(&mut self.endpoint_class);
285 if let Some(b) = &mut self.batch {
286 c(&mut b.batch_id);
287 }
288 if let Some(p) = &mut self.principal_digest {
289 c(p);
290 }
291 self
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
297#[serde(rename_all = "snake_case", deny_unknown_fields)]
298pub enum ExchangeReference {
299 Retained {
300 digest: String,
302 schema: String,
304 retention: RetentionClass,
305 locator: String,
307 },
308 Unavailable {
309 reason: EvidenceUnavailable,
310 },
311}
312
313impl ExchangeReference {
314 fn is_bounded(&self, max: usize) -> bool {
315 match self {
316 ExchangeReference::Retained {
317 digest,
318 schema,
319 locator,
320 ..
321 } => [digest, schema, locator].iter().all(|s| s.len() <= max),
322 ExchangeReference::Unavailable { .. } => true,
323 }
324 }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
329#[serde(rename_all = "snake_case")]
330pub enum RetentionClass {
331 DigestOnly,
333 BytesRetained,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
340#[serde(rename_all = "snake_case")]
341pub enum EvidenceUnavailable {
342 NoExchange,
344 Pending,
346 NotDelivered,
348 NoLocator,
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
354#[serde(rename_all = "snake_case")]
355pub enum ChargeProvenance {
356 Reported,
358 Attributed,
360 NothingSent,
362 Unknown,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
368#[serde(deny_unknown_fields)]
369pub struct BatchMembership {
370 pub batch_id: String,
371 pub index: u32,
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
375#[serde(rename_all = "snake_case", deny_unknown_fields)]
376pub enum RecordedExecution {
377 None,
378 Declared(ExecutionPolicyId),
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
382#[serde(rename_all = "snake_case")]
383pub enum Termination {
384 Judged,
386 Cancelled,
388}
389
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
391#[serde(rename_all = "snake_case", deny_unknown_fields)]
392pub enum CoreEvidence {
393 Recorded(Box<EvidenceRecord>),
395 NotProduced,
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
400#[serde(deny_unknown_fields)]
401pub struct Timing {
402 pub queued_ms: u64,
403 pub elapsed_ms: u64,
404 pub deadline_ms: u64,
405 pub deadline_expired: bool,
406}
407
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
409#[serde(deny_unknown_fields)]
410pub struct RequestRecord {
411 pub step: String,
412 pub instance: Vec<String>,
413 pub result: RequestResult,
414 pub attempts: Vec<AttemptRecord>,
415 pub transitions: Vec<Transition>,
416 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub optimization: Option<OptimizationRecord>,
419 #[serde(
421 default,
422 skip_serializing_if = "Option::is_none",
423 deserialize_with = "crate::screening::present"
424 )]
425 #[schemars(with = "crate::screening::ScreenRequest")]
426 pub screen: Option<crate::screening::ScreenRequest>,
427}
428
429#[derive(Debug, Clone, Copy, PartialEq)]
431pub struct SuppliedFrom<'r> {
432 pub target: u32,
435 pub attempt: Option<&'r AttemptRecord>,
439 pub reused_from: Option<&'r str>,
443}
444
445impl RequestRecord {
446 pub fn supplied_from(&self) -> Option<SuppliedFrom<'_>> {
449 match &self.result {
450 RequestResult::NotSupplied | RequestResult::Withdrawn => None,
451 RequestResult::Output { target } => Some(SuppliedFrom {
452 target: *target,
453 attempt: self.attempts.last(),
454 reused_from: self
455 .optimization
456 .as_ref()
457 .filter(|o| self.attempts.is_empty() && o.outcome == OptimizationOutcome::Hit)
458 .and_then(|o| o.source_attempt_id.as_deref()),
459 }),
460 RequestResult::Failed(_) => {
461 let target = self
462 .transitions
463 .iter()
464 .rev()
465 .find_map(|t| match t {
466 Transition::Fallback { to, .. } => Some(*to),
467 _ => None,
468 })
469 .unwrap_or(0);
470 let n = self.attempts.len() as u32;
471 let moved_on = self.transitions.iter().any(|t| {
472 matches!(t, Transition::Retry { after, .. } | Transition::Fallback { after, .. } if *after == n)
473 });
474 Some(SuppliedFrom {
475 target,
476 attempt: if moved_on { None } else { self.attempts.last() },
477 reused_from: None,
478 })
479 }
480 }
481 }
482}
483
484#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
485#[serde(rename_all = "snake_case", deny_unknown_fields)]
486pub enum RequestResult {
487 Output { target: u32 },
489 Failed(Unresolved),
491 NotSupplied,
493 Withdrawn,
495}
496
497#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
498#[serde(deny_unknown_fields)]
499pub struct AttemptRecord {
500 pub attempt_id: String,
503 pub target: u32,
504 pub backend_id: String,
505 pub artifact: ArtifactId,
506 pub dispatched_ms: u64,
507 pub ended_ms: u64,
508 pub end: AttemptEnd,
509 pub cancellation: Cancellation,
510 pub remote: RemoteState,
511 pub cost: AttemptCost,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
515#[serde(rename_all = "snake_case", deny_unknown_fields)]
516pub enum AttemptEnd {
517 Output,
519 Failed {
520 class: FailureClass,
521 detail: String,
522 },
523 Deadline,
525 Cancelled,
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
531#[serde(rename_all = "snake_case", deny_unknown_fields)]
532pub enum Cancellation {
533 NotRequested,
534 Requested { answer: CancelAnswer },
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
538#[serde(rename_all = "snake_case")]
539pub enum CancelAnswer {
540 Stopped,
542 Unconfirmed,
544 NotObserved,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
549#[serde(rename_all = "snake_case")]
550pub enum RemoteState {
551 Finished,
553 Stopped,
555 PossiblyContinuing,
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
560#[serde(deny_unknown_fields)]
561pub struct AttemptCost {
562 pub bound: CostBound,
563 pub reserved: u64,
564 pub charge: Charge,
565}
566
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
569#[serde(rename_all = "snake_case")]
570pub enum CostModel {
571 Bounded,
573 Estimated,
575 Unknown,
576}
577
578#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
580#[serde(rename_all = "snake_case", deny_unknown_fields)]
581pub enum CostBound {
582 Bounded {
584 max_units: u64,
585 },
586 Estimated {
587 units: u64,
588 },
589 Unknown,
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
594#[serde(rename_all = "snake_case", deny_unknown_fields)]
595pub enum Charge {
596 Observed { units: u64 },
597 Estimated { units: u64 },
598 Unknown,
599}
600
601#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
602#[serde(rename_all = "snake_case", deny_unknown_fields)]
603pub enum Transition {
604 Retry {
606 after: u32,
607 class: FailureClass,
608 delay_ms: u64,
609 },
610 Fallback {
612 after: u32,
613 class: FailureClass,
614 to: u32,
615 },
616 Stop { after: u32, reason: String },
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
621#[serde(rename_all = "snake_case")]
622pub enum CostMode {
623 Unlimited,
624 Hard,
625 Estimated,
626}
627
628#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
630#[serde(deny_unknown_fields)]
631pub struct CostSummary {
632 pub mode: CostMode,
633 pub limit: u64,
635 pub observed: u64,
636 pub estimated: u64,
637 pub liability: u64,
639 pub bound_violations: u64,
641 pub final_cost: FinalCost,
642 pub within_guaranteed_cap: bool,
644}
645
646#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
647#[serde(rename_all = "snake_case")]
648pub enum FinalCost {
649 Known,
650 IncludesEstimates,
651 Unknown,
652}