1use 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}