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