Skip to main content

ferrum_interfaces/vnext/operation/
provider.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use sha2::{Digest, Sha256};
3use std::collections::BTreeSet;
4use std::fmt;
5
6use super::super::{
7    CapabilityId, ContractVersion, DeviceId, ExecutionIdentityEnvelope, NodeId, OperationId,
8    ProviderId, QuantizationFormatId, UnvalidatedExecutionIdentityParts, VNextError,
9    WeightFormatId,
10};
11use super::foundation::{canonical_sha256, invalid_operation};
12use super::{
13    DynamicStorageRequirement, ProfilePhase, ProviderCheckpointCapability,
14    ProviderStorageBindingRequirement, ResolvedValueRole,
15};
16
17pub const PROVIDER_EXECUTION_SEMANTICS_VERSION: ContractVersion = ContractVersion::new(1, 0);
18pub const MAX_OPERATION_FAILURE_WIRE_BYTES: usize = 16 * 1024;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub struct ProviderExecutionContractFingerprint([u8; 32]);
22
23impl ProviderExecutionContractFingerprint {
24    pub const fn as_bytes(&self) -> &[u8; 32] {
25        &self.0
26    }
27}
28
29impl fmt::Display for ProviderExecutionContractFingerprint {
30    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        for byte in self.0 {
32            write!(formatter, "{byte:02x}")?;
33        }
34        Ok(())
35    }
36}
37
38impl Serialize for ProviderExecutionContractFingerprint {
39    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
40    where
41        S: Serializer,
42    {
43        serializer.collect_str(self)
44    }
45}
46
47impl<'de> Deserialize<'de> for ProviderExecutionContractFingerprint {
48    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
49    where
50        D: Deserializer<'de>,
51    {
52        let value = String::deserialize(deserializer)?;
53        if !canonical_sha256(&value) {
54            return Err(serde::de::Error::custom(
55                "provider execution contract fingerprint must be a lowercase SHA256",
56            ));
57        }
58        let mut bytes = [0_u8; 32];
59        for (index, byte) in bytes.iter_mut().enumerate() {
60            *byte = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16)
61                .map_err(serde::de::Error::custom)?;
62        }
63        Ok(Self(bytes))
64    }
65}
66
67/// Repeatability promised for one immutable plan/provider/runtime binding.
68///
69/// Bitwise equality covers every declared output and state effect when logical
70/// inputs, explicit RNG state, initial state, and initialized workspaces are
71/// identical. Approximation against an independent oracle is a separate
72/// operation contract and cannot weaken this boundary.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ProviderExecutionRepeatability {
76    BitwiseSameRuntime,
77}
78
79impl ProviderExecutionRepeatability {
80    pub const fn as_str(self) -> &'static str {
81        match self {
82            Self::BitwiseSameRuntime => "bitwise_same_runtime",
83        }
84    }
85}
86
87/// Whether a provider authorizes reusable device execution for the same
88/// immutable eager operation.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum ProviderReplayEquivalence {
92    Ineligible,
93    BitwiseEagerEquivalent,
94}
95
96impl ProviderReplayEquivalence {
97    pub const fn as_str(self) -> &'static str {
98        match self {
99            Self::Ineligible => "ineligible",
100            Self::BitwiseEagerEquivalent => "bitwise_eager_equivalent",
101        }
102    }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
106pub struct ProviderExecutionSemantics {
107    contract_version: ContractVersion,
108    contract_fingerprint: ProviderExecutionContractFingerprint,
109    repeatability: ProviderExecutionRepeatability,
110    replay_equivalence: ProviderReplayEquivalence,
111}
112
113#[derive(Deserialize)]
114#[serde(deny_unknown_fields)]
115struct ProviderExecutionSemanticsWire {
116    contract_version: ContractVersion,
117    contract_fingerprint: ProviderExecutionContractFingerprint,
118    repeatability: ProviderExecutionRepeatability,
119    replay_equivalence: ProviderReplayEquivalence,
120}
121
122impl<'de> Deserialize<'de> for ProviderExecutionSemantics {
123    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124    where
125        D: Deserializer<'de>,
126    {
127        let wire = ProviderExecutionSemanticsWire::deserialize(deserializer)?;
128        let semantics = Self::new(
129            wire.contract_version,
130            wire.repeatability,
131            wire.replay_equivalence,
132        )
133        .map_err(serde::de::Error::custom)?;
134        if semantics.contract_fingerprint != wire.contract_fingerprint {
135            return Err(serde::de::Error::custom(format!(
136                "provider execution contract fingerprint mismatch: expected `{}`, actual `{}`",
137                semantics.contract_fingerprint, wire.contract_fingerprint
138            )));
139        }
140        Ok(semantics)
141    }
142}
143
144impl ProviderExecutionSemantics {
145    fn new(
146        contract_version: ContractVersion,
147        repeatability: ProviderExecutionRepeatability,
148        replay_equivalence: ProviderReplayEquivalence,
149    ) -> Result<Self, VNextError> {
150        if contract_version != PROVIDER_EXECUTION_SEMANTICS_VERSION {
151            return Err(invalid_operation(format!(
152                "provider execution semantics version {contract_version} is unsupported"
153            )));
154        }
155        let mut digest = Sha256::new();
156        digest.update(b"ferrum.runtime-vnext.provider-execution-semantics.v1\0");
157        digest.update(contract_version.major.to_le_bytes());
158        digest.update(contract_version.minor.to_le_bytes());
159        digest.update([match repeatability {
160            ProviderExecutionRepeatability::BitwiseSameRuntime => 1,
161        }]);
162        digest.update([match replay_equivalence {
163            ProviderReplayEquivalence::Ineligible => 0,
164            ProviderReplayEquivalence::BitwiseEagerEquivalent => 1,
165        }]);
166        Ok(Self {
167            contract_version,
168            contract_fingerprint: ProviderExecutionContractFingerprint(digest.finalize().into()),
169            repeatability,
170            replay_equivalence,
171        })
172    }
173
174    pub fn bitwise_eager_only() -> Self {
175        Self::new(
176            PROVIDER_EXECUTION_SEMANTICS_VERSION,
177            ProviderExecutionRepeatability::BitwiseSameRuntime,
178            ProviderReplayEquivalence::Ineligible,
179        )
180        .expect("built-in eager execution semantics are valid")
181    }
182
183    pub fn bitwise_eager_and_replay() -> Self {
184        Self::new(
185            PROVIDER_EXECUTION_SEMANTICS_VERSION,
186            ProviderExecutionRepeatability::BitwiseSameRuntime,
187            ProviderReplayEquivalence::BitwiseEagerEquivalent,
188        )
189        .expect("built-in reusable execution semantics are valid")
190    }
191
192    pub const fn contract_version(self) -> ContractVersion {
193        self.contract_version
194    }
195
196    pub const fn contract_fingerprint(self) -> ProviderExecutionContractFingerprint {
197        self.contract_fingerprint
198    }
199
200    pub const fn repeatability(self) -> ProviderExecutionRepeatability {
201        self.repeatability
202    }
203
204    pub const fn replay_equivalence(self) -> ProviderReplayEquivalence {
205        self.replay_equivalence
206    }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum ExecutionDeterminismRequirement {
212    BitwiseSameRuntime,
213    BitwiseSameRuntimeWithReplay,
214}
215
216impl ExecutionDeterminismRequirement {
217    pub const fn as_str(self) -> &'static str {
218        match self {
219            Self::BitwiseSameRuntime => "bitwise_same_runtime",
220            Self::BitwiseSameRuntimeWithReplay => "bitwise_same_runtime_with_replay",
221        }
222    }
223
224    pub const fn requires_replay_equivalence(self) -> bool {
225        matches!(self, Self::BitwiseSameRuntimeWithReplay)
226    }
227
228    pub fn accepts(self, semantics: ProviderExecutionSemantics) -> bool {
229        semantics.repeatability == ProviderExecutionRepeatability::BitwiseSameRuntime
230            && (!self.requires_replay_equivalence()
231                || semantics.replay_equivalence
232                    == ProviderReplayEquivalence::BitwiseEagerEquivalent)
233    }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
237pub struct OperationProviderDescriptor {
238    provider_id: ProviderId,
239    operation_id: OperationId,
240    operation_fingerprint: String,
241    provider_implementation_fingerprint: String,
242    execution_semantics: ProviderExecutionSemantics,
243    #[serde(skip_serializing_if = "ProviderCheckpointCapability::is_unsupported")]
244    checkpoint: ProviderCheckpointCapability,
245    version: ContractVersion,
246    device_id: DeviceId,
247    capabilities: BTreeSet<CapabilityId>,
248    accepted_weight_formats: BTreeSet<WeightFormatId>,
249    accepted_quantization_formats: BTreeSet<QuantizationFormatId>,
250    dynamic_storage_bindings: Vec<ProviderStorageBindingRequirement>,
251    resource_estimator_id: String,
252    resource_estimator_version: ContractVersion,
253    resource_estimator_implementation_fingerprint: String,
254}
255
256#[derive(Deserialize)]
257#[serde(deny_unknown_fields)]
258struct OperationProviderDescriptorWire {
259    provider_id: ProviderId,
260    operation_id: OperationId,
261    operation_fingerprint: String,
262    provider_implementation_fingerprint: String,
263    execution_semantics: ProviderExecutionSemantics,
264    #[serde(default)]
265    checkpoint: ProviderCheckpointCapability,
266    version: ContractVersion,
267    device_id: DeviceId,
268    capabilities: BTreeSet<CapabilityId>,
269    accepted_weight_formats: BTreeSet<WeightFormatId>,
270    accepted_quantization_formats: BTreeSet<QuantizationFormatId>,
271    dynamic_storage_bindings: Vec<ProviderStorageBindingRequirement>,
272    resource_estimator_id: String,
273    resource_estimator_version: ContractVersion,
274    resource_estimator_implementation_fingerprint: String,
275}
276
277impl<'de> Deserialize<'de> for OperationProviderDescriptor {
278    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
279    where
280        D: Deserializer<'de>,
281    {
282        let wire = OperationProviderDescriptorWire::deserialize(deserializer)?;
283        let original_bindings = wire.dynamic_storage_bindings.clone();
284        let descriptor = Self::new(
285            wire.provider_id,
286            wire.operation_id,
287            wire.operation_fingerprint,
288            wire.provider_implementation_fingerprint,
289            wire.execution_semantics,
290            wire.version,
291            wire.device_id,
292            wire.capabilities,
293            wire.accepted_weight_formats,
294            wire.accepted_quantization_formats,
295            wire.dynamic_storage_bindings,
296            wire.resource_estimator_id,
297            wire.resource_estimator_version,
298            wire.resource_estimator_implementation_fingerprint,
299        )
300        .map_err(serde::de::Error::custom)?
301        .with_checkpoint_capability(wire.checkpoint);
302        if descriptor.dynamic_storage_bindings != original_bindings {
303            return Err(serde::de::Error::custom(
304                "provider storage binding requirements are not canonical",
305            ));
306        }
307        Ok(descriptor)
308    }
309}
310
311impl OperationProviderDescriptor {
312    #[allow(clippy::too_many_arguments)]
313    pub fn new(
314        provider_id: ProviderId,
315        operation_id: OperationId,
316        operation_fingerprint: impl Into<String>,
317        provider_implementation_fingerprint: impl Into<String>,
318        execution_semantics: ProviderExecutionSemantics,
319        version: ContractVersion,
320        device_id: DeviceId,
321        capabilities: BTreeSet<CapabilityId>,
322        accepted_weight_formats: BTreeSet<WeightFormatId>,
323        accepted_quantization_formats: BTreeSet<QuantizationFormatId>,
324        mut dynamic_storage_bindings: Vec<ProviderStorageBindingRequirement>,
325        resource_estimator_id: impl Into<String>,
326        resource_estimator_version: ContractVersion,
327        resource_estimator_implementation_fingerprint: impl Into<String>,
328    ) -> Result<Self, VNextError> {
329        let operation_fingerprint = operation_fingerprint.into();
330        let provider_implementation_fingerprint = provider_implementation_fingerprint.into();
331        let resource_estimator_id = resource_estimator_id.into();
332        let resource_estimator_implementation_fingerprint =
333            resource_estimator_implementation_fingerprint.into();
334        dynamic_storage_bindings.sort_by_key(|binding| (binding.role(), binding.ordinal()));
335        if version.major == 0
336            || !canonical_sha256(&operation_fingerprint)
337            || !canonical_sha256(&provider_implementation_fingerprint)
338            || resource_estimator_id.is_empty()
339            || resource_estimator_id.len() > 160
340            || !resource_estimator_id.bytes().all(|byte| {
341                byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':' | b'/')
342            })
343            || resource_estimator_version.major == 0
344            || !canonical_sha256(&resource_estimator_implementation_fingerprint)
345            || dynamic_storage_bindings.is_empty()
346            || dynamic_storage_bindings.windows(2).any(|pair| {
347                (pair[0].role(), pair[0].ordinal()) == (pair[1].role(), pair[1].ordinal())
348            })
349        {
350            return Err(invalid_operation(
351                "operation provider or resource-estimator identity is invalid",
352            ));
353        }
354        Ok(Self {
355            provider_id,
356            operation_id,
357            operation_fingerprint,
358            provider_implementation_fingerprint,
359            execution_semantics,
360            checkpoint: ProviderCheckpointCapability::Unsupported,
361            version,
362            device_id,
363            capabilities,
364            accepted_weight_formats,
365            accepted_quantization_formats,
366            dynamic_storage_bindings,
367            resource_estimator_id,
368            resource_estimator_version,
369            resource_estimator_implementation_fingerprint,
370        })
371    }
372
373    pub fn provider_id(&self) -> &ProviderId {
374        &self.provider_id
375    }
376
377    pub fn operation_id(&self) -> &OperationId {
378        &self.operation_id
379    }
380
381    pub fn operation_fingerprint(&self) -> &str {
382        &self.operation_fingerprint
383    }
384
385    pub fn provider_implementation_fingerprint(&self) -> &str {
386        &self.provider_implementation_fingerprint
387    }
388
389    pub const fn execution_semantics(&self) -> ProviderExecutionSemantics {
390        self.execution_semantics
391    }
392
393    /// Declares checkpoint behavior for this exact implementation and operation
394    /// fingerprint. This does not bypass layout, state-closure, or oracle checks
395    /// when a plan derives its actual checkpoint eligibility.
396    pub fn with_checkpoint_capability(mut self, capability: ProviderCheckpointCapability) -> Self {
397        self.checkpoint = capability;
398        self
399    }
400
401    pub fn checkpoint_capability(&self) -> &ProviderCheckpointCapability {
402        &self.checkpoint
403    }
404
405    pub fn version(&self) -> ContractVersion {
406        self.version
407    }
408
409    pub fn device_id(&self) -> &DeviceId {
410        &self.device_id
411    }
412
413    pub fn capabilities(&self) -> &BTreeSet<CapabilityId> {
414        &self.capabilities
415    }
416
417    pub fn accepted_weight_formats(&self) -> &BTreeSet<WeightFormatId> {
418        &self.accepted_weight_formats
419    }
420
421    pub fn accepted_quantization_formats(&self) -> &BTreeSet<QuantizationFormatId> {
422        &self.accepted_quantization_formats
423    }
424
425    pub fn dynamic_storage_bindings(&self) -> &[ProviderStorageBindingRequirement] {
426        &self.dynamic_storage_bindings
427    }
428
429    pub fn dynamic_storage_for(
430        &self,
431        role: ResolvedValueRole,
432        ordinal: u32,
433    ) -> Option<&DynamicStorageRequirement> {
434        self.dynamic_storage_bindings
435            .binary_search_by_key(&(role, ordinal), |binding| {
436                (binding.role(), binding.ordinal())
437            })
438            .ok()
439            .map(|index| self.dynamic_storage_bindings[index].storage())
440    }
441
442    pub fn resource_estimator_id(&self) -> &str {
443        &self.resource_estimator_id
444    }
445
446    pub const fn resource_estimator_version(&self) -> ContractVersion {
447        self.resource_estimator_version
448    }
449
450    pub fn resource_estimator_implementation_fingerprint(&self) -> &str {
451        &self.resource_estimator_implementation_fingerprint
452    }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
456pub struct EngineProviderDescriptor {
457    provider_id: ProviderId,
458    contract_version: ContractVersion,
459    implementation_fingerprint: String,
460    device_id: DeviceId,
461    capabilities: BTreeSet<CapabilityId>,
462}
463
464#[derive(Deserialize)]
465#[serde(deny_unknown_fields)]
466struct EngineProviderDescriptorWire {
467    provider_id: ProviderId,
468    contract_version: ContractVersion,
469    implementation_fingerprint: String,
470    device_id: DeviceId,
471    capabilities: BTreeSet<CapabilityId>,
472}
473
474impl<'de> Deserialize<'de> for EngineProviderDescriptor {
475    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
476    where
477        D: Deserializer<'de>,
478    {
479        let wire = EngineProviderDescriptorWire::deserialize(deserializer)?;
480        Self::new(
481            wire.provider_id,
482            wire.contract_version,
483            wire.implementation_fingerprint,
484            wire.device_id,
485            wire.capabilities,
486        )
487        .map_err(serde::de::Error::custom)
488    }
489}
490
491impl EngineProviderDescriptor {
492    pub fn new(
493        provider_id: ProviderId,
494        contract_version: ContractVersion,
495        implementation_fingerprint: impl Into<String>,
496        device_id: DeviceId,
497        capabilities: BTreeSet<CapabilityId>,
498    ) -> Result<Self, VNextError> {
499        let implementation_fingerprint = implementation_fingerprint.into();
500        if contract_version.major == 0 || !canonical_sha256(&implementation_fingerprint) {
501            return Err(invalid_operation(
502                "engine provider contract version or implementation fingerprint is invalid",
503            ));
504        }
505        Ok(Self {
506            provider_id,
507            contract_version,
508            implementation_fingerprint,
509            device_id,
510            capabilities,
511        })
512    }
513
514    pub fn provider_id(&self) -> &ProviderId {
515        &self.provider_id
516    }
517
518    pub const fn contract_version(&self) -> ContractVersion {
519        self.contract_version
520    }
521
522    pub fn implementation_fingerprint(&self) -> &str {
523        &self.implementation_fingerprint
524    }
525
526    pub fn device_id(&self) -> &DeviceId {
527        &self.device_id
528    }
529
530    pub fn capabilities(&self) -> &BTreeSet<CapabilityId> {
531        &self.capabilities
532    }
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
536pub struct ProviderCompatibilityRequest {
537    operation_id: OperationId,
538    required_version: ContractVersion,
539    required_capabilities: BTreeSet<CapabilityId>,
540    required_weight_formats: BTreeSet<WeightFormatId>,
541    required_quantization_formats: BTreeSet<QuantizationFormatId>,
542    execution_determinism: ExecutionDeterminismRequirement,
543}
544
545#[derive(Deserialize)]
546#[serde(deny_unknown_fields)]
547struct ProviderCompatibilityRequestWire {
548    operation_id: OperationId,
549    required_version: ContractVersion,
550    required_capabilities: BTreeSet<CapabilityId>,
551    required_weight_formats: BTreeSet<WeightFormatId>,
552    required_quantization_formats: BTreeSet<QuantizationFormatId>,
553    execution_determinism: ExecutionDeterminismRequirement,
554}
555
556impl<'de> Deserialize<'de> for ProviderCompatibilityRequest {
557    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
558    where
559        D: Deserializer<'de>,
560    {
561        let wire = ProviderCompatibilityRequestWire::deserialize(deserializer)?;
562        Self::new(
563            wire.operation_id,
564            wire.required_version,
565            wire.required_capabilities,
566            wire.required_weight_formats,
567            wire.required_quantization_formats,
568            wire.execution_determinism,
569        )
570        .map_err(serde::de::Error::custom)
571    }
572}
573
574impl ProviderCompatibilityRequest {
575    pub fn new(
576        operation_id: OperationId,
577        required_version: ContractVersion,
578        required_capabilities: BTreeSet<CapabilityId>,
579        required_weight_formats: BTreeSet<WeightFormatId>,
580        required_quantization_formats: BTreeSet<QuantizationFormatId>,
581        execution_determinism: ExecutionDeterminismRequirement,
582    ) -> Result<Self, VNextError> {
583        if required_version.major == 0 {
584            return Err(invalid_operation(
585                "provider compatibility request has a zero major version",
586            ));
587        }
588        Ok(Self {
589            operation_id,
590            required_version,
591            required_capabilities,
592            required_weight_formats,
593            required_quantization_formats,
594            execution_determinism,
595        })
596    }
597
598    pub fn operation_id(&self) -> &OperationId {
599        &self.operation_id
600    }
601
602    pub const fn required_version(&self) -> ContractVersion {
603        self.required_version
604    }
605
606    pub fn required_capabilities(&self) -> &BTreeSet<CapabilityId> {
607        &self.required_capabilities
608    }
609
610    pub fn required_weight_formats(&self) -> &BTreeSet<WeightFormatId> {
611        &self.required_weight_formats
612    }
613
614    pub fn required_quantization_formats(&self) -> &BTreeSet<QuantizationFormatId> {
615        &self.required_quantization_formats
616    }
617
618    pub const fn execution_determinism(&self) -> ExecutionDeterminismRequirement {
619        self.execution_determinism
620    }
621
622    pub(super) fn extend_required_capabilities(
623        &mut self,
624        capabilities: impl IntoIterator<Item = CapabilityId>,
625    ) {
626        self.required_capabilities.extend(capabilities);
627    }
628}
629
630#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
631#[serde(rename_all = "snake_case")]
632pub enum ProviderCompatibilityRejectReason {
633    OperationVersionMismatch {
634        required: ContractVersion,
635        available: ContractVersion,
636    },
637    ProviderVersionMismatch {
638        required: ContractVersion,
639        available: ContractVersion,
640    },
641    MissingCapabilities {
642        capabilities: BTreeSet<CapabilityId>,
643    },
644    UnsupportedWeightFormats {
645        formats: BTreeSet<WeightFormatId>,
646    },
647    UnsupportedQuantizationFormats {
648        formats: BTreeSet<QuantizationFormatId>,
649    },
650    InsufficientExecutionDeterminism {
651        required: ExecutionDeterminismRequirement,
652        available: ProviderExecutionSemantics,
653    },
654}
655
656#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
657pub struct ProviderCompatibilityRejection {
658    pub provider_id: ProviderId,
659    pub reasons: Vec<ProviderCompatibilityRejectReason>,
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
663pub struct ProviderCompatibilityReport {
664    request: ProviderCompatibilityRequest,
665    compatible_provider_ids: Vec<ProviderId>,
666    rejected: Vec<ProviderCompatibilityRejection>,
667}
668
669#[derive(Deserialize)]
670#[serde(deny_unknown_fields)]
671struct ProviderCompatibilityReportWire {
672    request: ProviderCompatibilityRequest,
673    compatible_provider_ids: Vec<ProviderId>,
674    rejected: Vec<ProviderCompatibilityRejection>,
675}
676
677impl<'de> Deserialize<'de> for ProviderCompatibilityReport {
678    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
679    where
680        D: Deserializer<'de>,
681    {
682        let wire = ProviderCompatibilityReportWire::deserialize(deserializer)?;
683        let report = Self {
684            request: wire.request,
685            compatible_provider_ids: wire.compatible_provider_ids,
686            rejected: wire.rejected,
687        };
688        report.validate_shape().map_err(serde::de::Error::custom)?;
689        Ok(report)
690    }
691}
692
693impl ProviderCompatibilityReport {
694    pub(super) fn from_classification(
695        request: ProviderCompatibilityRequest,
696        compatible_provider_ids: Vec<ProviderId>,
697        rejected: Vec<ProviderCompatibilityRejection>,
698    ) -> Result<Self, VNextError> {
699        let report = Self {
700            request,
701            compatible_provider_ids,
702            rejected,
703        };
704        report.validate_shape()?;
705        Ok(report)
706    }
707
708    fn rejection_summary(&self) -> String {
709        serde_json::to_string(&self.rejected)
710            .map(|rejected| format!("all providers were rejected: {rejected}"))
711            .unwrap_or_else(|_| "all providers were rejected".to_owned())
712    }
713
714    fn validate_shape(&self) -> Result<(), VNextError> {
715        let compatible = self.compatible_provider_ids.iter().collect::<BTreeSet<_>>();
716        let rejected = self
717            .rejected
718            .iter()
719            .map(|rejection| &rejection.provider_id)
720            .collect::<BTreeSet<_>>();
721        if compatible.len() != self.compatible_provider_ids.len()
722            || rejected.len() != self.rejected.len()
723            || (compatible.is_empty() && rejected.is_empty())
724            || !compatible.is_disjoint(&rejected)
725            || self
726                .rejected
727                .iter()
728                .any(|rejection| rejection.reasons.is_empty())
729            || self
730                .compatible_provider_ids
731                .windows(2)
732                .any(|pair| pair[0] >= pair[1])
733            || self
734                .rejected
735                .windows(2)
736                .any(|pair| pair[0].provider_id >= pair[1].provider_id)
737        {
738            return Err(invalid_operation(
739                "provider compatibility report is duplicate, overlapping, empty, or non-canonical",
740            ));
741        }
742        Ok(())
743    }
744
745    pub fn request(&self) -> &ProviderCompatibilityRequest {
746        &self.request
747    }
748
749    pub fn compatible_provider_ids(&self) -> &[ProviderId] {
750        &self.compatible_provider_ids
751    }
752
753    pub fn rejected(&self) -> &[ProviderCompatibilityRejection] {
754        &self.rejected
755    }
756
757    pub fn require_compatible(&self, device_id: &DeviceId) -> Result<(), VNextError> {
758        if self.compatible_provider_ids.is_empty() {
759            return Err(VNextError::UnsupportedOperation {
760                node_id: None,
761                operation_id: self.request.operation_id.to_string(),
762                device_id: device_id.to_string(),
763                reason: self.rejection_summary(),
764            });
765        }
766        Ok(())
767    }
768
769    /// Requires one compatible provider while retaining the plan node that
770    /// caused a missing capability or version failure.
771    pub fn require_compatible_for_node(
772        &self,
773        device_id: &DeviceId,
774        node_id: &NodeId,
775    ) -> Result<(), VNextError> {
776        if !self.compatible_provider_ids.is_empty() {
777            return Ok(());
778        }
779        let operation_version_mismatch = self
780            .rejected
781            .iter()
782            .flat_map(|rejection| &rejection.reasons)
783            .find_map(|reason| match reason {
784                ProviderCompatibilityRejectReason::OperationVersionMismatch {
785                    required,
786                    available,
787                } => Some((*required, *available)),
788                _ => None,
789            });
790        let provider_version_mismatch = self
791            .rejected
792            .iter()
793            .map(|rejection| {
794                rejection.reasons.iter().find_map(|reason| match reason {
795                    ProviderCompatibilityRejectReason::ProviderVersionMismatch {
796                        required,
797                        available,
798                    } => Some((*required, *available)),
799                    _ => None,
800                })
801            })
802            .collect::<Option<Vec<_>>>()
803            .and_then(|versions| {
804                versions
805                    .into_iter()
806                    .max_by_key(|(_, available)| (available.major, available.minor))
807            });
808        if let Some((required, available)) =
809            operation_version_mismatch.or(provider_version_mismatch)
810        {
811            return Err(VNextError::IncompatibleOperationVersion {
812                node_id: Some(node_id.to_string()),
813                operation_id: self.request.operation_id.to_string(),
814                required_major: required.major,
815                required_minor: required.minor,
816                available_major: available.major,
817                available_minor: available.minor,
818            });
819        }
820        Err(VNextError::UnsupportedOperation {
821            node_id: Some(node_id.to_string()),
822            operation_id: self.request.operation_id.to_string(),
823            device_id: device_id.to_string(),
824            reason: self.rejection_summary(),
825        })
826    }
827}
828
829#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
830pub struct OperationFailure {
831    identity: ExecutionIdentityEnvelope,
832    phase: ProfilePhase,
833    code: String,
834    message: String,
835    retryable: bool,
836}
837
838#[derive(Debug, Clone, PartialEq, Eq)]
839pub struct UnvalidatedOperationFailure {
840    identity: UnvalidatedExecutionIdentityParts,
841    phase: ProfilePhase,
842    code: String,
843    message: String,
844    retryable: bool,
845}
846
847#[derive(Deserialize)]
848#[serde(deny_unknown_fields)]
849struct UnvalidatedOperationFailureWire {
850    identity: UnvalidatedExecutionIdentityParts,
851    phase: ProfilePhase,
852    code: String,
853    message: String,
854    retryable: bool,
855}
856
857impl From<UnvalidatedOperationFailureWire> for UnvalidatedOperationFailure {
858    fn from(wire: UnvalidatedOperationFailureWire) -> Self {
859        Self {
860            identity: wire.identity,
861            phase: wire.phase,
862            code: wire.code,
863            message: wire.message,
864            retryable: wire.retryable,
865        }
866    }
867}
868
869impl UnvalidatedOperationFailure {
870    pub fn revalidate(
871        self,
872        expected_identity: &ExecutionIdentityEnvelope,
873        expected_phase: ProfilePhase,
874    ) -> Result<OperationFailure, VNextError> {
875        let identity = ExecutionIdentityEnvelope::new(self.identity.into())?;
876        if &identity != expected_identity || self.phase != expected_phase {
877            return Err(invalid_operation(
878                "serialized operation failure differs from the expected execution context",
879            ));
880        }
881        OperationFailure::new(
882            identity,
883            self.phase,
884            self.code,
885            self.message,
886            self.retryable,
887        )
888    }
889}
890
891impl OperationFailure {
892    pub fn new(
893        identity: ExecutionIdentityEnvelope,
894        phase: ProfilePhase,
895        code: impl Into<String>,
896        message: impl Into<String>,
897        retryable: bool,
898    ) -> Result<Self, VNextError> {
899        let code = code.into();
900        let message = message.into();
901        let parts = identity.parts();
902        if parts.frame_id.is_none()
903            || parts.node_invocation_id.is_none()
904            || parts.node_id.is_none()
905            || parts.operation_id.is_none()
906            || parts.provider_id.is_none()
907            || parts.device_id.is_none()
908            || parts.plan_id.is_none()
909            || parts.plan_hash.is_none()
910            || parts.transaction_id.is_none()
911            || parts.resource_pool_id.is_none()
912            || parts.resource_pool_identity_fingerprint.is_none()
913            || parts.provisioning_run_id.is_none()
914            || parts.provisioning_request_id.is_none()
915            || parts.active_sequence_slot.is_none()
916            || parts.admission_generation.is_none()
917            || parts.activation_epoch.is_none()
918            || parts.runtime_implementation_fingerprint.is_none()
919            || parts.active_sequence_fingerprint.is_none()
920            || parts.completed_sequence_fingerprint.is_some()
921            || parts.aborted_sequence_fingerprint.is_some()
922            || parts.resource_id.is_some()
923            || parts.resource_generation.is_some()
924            || parts.resource_batch_fingerprint.is_some()
925            || code.trim().is_empty()
926            || message.trim().is_empty()
927            || code.len() > 64
928            || message
929                .bytes()
930                .any(|byte| byte.is_ascii_control() && !matches!(byte, b'\n' | b'\t'))
931            || message.len() > 4096
932            || !code
933                .bytes()
934                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
935        {
936            return Err(invalid_operation(
937                "operation failure requires complete execution identity, code, and message",
938            ));
939        }
940        Ok(Self {
941            identity,
942            phase,
943            code,
944            message,
945            retryable,
946        })
947    }
948
949    pub fn identity(&self) -> &ExecutionIdentityEnvelope {
950        &self.identity
951    }
952
953    pub const fn phase(&self) -> ProfilePhase {
954        self.phase
955    }
956
957    pub fn code(&self) -> &str {
958        &self.code
959    }
960
961    pub fn message(&self) -> &str {
962        &self.message
963    }
964
965    pub const fn retryable(&self) -> bool {
966        self.retryable
967    }
968
969    pub fn decode_untrusted(bytes: &[u8]) -> Result<UnvalidatedOperationFailure, VNextError> {
970        if bytes.len() > MAX_OPERATION_FAILURE_WIRE_BYTES {
971            return Err(VNextError::Serialization {
972                context: "decode untrusted operation failure",
973                message: format!(
974                    "operation failure wire size {} exceeds limit {}",
975                    bytes.len(),
976                    MAX_OPERATION_FAILURE_WIRE_BYTES
977                ),
978            });
979        }
980        serde_json::from_slice::<UnvalidatedOperationFailureWire>(bytes)
981            .map(UnvalidatedOperationFailure::from)
982            .map_err(|error| VNextError::Serialization {
983                context: "decode untrusted operation failure",
984                message: error.to_string(),
985            })
986    }
987}