Skip to main content

chio_kernel/tool_outcome/
post_return.rs

1//! Frozen post-return evaluation journal and recovery contract.
2
3use super::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum EvaluationPhaseV1 {
8    OutputGuard,
9    Pricing,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
14pub enum EvaluationModeV1 {
15    Pure,
16    ExternalStateful { call_id: AdmissionIdentifier },
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct FrozenEvaluationStepV1 {
22    pub phase: EvaluationPhaseV1,
23    pub position: u32,
24    pub component_id: AdmissionIdentifier,
25    pub component_version: AdmissionIdentifier,
26    pub implementation_digest: AdmissionDigest,
27    pub mode: EvaluationModeV1,
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(deny_unknown_fields)]
32pub struct PostReturnNormalizedRequestContextV1 {
33    normalized: Value,
34    digest: AdmissionDigest,
35}
36
37impl PostReturnNormalizedRequestContextV1 {
38    #[allow(dead_code)]
39    pub(crate) fn from_verified_normalization(normalized: Value) -> Result<Self, ToolOutcomeError> {
40        let bytes = bounded(
41            "normalized_request_context",
42            &normalized,
43            MAX_FROZEN_INPUT_BYTES,
44        )?;
45        Ok(Self {
46            normalized,
47            digest: digest_bytes("normalized_request_context.digest", &bytes)?,
48        })
49    }
50
51    fn validate(&self) -> Result<(), ToolOutcomeError> {
52        let bytes = bounded(
53            "normalized_request_context",
54            &self.normalized,
55            MAX_FROZEN_INPUT_BYTES,
56        )?;
57        if digest_bytes("normalized_request_context.digest", &bytes)? != self.digest {
58            return Err(ToolOutcomeError::Binding(
59                "normalized_request_context.digest",
60            ));
61        }
62        Ok(())
63    }
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68pub struct PostReturnExactInputsV1 {
69    schema: String,
70    operation_id: AdmissionOperationId,
71    pub(super) operation_version: u64,
72    request_id: AdmissionIdentifier,
73    request_binding_hash: AdmissionDigest,
74    capability_id: AdmissionIdentifier,
75    policy_hash: AdmissionDigest,
76    dispatch_commit: AdmissionDispatchCommitBindingV1,
77    outcome_recording_fence: StoreMutationFence,
78    outcome_recorded_at_unix_ms: u64,
79    tool_outcome_id: AdmissionDigest,
80    tool_outcome_version: u64,
81    raw_output_digest: AdmissionDigest,
82    tool_server: AdmissionIdentifier,
83    tool_name: AdmissionIdentifier,
84    provider_attempt: ProviderAttemptBindingV1,
85    transport_terminal_evidence_digest: AdmissionDigest,
86    reported_cost: Option<MonetaryAmount>,
87    normalized_request_context: PostReturnNormalizedRequestContextV1,
88    trusted_time_unix_ms: u64,
89}
90
91impl PostReturnExactInputsV1 {
92    #[allow(dead_code)]
93    fn from_records(
94        operation: &AdmissionOperationV1,
95        outcome: &ToolOutcomeRecordV1,
96        normalized_request_context: PostReturnNormalizedRequestContextV1,
97        trusted_time_unix_ms: u64,
98    ) -> Result<Self, ToolOutcomeError> {
99        validate_committed_operation(
100            operation,
101            operation
102                .dispatch_commit()
103                .ok_or(ToolOutcomeError::Binding("exact_inputs.dispatch_commit"))?,
104        )?;
105        outcome.validate_against(operation)?;
106        let exact = Self {
107            schema: POST_RETURN_EXACT_INPUTS_SCHEMA.to_owned(),
108            operation_id: operation.binding().operation_id().clone(),
109            operation_version: operation.version(),
110            request_id: operation.replay_key().request_id,
111            request_binding_hash: operation.binding().request_binding_hash().clone(),
112            capability_id: operation.binding().capability_id().clone(),
113            policy_hash: operation.binding().policy_hash().clone(),
114            dispatch_commit: outcome.dispatch_commit.clone(),
115            outcome_recording_fence: outcome.recording_fence.clone(),
116            outcome_recorded_at_unix_ms: outcome.recorded_at_unix_ms,
117            tool_outcome_id: outcome.outcome_id.clone(),
118            tool_outcome_version: outcome.version,
119            raw_output_digest: outcome.raw_output.digest().clone(),
120            tool_server: outcome.tool_server.clone(),
121            tool_name: outcome.tool_name.clone(),
122            provider_attempt: outcome.provider_attempt.clone(),
123            transport_terminal_evidence_digest: outcome.transport_terminal_evidence_digest.clone(),
124            reported_cost: outcome.reported_cost.clone(),
125            normalized_request_context,
126            trusted_time_unix_ms,
127        };
128        exact.validate_against(operation, outcome)?;
129        Ok(exact)
130    }
131
132    fn validate(&self) -> Result<(), ToolOutcomeError> {
133        if self.schema != POST_RETURN_EXACT_INPUTS_SCHEMA {
134            return Err(ToolOutcomeError::Invalid("exact_inputs.schema"));
135        }
136        positive(
137            "exact_inputs.tool_outcome_version",
138            self.tool_outcome_version,
139        )?;
140        positive("exact_inputs.operation_version", self.operation_version)?;
141        positive("exact_inputs.trusted_time", self.trusted_time_unix_ms)?;
142        positive(
143            "exact_inputs.dispatch_commit.committed_version",
144            self.dispatch_commit.committed_version,
145        )?;
146        positive(
147            "exact_inputs.dispatch_commit.coordinator_lease_epoch",
148            self.dispatch_commit.coordinator_lease_epoch,
149        )?;
150        validate_store_fence(&self.dispatch_commit.store_fence)?;
151        validate_successor_fence(
152            &self.dispatch_commit.store_fence,
153            &self.outcome_recording_fence,
154        )?;
155        positive(
156            "exact_inputs.outcome_recorded_at",
157            self.outcome_recorded_at_unix_ms,
158        )?;
159        if self.operation_version < self.dispatch_commit.committed_version {
160            return Err(ToolOutcomeError::Binding("exact_inputs.operation_version"));
161        }
162        if self.trusted_time_unix_ms < self.outcome_recorded_at_unix_ms {
163            return Err(ToolOutcomeError::Invalid("exact_inputs.trusted_time"));
164        }
165        if let Some(cost) = &self.reported_cost {
166            amount(cost)?;
167        }
168        self.normalized_request_context.validate()
169    }
170
171    #[allow(dead_code)]
172    fn validate_against(
173        &self,
174        operation: &AdmissionOperationV1,
175        outcome: &ToolOutcomeRecordV1,
176    ) -> Result<(), ToolOutcomeError> {
177        self.validate()?;
178        outcome.validate_against(operation)?;
179        let commit = operation
180            .dispatch_commit()
181            .ok_or(ToolOutcomeError::Binding("exact_inputs.dispatch_commit"))?;
182        let recorded_return_version = recorded_return_version(outcome)?;
183        if self.operation_id != *operation.binding().operation_id()
184            || self.operation_version > operation.version()
185            || self.request_id != operation.replay_key().request_id
186            || self.request_binding_hash != *operation.binding().request_binding_hash()
187            || self.capability_id != *operation.binding().capability_id()
188            || self.policy_hash != *operation.binding().policy_hash()
189            || self.dispatch_commit != *commit
190            || self.outcome_recording_fence != outcome.recording_fence
191            || self.outcome_recorded_at_unix_ms != outcome.recorded_at_unix_ms
192            || self.tool_outcome_id != outcome.outcome_id
193            || self.tool_outcome_version != recorded_return_version
194            || self.raw_output_digest != *outcome.raw_output.digest()
195            || self.tool_server != outcome.tool_server
196            || self.tool_name != outcome.tool_name
197            || self.provider_attempt != outcome.provider_attempt
198            || self.transport_terminal_evidence_digest != outcome.transport_terminal_evidence_digest
199            || self.reported_cost != outcome.reported_cost
200        {
201            return Err(ToolOutcomeError::Binding("exact_inputs.records"));
202        }
203        Ok(())
204    }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(deny_unknown_fields)]
209pub struct ExternalEvaluationResultRefV1 {
210    pub step_index: u32,
211    pub call_id: AdmissionIdentifier,
212    pub result_digest: AdmissionDigest,
213    pub result_blob_ref: ContentAddressedBlobRefV1,
214    pub verifier_identity: AdmissionIdentifier,
215    pub verifier_key_epoch: u64,
216    pub authenticated_at_unix_ms: u64,
217}
218
219impl ExternalEvaluationResultRefV1 {
220    #[allow(dead_code)]
221    pub(crate) fn new(
222        step_index: u32,
223        step: &FrozenEvaluationStepV1,
224        result_digest: AdmissionDigest,
225        verifier_identity: AdmissionIdentifier,
226        verifier_key_epoch: u64,
227        authenticated_at_unix_ms: u64,
228    ) -> Result<Self, ToolOutcomeError> {
229        let EvaluationModeV1::ExternalStateful { call_id } = &step.mode else {
230            return Err(ToolOutcomeError::Invalid("external_result.pure_step"));
231        };
232        positive("external_result.verifier_key_epoch", verifier_key_epoch)?;
233        positive("external_result.authenticated_at", authenticated_at_unix_ms)?;
234        Ok(Self {
235            step_index,
236            call_id: call_id.clone(),
237            result_blob_ref: ContentAddressedBlobRefV1::new(result_digest.clone()),
238            result_digest,
239            verifier_identity,
240            verifier_key_epoch,
241            authenticated_at_unix_ms,
242        })
243    }
244
245    fn validate_for(
246        &self,
247        index: usize,
248        step: &FrozenEvaluationStepV1,
249        evaluation_trusted_time_unix_ms: u64,
250    ) -> Result<(), ToolOutcomeError> {
251        let EvaluationModeV1::ExternalStateful { call_id } = &step.mode else {
252            return Err(ToolOutcomeError::Binding("external_result.step_mode"));
253        };
254        if usize::try_from(self.step_index).ok() != Some(index) || self.call_id != *call_id {
255            return Err(ToolOutcomeError::Binding("external_result.step"));
256        }
257        self.result_blob_ref.validate()?;
258        if self.result_blob_ref.digest() != &self.result_digest {
259            return Err(ToolOutcomeError::Binding("external_result.blob_ref"));
260        }
261        positive(
262            "external_result.verifier_key_epoch",
263            self.verifier_key_epoch,
264        )?;
265        positive(
266            "external_result.authenticated_at",
267            self.authenticated_at_unix_ms,
268        )?;
269        if self.authenticated_at_unix_ms < evaluation_trusted_time_unix_ms {
270            return Err(ToolOutcomeError::Binding(
271                "external_result.evaluation_trusted_time",
272            ));
273        }
274        Ok(())
275    }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(deny_unknown_fields)]
280pub struct EvaluationStepResultV1 {
281    pub(super) step_index: u32,
282    pub(super) input_dependency_digest: AdmissionDigest,
283    pub(super) result_digest: AdmissionDigest,
284    pub(super) external_result: Option<ExternalEvaluationResultRefV1>,
285}
286
287impl EvaluationStepResultV1 {
288    #[allow(dead_code)]
289    pub(crate) fn pure(
290        step_index: u32,
291        input_dependency_digest: AdmissionDigest,
292        result_digest: AdmissionDigest,
293    ) -> Self {
294        Self {
295            step_index,
296            input_dependency_digest,
297            result_digest,
298            external_result: None,
299        }
300    }
301
302    #[allow(dead_code)]
303    pub(crate) fn external(
304        input_dependency_digest: AdmissionDigest,
305        external_result: ExternalEvaluationResultRefV1,
306    ) -> Self {
307        Self {
308            step_index: external_result.step_index,
309            input_dependency_digest,
310            result_digest: external_result.result_digest.clone(),
311            external_result: Some(external_result),
312        }
313    }
314
315    fn validate_for(
316        &self,
317        index: usize,
318        step: &FrozenEvaluationStepV1,
319        expected_dependency: &AdmissionDigest,
320        evaluation_trusted_time_unix_ms: u64,
321    ) -> Result<(), ToolOutcomeError> {
322        if usize::try_from(self.step_index).ok() != Some(index)
323            || &self.input_dependency_digest != expected_dependency
324        {
325            return Err(ToolOutcomeError::Binding("step_result.order_or_dependency"));
326        }
327        match (&step.mode, &self.external_result) {
328            (EvaluationModeV1::Pure, None) => Ok(()),
329            (EvaluationModeV1::ExternalStateful { .. }, Some(result)) => {
330                result.validate_for(index, step, evaluation_trusted_time_unix_ms)?;
331                if result.result_digest != self.result_digest {
332                    return Err(ToolOutcomeError::Binding("step_result.external_result"));
333                }
334                Ok(())
335            }
336            _ => Err(ToolOutcomeError::Binding("step_result.evidence_kind")),
337        }
338    }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342#[serde(tag = "reason", rename_all = "snake_case", deny_unknown_fields)]
343pub enum EvaluationFreezeV1 {
344    AmbiguousExternalResult {
345        step_index: u32,
346        evidence_digest: AdmissionDigest,
347    },
348    ExternalResultConflict {
349        step_index: u32,
350        evidence_digest: AdmissionDigest,
351    },
352    AuthenticatedResultUnavailable {
353        step_index: u32,
354        evidence_digest: AdmissionDigest,
355    },
356}
357
358impl EvaluationFreezeV1 {
359    fn step_index(&self) -> u32 {
360        match self {
361            Self::AmbiguousExternalResult { step_index, .. }
362            | Self::ExternalResultConflict { step_index, .. }
363            | Self::AuthenticatedResultUnavailable { step_index, .. } => *step_index,
364        }
365    }
366
367    #[allow(dead_code)]
368    fn evidence_digest(&self) -> &AdmissionDigest {
369        match self {
370            Self::AmbiguousExternalResult {
371                evidence_digest, ..
372            }
373            | Self::ExternalResultConflict {
374                evidence_digest, ..
375            }
376            | Self::AuthenticatedResultUnavailable {
377                evidence_digest, ..
378            } => evidence_digest,
379        }
380    }
381
382    fn validate(&self, step_count: usize) -> Result<(), ToolOutcomeError> {
383        if usize::try_from(self.step_index()).map_or(true, |index| index >= step_count) {
384            return Err(ToolOutcomeError::Invalid("evaluation_freeze.step_index"));
385        }
386        Ok(())
387    }
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(deny_unknown_fields)]
392pub struct PostReturnResolutionV1 {
393    pub(super) resolved_output: ContentAddressedBlobRefV1,
394    pub(super) resolved_output_size_bytes: u64,
395    pub(super) terminal_dependency_root_digest: AdmissionDigest,
396    pub(super) post_guard_decision_digest: AdmissionDigest,
397    pub(super) pricing_verdict_digest: AdmissionDigest,
398    pub(super) settlement_disposition: SettlementDispositionV1,
399}
400
401impl PostReturnResolutionV1 {
402    #[allow(dead_code)]
403    pub(crate) fn from_output(
404        evaluation: &PostReturnEvaluationRecordV1,
405        output: &Value,
406        post_guard_decision_digest: AdmissionDigest,
407        pricing_verdict_digest: AdmissionDigest,
408        settlement_disposition: SettlementDispositionV1,
409    ) -> Result<Self, ToolOutcomeError> {
410        Self::from_output_bounded(
411            evaluation,
412            output,
413            post_guard_decision_digest,
414            pricing_verdict_digest,
415            settlement_disposition,
416            MAX_RESOLVED_OUTPUT_BYTES,
417        )
418    }
419
420    #[allow(dead_code)]
421    fn from_output_bounded(
422        evaluation: &PostReturnEvaluationRecordV1,
423        output: &Value,
424        post_guard_decision_digest: AdmissionDigest,
425        pricing_verdict_digest: AdmissionDigest,
426        settlement_disposition: SettlementDispositionV1,
427        maximum: usize,
428    ) -> Result<Self, ToolOutcomeError> {
429        let bytes = bounded("resolved_output", output, maximum)?;
430        Self::from_signing_preimage(
431            evaluation,
432            bytes,
433            post_guard_decision_digest,
434            pricing_verdict_digest,
435            settlement_disposition,
436        )
437        .map(|(resolution, _)| resolution)
438    }
439
440    pub(crate) fn from_signing_preimage(
441        evaluation: &PostReturnEvaluationRecordV1,
442        signing_preimage: Vec<u8>,
443        post_guard_decision_digest: AdmissionDigest,
444        pricing_verdict_digest: AdmissionDigest,
445        settlement_disposition: SettlementDispositionV1,
446    ) -> Result<(Self, CanonicalResolvedOutputBlobV1), ToolOutcomeError> {
447        evaluation.validate()?;
448        if !matches!(evaluation.state, PostReturnEvaluationStateV1::Evaluating)
449            || evaluation.step_results.len() != evaluation.frozen_steps.len()
450        {
451            return Err(ToolOutcomeError::Invalid(
452                "resolution.incomplete_evaluation",
453            ));
454        }
455        settlement_disposition.validate()?;
456        let blob = CanonicalResolvedOutputBlobV1::from_signing_preimage(signing_preimage)?;
457        let record = Self {
458            resolved_output: blob.blob_ref().clone(),
459            resolved_output_size_bytes: u64::try_from(blob.bytes().len())
460                .map_err(|_| ToolOutcomeError::Overflow("resolved_output_size_bytes"))?,
461            terminal_dependency_root_digest: evaluation.step_result_root()?,
462            post_guard_decision_digest,
463            pricing_verdict_digest,
464            settlement_disposition,
465        };
466        record.validate()?;
467        Ok((record, blob))
468    }
469
470    fn validate(&self) -> Result<(), ToolOutcomeError> {
471        self.resolved_output.validate()?;
472        if usize::try_from(self.resolved_output_size_bytes)
473            .map_or(true, |size| size > MAX_RESOLVED_OUTPUT_BYTES)
474        {
475            return Err(ToolOutcomeError::Invalid(
476                "resolution.resolved_output_size_bytes",
477            ));
478        }
479        self.settlement_disposition.validate()
480    }
481}
482
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484#[serde(tag = "state", rename_all = "snake_case")]
485pub enum PostReturnEvaluationStateV1 {
486    Evaluating,
487    Resolved { resolution: PostReturnResolutionV1 },
488    Frozen { freeze: EvaluationFreezeV1 },
489}
490
491impl PostReturnEvaluationStateV1 {
492    #[allow(dead_code)]
493    fn name(&self) -> &'static str {
494        match self {
495            Self::Evaluating => "evaluating",
496            Self::Resolved { .. } => "resolved",
497            Self::Frozen { .. } => "frozen",
498        }
499    }
500}
501
502#[derive(Debug, Clone, PartialEq, Serialize)]
503pub struct PostReturnEvaluationRecordV1 {
504    pub(super) schema: &'static str,
505    pub(super) evaluation_id: AdmissionDigest,
506    pub(super) operation_id: AdmissionOperationId,
507    pub(super) tool_outcome_id: AdmissionDigest,
508    pub(super) tool_outcome_version: u64,
509    pub(super) raw_output_digest: AdmissionDigest,
510    pub(super) plan_digest: AdmissionDigest,
511    pub(super) frozen_steps: Vec<FrozenEvaluationStepV1>,
512    pub(super) trusted_time_unix_ms: u64,
513    pub(super) exact_inputs: PostReturnExactInputsV1,
514    pub(super) exact_inputs_digest: AdmissionDigest,
515    pub(super) step_results: Vec<EvaluationStepResultV1>,
516    pub(super) state: PostReturnEvaluationStateV1,
517    pub(super) version: u64,
518    pub(super) lifecycle_digest: AdmissionDigest,
519}
520
521#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
522#[serde(deny_unknown_fields)]
523pub struct PersistedPostReturnEvaluationRecordV1 {
524    pub schema: String,
525    pub evaluation_id: AdmissionDigest,
526    pub operation_id: AdmissionOperationId,
527    pub tool_outcome_id: AdmissionDigest,
528    pub tool_outcome_version: u64,
529    pub raw_output_digest: AdmissionDigest,
530    pub plan_digest: AdmissionDigest,
531    pub frozen_steps: Vec<FrozenEvaluationStepV1>,
532    pub trusted_time_unix_ms: u64,
533    pub exact_inputs: PostReturnExactInputsV1,
534    pub exact_inputs_digest: AdmissionDigest,
535    pub step_results: Vec<EvaluationStepResultV1>,
536    pub state: PostReturnEvaluationStateV1,
537    pub version: u64,
538    pub lifecycle_digest: AdmissionDigest,
539}
540
541#[derive(Serialize)]
542struct EvaluationIdentity<'a> {
543    operation_id: &'a AdmissionOperationId,
544    tool_outcome_id: &'a AdmissionDigest,
545    tool_outcome_version: u64,
546    raw_output_digest: &'a AdmissionDigest,
547    plan_digest: &'a AdmissionDigest,
548    trusted_time_unix_ms: u64,
549    exact_inputs_digest: &'a AdmissionDigest,
550}
551
552#[derive(Serialize)]
553struct EvaluationLifecycle<'a> {
554    evaluation_id: &'a AdmissionDigest,
555    step_results: &'a [EvaluationStepResultV1],
556    state: &'a PostReturnEvaluationStateV1,
557    version: u64,
558}
559
560fn evaluation_lifecycle_digest(
561    evaluation_id: &AdmissionDigest,
562    step_results: &[EvaluationStepResultV1],
563    state: &PostReturnEvaluationStateV1,
564    version: u64,
565) -> Result<AdmissionDigest, ToolOutcomeError> {
566    domain_digest(
567        "chio.post-return-evaluation.lifecycle.v1",
568        &EvaluationLifecycle {
569            evaluation_id,
570            step_results,
571            state,
572            version,
573        },
574    )
575}
576
577impl PostReturnEvaluationRecordV1 {
578    pub fn to_persisted(&self) -> PersistedPostReturnEvaluationRecordV1 {
579        PersistedPostReturnEvaluationRecordV1 {
580            schema: self.schema.to_owned(),
581            evaluation_id: self.evaluation_id.clone(),
582            operation_id: self.operation_id.clone(),
583            tool_outcome_id: self.tool_outcome_id.clone(),
584            tool_outcome_version: self.tool_outcome_version,
585            raw_output_digest: self.raw_output_digest.clone(),
586            plan_digest: self.plan_digest.clone(),
587            frozen_steps: self.frozen_steps.clone(),
588            trusted_time_unix_ms: self.trusted_time_unix_ms,
589            exact_inputs: self.exact_inputs.clone(),
590            exact_inputs_digest: self.exact_inputs_digest.clone(),
591            step_results: self.step_results.clone(),
592            state: self.state.clone(),
593            version: self.version,
594            lifecycle_digest: self.lifecycle_digest.clone(),
595        }
596    }
597
598    pub fn from_persisted(
599        value: PersistedPostReturnEvaluationRecordV1,
600    ) -> Result<Self, ToolOutcomeError> {
601        if value.schema != POST_RETURN_EVALUATION_SCHEMA {
602            return Err(ToolOutcomeError::Invalid("evaluation.schema"));
603        }
604        let record = Self {
605            schema: POST_RETURN_EVALUATION_SCHEMA,
606            evaluation_id: value.evaluation_id,
607            operation_id: value.operation_id,
608            tool_outcome_id: value.tool_outcome_id,
609            tool_outcome_version: value.tool_outcome_version,
610            raw_output_digest: value.raw_output_digest,
611            plan_digest: value.plan_digest,
612            frozen_steps: value.frozen_steps,
613            trusted_time_unix_ms: value.trusted_time_unix_ms,
614            exact_inputs: value.exact_inputs,
615            exact_inputs_digest: value.exact_inputs_digest,
616            step_results: value.step_results,
617            state: value.state,
618            version: value.version,
619            lifecycle_digest: value.lifecycle_digest,
620        };
621        record.validate()?;
622        Ok(record)
623    }
624
625    #[allow(dead_code)]
626    pub(crate) fn prepare(
627        operation: &AdmissionOperationV1,
628        outcome: &ToolOutcomeRecordV1,
629        frozen_steps: Vec<FrozenEvaluationStepV1>,
630        trusted_time_unix_ms: u64,
631        normalized_request_context: PostReturnNormalizedRequestContextV1,
632    ) -> Result<Self, ToolOutcomeError> {
633        outcome.validate_against(operation)?;
634        if !matches!(outcome.disposition, ResolvedToolOutcomeV1::Returned) {
635            return Err(ToolOutcomeError::Transition {
636                state: outcome.disposition.name(),
637                transition: "prepare_evaluation",
638            });
639        }
640        validate_steps(&frozen_steps)?;
641        positive("evaluation.trusted_time", trusted_time_unix_ms)?;
642        let exact_inputs = PostReturnExactInputsV1::from_records(
643            operation,
644            outcome,
645            normalized_request_context,
646            trusted_time_unix_ms,
647        )?;
648        let exact_inputs_digest = digest_bytes(
649            "evaluation.exact_inputs_digest",
650            &bounded(
651                "evaluation.exact_inputs",
652                &exact_inputs,
653                MAX_FROZEN_INPUT_BYTES,
654            )?,
655        )?;
656        let plan_digest = domain_digest("chio.post-return-plan.v1", &frozen_steps)?;
657        let identity = EvaluationIdentity {
658            operation_id: &outcome.operation_id,
659            tool_outcome_id: &outcome.outcome_id,
660            tool_outcome_version: outcome.version,
661            raw_output_digest: outcome.raw_output.digest(),
662            plan_digest: &plan_digest,
663            trusted_time_unix_ms,
664            exact_inputs_digest: &exact_inputs_digest,
665        };
666        let evaluation_id = domain_digest("chio.post-return-evaluation.identity.v1", &identity)?;
667        let step_results = Vec::new();
668        let state = PostReturnEvaluationStateV1::Evaluating;
669        let version = 1;
670        let lifecycle_digest =
671            evaluation_lifecycle_digest(&evaluation_id, &step_results, &state, version)?;
672        let record = Self {
673            schema: POST_RETURN_EVALUATION_SCHEMA,
674            evaluation_id,
675            operation_id: outcome.operation_id.clone(),
676            tool_outcome_id: outcome.outcome_id.clone(),
677            tool_outcome_version: outcome.version,
678            raw_output_digest: outcome.raw_output.digest().clone(),
679            plan_digest,
680            frozen_steps,
681            trusted_time_unix_ms,
682            exact_inputs,
683            exact_inputs_digest,
684            step_results,
685            state,
686            version,
687            lifecycle_digest,
688        };
689        record.validate()?;
690        Ok(record)
691    }
692
693    pub fn evaluation_id(&self) -> &AdmissionDigest {
694        &self.evaluation_id
695    }
696
697    pub fn operation_id(&self) -> &AdmissionOperationId {
698        &self.operation_id
699    }
700
701    pub fn version(&self) -> u64 {
702        self.version
703    }
704
705    pub fn state(&self) -> &PostReturnEvaluationStateV1 {
706        &self.state
707    }
708
709    pub(crate) fn trusted_time_unix_ms(&self) -> u64 {
710        self.trusted_time_unix_ms
711    }
712
713    pub(crate) fn step_result_digest(&self, index: usize) -> Option<&AdmissionDigest> {
714        self.step_results
715            .get(index)
716            .map(|result| &result.result_digest)
717    }
718
719    pub(crate) fn validate_replay_contract(
720        &self,
721        frozen_steps: &[FrozenEvaluationStepV1],
722        normalized_request_context: &PostReturnNormalizedRequestContextV1,
723    ) -> Result<(), ToolOutcomeError> {
724        self.validate()?;
725        if self.frozen_steps != frozen_steps
726            || &self.exact_inputs.normalized_request_context != normalized_request_context
727        {
728            return Err(ToolOutcomeError::Binding("evaluation.replay_contract"));
729        }
730        Ok(())
731    }
732
733    #[allow(dead_code)]
734    pub fn validate_against(
735        &self,
736        operation: &AdmissionOperationV1,
737        outcome: &ToolOutcomeRecordV1,
738    ) -> Result<(), ToolOutcomeError> {
739        self.validate()?;
740        self.exact_inputs.validate_against(operation, outcome)?;
741        let recorded_return_version = recorded_return_version(outcome)?;
742        if self.operation_id != *operation.binding().operation_id()
743            || self.tool_outcome_id != outcome.outcome_id
744            || self.tool_outcome_version != recorded_return_version
745            || self.raw_output_digest != *outcome.raw_output.digest()
746        {
747            return Err(ToolOutcomeError::Binding("evaluation.records"));
748        }
749        Ok(())
750    }
751
752    pub(super) fn step_result_root(&self) -> Result<AdmissionDigest, ToolOutcomeError> {
753        domain_digest("chio.post-return-step-results.v1", &self.step_results)
754    }
755
756    pub(crate) fn record_next_pure_result(
757        &self,
758        result_digest: AdmissionDigest,
759    ) -> Result<Self, ToolOutcomeError> {
760        let index = self.step_results.len();
761        let step = self
762            .frozen_steps
763            .get(index)
764            .ok_or(ToolOutcomeError::Invalid(
765                "evaluation.no_step_result_expected",
766            ))?;
767        if step.mode != EvaluationModeV1::Pure {
768            return Err(ToolOutcomeError::Invalid(
769                "evaluation.next_step_is_not_pure",
770            ));
771        }
772        let step_index = u32::try_from(index)
773            .map_err(|_| ToolOutcomeError::Overflow("evaluation.step_index"))?;
774        let input_dependency_digest = self.step_results.last().map_or_else(
775            || self.exact_inputs_digest.clone(),
776            |prior| prior.result_digest.clone(),
777        );
778        self.transition(
779            self.version,
780            PostReturnEvaluationTransitionV1::RecordStepResult(EvaluationStepResultV1::pure(
781                step_index,
782                input_dependency_digest,
783                result_digest,
784            )),
785        )
786    }
787
788    pub(crate) fn resolve_with_signing_preimage(
789        &self,
790        signing_preimage: Vec<u8>,
791        post_guard_decision_digest: AdmissionDigest,
792        pricing_verdict_digest: AdmissionDigest,
793        settlement_disposition: SettlementDispositionV1,
794    ) -> Result<(Self, CanonicalResolvedOutputBlobV1), ToolOutcomeError> {
795        let (resolution, blob) = PostReturnResolutionV1::from_signing_preimage(
796            self,
797            signing_preimage,
798            post_guard_decision_digest,
799            pricing_verdict_digest,
800            settlement_disposition,
801        )?;
802        self.transition(
803            self.version,
804            PostReturnEvaluationTransitionV1::Resolve(resolution),
805        )
806        .map(|terminal| (terminal, blob))
807    }
808
809    pub(super) fn validate(&self) -> Result<(), ToolOutcomeError> {
810        positive("evaluation.tool_outcome_version", self.tool_outcome_version)?;
811        positive("evaluation.trusted_time", self.trusted_time_unix_ms)?;
812        positive("evaluation.version", self.version)?;
813        validate_steps(&self.frozen_steps)?;
814        self.exact_inputs.validate()?;
815        if self.exact_inputs.operation_id != self.operation_id
816            || self.exact_inputs.tool_outcome_id != self.tool_outcome_id
817            || self.exact_inputs.tool_outcome_version != self.tool_outcome_version
818            || self.exact_inputs.raw_output_digest != self.raw_output_digest
819            || self.exact_inputs.trusted_time_unix_ms != self.trusted_time_unix_ms
820        {
821            return Err(ToolOutcomeError::Binding("evaluation.exact_inputs"));
822        }
823        if domain_digest("chio.post-return-plan.v1", &self.frozen_steps)? != self.plan_digest {
824            return Err(ToolOutcomeError::Binding("evaluation.plan_digest"));
825        }
826        if digest_bytes(
827            "evaluation.exact_inputs_digest",
828            &bounded(
829                "evaluation.exact_inputs",
830                &self.exact_inputs,
831                MAX_FROZEN_INPUT_BYTES,
832            )?,
833        )? != self.exact_inputs_digest
834        {
835            return Err(ToolOutcomeError::Binding("evaluation.exact_inputs_digest"));
836        }
837        let expected = domain_digest(
838            "chio.post-return-evaluation.identity.v1",
839            &EvaluationIdentity {
840                operation_id: &self.operation_id,
841                tool_outcome_id: &self.tool_outcome_id,
842                tool_outcome_version: self.tool_outcome_version,
843                raw_output_digest: &self.raw_output_digest,
844                plan_digest: &self.plan_digest,
845                trusted_time_unix_ms: self.trusted_time_unix_ms,
846                exact_inputs_digest: &self.exact_inputs_digest,
847            },
848        )?;
849        if expected != self.evaluation_id {
850            return Err(ToolOutcomeError::Binding("evaluation.evaluation_id"));
851        }
852        if self.lifecycle_digest
853            != evaluation_lifecycle_digest(
854                &self.evaluation_id,
855                &self.step_results,
856                &self.state,
857                self.version,
858            )?
859        {
860            return Err(ToolOutcomeError::Binding("evaluation.lifecycle_digest"));
861        }
862        validate_results(
863            &self.frozen_steps,
864            &self.step_results,
865            &self.exact_inputs_digest,
866            self.trusted_time_unix_ms,
867        )?;
868        let terminal_increment = u64::from(!matches!(
869            &self.state,
870            PostReturnEvaluationStateV1::Evaluating
871        ));
872        let expected_version = u64::try_from(self.step_results.len())
873            .ok()
874            .and_then(|count| count.checked_add(1))
875            .and_then(|version| version.checked_add(terminal_increment))
876            .ok_or(ToolOutcomeError::Overflow("evaluation.lifecycle_version"))?;
877        if self.version != expected_version {
878            return Err(ToolOutcomeError::Binding("evaluation.lifecycle_version"));
879        }
880        match &self.state {
881            PostReturnEvaluationStateV1::Evaluating => Ok(()),
882            PostReturnEvaluationStateV1::Resolved { resolution } => {
883                resolution.validate()?;
884                if self.step_results.len() != self.frozen_steps.len()
885                    || resolution.terminal_dependency_root_digest != self.step_result_root()?
886                {
887                    return Err(ToolOutcomeError::Binding(
888                        "evaluation.terminal_dependency_root",
889                    ));
890                }
891                Ok(())
892            }
893            PostReturnEvaluationStateV1::Frozen { freeze } => {
894                validate_freeze(freeze, &self.frozen_steps, self.step_results.len())
895            }
896        }
897    }
898
899    #[allow(dead_code)]
900    pub(crate) fn transition(
901        &self,
902        expected_version: u64,
903        transition: PostReturnEvaluationTransitionV1,
904    ) -> Result<Self, ToolOutcomeError> {
905        self.validate()?;
906        if self.version != expected_version {
907            return Err(ToolOutcomeError::Cas {
908                expected: expected_version,
909                actual: self.version,
910            });
911        }
912        if !matches!(self.state, PostReturnEvaluationStateV1::Evaluating) {
913            return Err(ToolOutcomeError::Transition {
914                state: self.state.name(),
915                transition: transition.name(),
916            });
917        }
918        let mut next = self.clone();
919        next.version = next
920            .version
921            .checked_add(1)
922            .ok_or(ToolOutcomeError::Overflow("evaluation.version"))?;
923        match transition {
924            PostReturnEvaluationTransitionV1::RecordStepResult(result) => {
925                let index = self.step_results.len();
926                let step = self
927                    .frozen_steps
928                    .get(index)
929                    .ok_or(ToolOutcomeError::Invalid(
930                        "evaluation.no_step_result_expected",
931                    ))?;
932                let dependency = self
933                    .step_results
934                    .last()
935                    .map_or(&self.exact_inputs_digest, |prior| &prior.result_digest);
936                result.validate_for(index, step, dependency, self.trusted_time_unix_ms)?;
937                next.step_results.push(result);
938            }
939            PostReturnEvaluationTransitionV1::Resolve(resolution) => {
940                if self.step_results.len() != self.frozen_steps.len()
941                    || resolution.terminal_dependency_root_digest != self.step_result_root()?
942                {
943                    return Err(ToolOutcomeError::Invalid(
944                        "evaluation.incomplete_step_results",
945                    ));
946                }
947                resolution.validate()?;
948                next.state = PostReturnEvaluationStateV1::Resolved { resolution };
949            }
950            PostReturnEvaluationTransitionV1::Freeze(freeze) => {
951                validate_freeze(&freeze, &self.frozen_steps, self.step_results.len())?;
952                next.state = PostReturnEvaluationStateV1::Frozen { freeze };
953            }
954        }
955        next.lifecycle_digest = evaluation_lifecycle_digest(
956            &next.evaluation_id,
957            &next.step_results,
958            &next.state,
959            next.version,
960        )?;
961        next.validate()?;
962        Ok(next)
963    }
964
965    pub fn replay_action(
966        &self,
967        step_index: u32,
968    ) -> Result<PostReturnReplayActionV1, ToolOutcomeError> {
969        let index = usize::try_from(step_index)
970            .map_err(|_| ToolOutcomeError::Invalid("replay.step_index"))?;
971        let step = self
972            .frozen_steps
973            .get(index)
974            .ok_or(ToolOutcomeError::Invalid("replay.step_index"))?;
975        match &self.state {
976            PostReturnEvaluationStateV1::Resolved { .. } => {
977                Ok(PostReturnReplayActionV1::DoNotRunResolved)
978            }
979            PostReturnEvaluationStateV1::Frozen { .. } => {
980                Ok(PostReturnReplayActionV1::DoNotRunFrozen)
981            }
982            PostReturnEvaluationStateV1::Evaluating if index < self.step_results.len() => {
983                Ok(PostReturnReplayActionV1::UseRecordedStepResult {
984                    result_digest: self.step_results[index].result_digest.clone(),
985                })
986            }
987            PostReturnEvaluationStateV1::Evaluating if index > self.step_results.len() => {
988                Err(ToolOutcomeError::Invalid("replay.out_of_order"))
989            }
990            PostReturnEvaluationStateV1::Evaluating => match &step.mode {
991                EvaluationModeV1::Pure => Ok(PostReturnReplayActionV1::ReplayPureFromFrozenInputs),
992                EvaluationModeV1::ExternalStateful { call_id } => {
993                    Ok(PostReturnReplayActionV1::LookupExternalResult {
994                        call_id: call_id.clone(),
995                    })
996                }
997            },
998        }
999    }
1000
1001    pub fn validate_for_store_mutation(
1002        &self,
1003        trusted_now_unix_ms: u64,
1004    ) -> Result<(), ToolOutcomeError> {
1005        self.validate()?;
1006        positive("evaluation.store_trusted_now", trusted_now_unix_ms)?;
1007        if trusted_now_unix_ms < self.trusted_time_unix_ms
1008            || self.step_results.iter().any(|result| {
1009                result
1010                    .external_result
1011                    .as_ref()
1012                    .is_some_and(|external| external.authenticated_at_unix_ms > trusted_now_unix_ms)
1013            })
1014        {
1015            return Err(ToolOutcomeError::Binding("evaluation.store_trusted_now"));
1016        }
1017        Ok(())
1018    }
1019
1020    #[allow(dead_code)]
1021    pub(crate) fn terminal_evidence(
1022        &self,
1023    ) -> Result<PostReturnTerminalEvidenceV1, ToolOutcomeError> {
1024        self.validate()?;
1025        let PostReturnEvaluationStateV1::Resolved { resolution } = &self.state else {
1026            return Err(ToolOutcomeError::Invalid("terminal_evidence.unresolved"));
1027        };
1028        Ok(PostReturnTerminalEvidenceV1 {
1029            evaluation_id: self.evaluation_id.clone(),
1030            operation_id: self.operation_id.clone(),
1031            tool_outcome_id: self.tool_outcome_id.clone(),
1032            tool_outcome_version: self.tool_outcome_version,
1033            raw_output_digest: self.raw_output_digest.clone(),
1034            resolved_output: resolution.resolved_output.clone(),
1035            resolved_output_size_bytes: resolution.resolved_output_size_bytes,
1036            terminal_dependency_root_digest: resolution.terminal_dependency_root_digest.clone(),
1037            post_guard_decision_digest: resolution.post_guard_decision_digest.clone(),
1038            pricing_verdict_digest: resolution.pricing_verdict_digest.clone(),
1039            settlement_disposition: resolution.settlement_disposition.clone(),
1040        })
1041    }
1042
1043    #[allow(dead_code)]
1044    pub(crate) fn freeze_evidence(&self) -> Result<PostReturnFreezeEvidenceV1, ToolOutcomeError> {
1045        self.validate()?;
1046        let PostReturnEvaluationStateV1::Frozen { freeze } = &self.state else {
1047            return Err(ToolOutcomeError::Invalid("freeze_evidence.not_frozen"));
1048        };
1049        Ok(PostReturnFreezeEvidenceV1 {
1050            evaluation_id: self.evaluation_id.clone(),
1051            operation_id: self.operation_id.clone(),
1052            tool_outcome_id: self.tool_outcome_id.clone(),
1053            tool_outcome_version: self.tool_outcome_version,
1054            raw_output_digest: self.raw_output_digest.clone(),
1055            freeze_evidence_digest: freeze.evidence_digest().clone(),
1056        })
1057    }
1058}
1059
1060#[allow(dead_code)]
1061fn recorded_return_version(outcome: &ToolOutcomeRecordV1) -> Result<u64, ToolOutcomeError> {
1062    match outcome.disposition {
1063        ResolvedToolOutcomeV1::Returned => Ok(outcome.version),
1064        ResolvedToolOutcomeV1::Resolved { .. } | ResolvedToolOutcomeV1::Frozen { .. } => outcome
1065            .version
1066            .checked_sub(1)
1067            .ok_or(ToolOutcomeError::Binding("evaluation.outcome_version")),
1068    }
1069}
1070
1071fn validate_steps(steps: &[FrozenEvaluationStepV1]) -> Result<(), ToolOutcomeError> {
1072    if steps.is_empty() || steps.len() > MAX_EVALUATION_STEPS {
1073        return Err(ToolOutcomeError::TooLarge {
1074            field: "evaluation.steps",
1075            actual: steps.len(),
1076            maximum: MAX_EVALUATION_STEPS,
1077        });
1078    }
1079    let mut guard_position = 0;
1080    let mut pricing_position = 0;
1081    let mut pricing_started = false;
1082    let mut external_call_ids = Vec::new();
1083    for step in steps {
1084        let expected = match step.phase {
1085            EvaluationPhaseV1::OutputGuard if !pricing_started => &mut guard_position,
1086            EvaluationPhaseV1::Pricing => {
1087                pricing_started = true;
1088                &mut pricing_position
1089            }
1090            EvaluationPhaseV1::OutputGuard => {
1091                return Err(ToolOutcomeError::Invalid("evaluation.steps.phase_order"));
1092            }
1093        };
1094        if step.position != *expected {
1095            return Err(ToolOutcomeError::Invalid("evaluation.steps.position"));
1096        }
1097        if let EvaluationModeV1::ExternalStateful { call_id } = &step.mode {
1098            if external_call_ids.contains(call_id) {
1099                return Err(ToolOutcomeError::Invalid(
1100                    "evaluation.steps.duplicate_call_id",
1101                ));
1102            }
1103            external_call_ids.push(call_id.clone());
1104        }
1105        *expected += 1;
1106    }
1107    Ok(())
1108}
1109
1110fn validate_results(
1111    steps: &[FrozenEvaluationStepV1],
1112    results: &[EvaluationStepResultV1],
1113    exact_inputs_digest: &AdmissionDigest,
1114    evaluation_trusted_time_unix_ms: u64,
1115) -> Result<(), ToolOutcomeError> {
1116    if results.len() > steps.len() {
1117        return Err(ToolOutcomeError::TooLarge {
1118            field: "evaluation.step_results",
1119            actual: results.len(),
1120            maximum: steps.len(),
1121        });
1122    }
1123    for (index, result) in results.iter().enumerate() {
1124        let dependency = index
1125            .checked_sub(1)
1126            .map_or(exact_inputs_digest, |previous| {
1127                &results[previous].result_digest
1128            });
1129        result.validate_for(
1130            index,
1131            steps
1132                .get(index)
1133                .ok_or(ToolOutcomeError::Binding("step_result.step"))?,
1134            dependency,
1135            evaluation_trusted_time_unix_ms,
1136        )?;
1137    }
1138    Ok(())
1139}
1140
1141fn validate_freeze(
1142    freeze: &EvaluationFreezeV1,
1143    steps: &[FrozenEvaluationStepV1],
1144    result_count: usize,
1145) -> Result<(), ToolOutcomeError> {
1146    freeze.validate(steps.len())?;
1147    if usize::try_from(freeze.step_index()).ok() != Some(result_count)
1148        || !matches!(
1149            steps.get(result_count).map(|step| &step.mode),
1150            Some(EvaluationModeV1::ExternalStateful { .. })
1151        )
1152    {
1153        return Err(ToolOutcomeError::Binding(
1154            "evaluation_freeze.next_external_step",
1155        ));
1156    }
1157    Ok(())
1158}
1159
1160#[allow(dead_code)]
1161#[derive(Debug, Clone)]
1162pub(crate) enum PostReturnEvaluationTransitionV1 {
1163    RecordStepResult(EvaluationStepResultV1),
1164    Resolve(PostReturnResolutionV1),
1165    Freeze(EvaluationFreezeV1),
1166}
1167
1168impl PostReturnEvaluationTransitionV1 {
1169    #[allow(dead_code)]
1170    fn name(&self) -> &'static str {
1171        match self {
1172            Self::RecordStepResult(_) => "record_step_result",
1173            Self::Resolve(_) => "resolve",
1174            Self::Freeze(_) => "freeze",
1175        }
1176    }
1177}
1178
1179#[derive(Debug, Clone, PartialEq, Eq)]
1180pub enum PostReturnReplayActionV1 {
1181    ReplayPureFromFrozenInputs,
1182    UseRecordedStepResult { result_digest: AdmissionDigest },
1183    LookupExternalResult { call_id: AdmissionIdentifier },
1184    DoNotRunResolved,
1185    DoNotRunFrozen,
1186}
1187
1188#[derive(Debug, Clone, Serialize)]
1189pub struct PostReturnTerminalEvidenceV1 {
1190    pub(super) evaluation_id: AdmissionDigest,
1191    pub(super) operation_id: AdmissionOperationId,
1192    pub(super) tool_outcome_id: AdmissionDigest,
1193    pub(super) tool_outcome_version: u64,
1194    pub(super) raw_output_digest: AdmissionDigest,
1195    pub(super) resolved_output: ContentAddressedBlobRefV1,
1196    pub(super) resolved_output_size_bytes: u64,
1197    pub(super) terminal_dependency_root_digest: AdmissionDigest,
1198    pub(super) post_guard_decision_digest: AdmissionDigest,
1199    pub(super) pricing_verdict_digest: AdmissionDigest,
1200    pub(super) settlement_disposition: SettlementDispositionV1,
1201}
1202
1203impl PostReturnTerminalEvidenceV1 {
1204    #[allow(dead_code)]
1205    pub(super) fn binds(&self, outcome: &ToolOutcomeRecordV1) -> Result<(), ToolOutcomeError> {
1206        if self.operation_id != outcome.operation_id
1207            || self.tool_outcome_id != outcome.outcome_id
1208            || self.tool_outcome_version != outcome.version
1209            || self.raw_output_digest != *outcome.raw_output.digest()
1210        {
1211            return Err(ToolOutcomeError::Binding("terminal_evidence.outcome"));
1212        }
1213        Ok(())
1214    }
1215}
1216
1217#[derive(Debug, Clone, Serialize)]
1218pub struct PostReturnFreezeEvidenceV1 {
1219    pub(super) evaluation_id: AdmissionDigest,
1220    pub(super) operation_id: AdmissionOperationId,
1221    pub(super) tool_outcome_id: AdmissionDigest,
1222    pub(super) tool_outcome_version: u64,
1223    pub(super) raw_output_digest: AdmissionDigest,
1224    pub(super) freeze_evidence_digest: AdmissionDigest,
1225}
1226
1227impl PostReturnFreezeEvidenceV1 {
1228    #[allow(dead_code)]
1229    pub(super) fn binds(&self, outcome: &ToolOutcomeRecordV1) -> Result<(), ToolOutcomeError> {
1230        if self.operation_id != outcome.operation_id
1231            || self.tool_outcome_id != outcome.outcome_id
1232            || self.tool_outcome_version != outcome.version
1233            || self.raw_output_digest != *outcome.raw_output.digest()
1234        {
1235            return Err(ToolOutcomeError::Binding("freeze_evidence.outcome"));
1236        }
1237        Ok(())
1238    }
1239}